]> git.ipfire.org Git - thirdparty/chrony.git/commitdiff
nts: add NTS-KE server and client
authorMiroslav Lichvar <mlichvar@redhat.com>
Tue, 4 Feb 2020 14:10:14 +0000 (15:10 +0100)
committerMiroslav Lichvar <mlichvar@redhat.com>
Thu, 5 Mar 2020 15:02:15 +0000 (16:02 +0100)
Add a client and server implementing the Network Time Security (NTS) Key
Establishment. Use the GnuTLS library for TLS.

12 files changed:
conf.c
conf.h
configure
main.c
nts_ke.h [new file with mode: 0644]
nts_ke_client.c [new file with mode: 0644]
nts_ke_client.h [new file with mode: 0644]
nts_ke_server.c [new file with mode: 0644]
nts_ke_server.h [new file with mode: 0644]
nts_ke_session.c [new file with mode: 0644]
nts_ke_session.h [new file with mode: 0644]
stubs.c

diff --git a/conf.c b/conf.c
index a5525ce3eb625193b4c8e626a532ce5461416704..6d79c04c3698d93aff54fb3680c1945c57cf3b50 100644 (file)
--- a/conf.c
+++ b/conf.c
@@ -223,6 +223,19 @@ static char *leapsec_tz = NULL;
 /* Name of the user to which will be dropped root privileges. */
 static char *user;
 
+/* NTS cache dir, certificates, private key, and port */
+static char *nts_cachedir = NULL;
+static char *nts_server_cert_file = NULL;
+static char *nts_server_key_file = NULL;
+static int nts_server_port = 11443;
+static int nts_server_processes = 1;
+static int nts_server_connections = 100;
+static int nts_rotate = 604800; /* 1 week */
+static char *nts_trusted_cert_file = NULL;
+
+/* Flag disabling use of system trusted certificates */
+static int no_system_cert = 0;
+
 /* Array of CNF_HwTsInterface */
 static ARR_Instance hwts_interfaces;
 
@@ -390,6 +403,10 @@ CNF_Finalise(void)
   Free(mail_user_on_change);
   Free(tempcomp_sensor_file);
   Free(tempcomp_point_file);
+  Free(nts_cachedir);
+  Free(nts_server_cert_file);
+  Free(nts_server_key_file);
+  Free(nts_trusted_cert_file);
 }
 
 /* ================================================== */
@@ -513,6 +530,8 @@ CNF_ParseLine(const char *filename, int number, char *line)
     parse_double(p, &max_drift);
   } else if (!strcasecmp(command, "maxjitter")) {
     parse_double(p, &max_jitter);
+  } else if (!strcasecmp(command, "maxntsconnections")) {
+    parse_int(p, &nts_server_connections);
   } else if (!strcasecmp(command, "maxsamples")) {
     parse_int(p, &max_samples);
   } else if (!strcasecmp(command, "maxslewrate")) {
@@ -525,8 +544,24 @@ CNF_ParseLine(const char *filename, int number, char *line)
     parse_int(p, &min_sources);
   } else if (!strcasecmp(command, "noclientlog")) {
     no_client_log = parse_null(p);
+  } else if (!strcasecmp(command, "nosystemcert")) {
+    no_system_cert = parse_null(p);
   } else if (!strcasecmp(command, "ntpsigndsocket")) {
     parse_string(p, &ntp_signd_socket);
+  } else if (!strcasecmp(command, "ntstrustedcerts")) {
+    parse_string(p, &nts_trusted_cert_file);
+  } else if (!strcasecmp(command, "ntscachedir")) {
+    parse_string(p, &nts_cachedir);
+  } else if (!strcasecmp(command, "ntsport")) {
+    parse_int(p, &nts_server_port);
+  } else if (!strcasecmp(command, "ntsprocesses")) {
+    parse_int(p, &nts_server_processes);
+  } else if (!strcasecmp(command, "ntsrotate")) {
+    parse_int(p, &nts_rotate);
+  } else if (!strcasecmp(command, "ntsservercert")) {
+    parse_string(p, &nts_server_cert_file);
+  } else if (!strcasecmp(command, "ntsserverkey")) {
+    parse_string(p, &nts_server_key_file);
   } else if (!strcasecmp(command, "peer")) {
     parse_source(p, NTP_PEER, 0);
   } else if (!strcasecmp(command, "pidfile")) {
@@ -2027,3 +2062,75 @@ CNF_GetHwTsInterface(unsigned int index, CNF_HwTsInterface **iface)
   *iface = (CNF_HwTsInterface *)ARR_GetElement(hwts_interfaces, index);
   return 1;
 }
+
+/* ================================================== */
+
+char *
+CNF_GetNtsCacheDir(void)
+{
+  return nts_cachedir;
+}
+
+/* ================================================== */
+
+char *
+CNF_GetNtsServerCertFile(void)
+{
+  return nts_server_cert_file;
+}
+
+/* ================================================== */
+
+char *
+CNF_GetNtsServerKeyFile(void)
+{
+  return nts_server_key_file;
+}
+
+/* ================================================== */
+
+int
+CNF_GetNtsServerPort(void)
+{
+  return nts_server_port;
+}
+
+/* ================================================== */
+
+int
+CNF_GetNtsServerProcesses(void)
+{
+  return nts_server_processes;
+}
+
+/* ================================================== */
+
+int
+CNF_GetNtsServerConnections(void)
+{
+  return nts_server_connections;
+}
+
+/* ================================================== */
+
+int
+CNF_GetNtsRotate(void)
+{
+  return nts_rotate;
+}
+
+/* ================================================== */
+
+char *
+CNF_GetNtsTrustedCertFile(void)
+{
+  return nts_trusted_cert_file;
+}
+
+/* ================================================== */
+
+int
+CNF_GetNoSystemCert(void)
+{
+  return no_system_cert;
+}
diff --git a/conf.h b/conf.h
index 43217fc6a97b388eb297860112a6f707c7d8325d..cc3479104175c33a9c5e9bbdfa6150b45e0c4cb5 100644 (file)
--- a/conf.h
+++ b/conf.h
@@ -139,4 +139,14 @@ typedef struct {
 
 extern int CNF_GetHwTsInterface(unsigned int index, CNF_HwTsInterface **iface);
 
+extern char *CNF_GetNtsCacheDir(void);
+extern char *CNF_GetNtsServerCertFile(void);
+extern char *CNF_GetNtsServerKeyFile(void);
+extern int CNF_GetNtsServerPort(void);
+extern int CNF_GetNtsServerProcesses(void);
+extern int CNF_GetNtsServerConnections(void);
+extern int CNF_GetNtsRotate(void);
+extern char *CNF_GetNtsTrustedCertFile(void);
+extern int CNF_GetNoSystemCert(void);
+
 #endif /* GOT_CONF_H */
index 597fd4dcbd3a04f8dbbf35f77f43d92ca50bd801..5af221e1116e8e1c209fb9ff16a54634a078ddd4 100755 (executable)
--- a/configure
+++ b/configure
@@ -89,6 +89,8 @@ For better control, use the options below.
   --without-nettle       Don't use nettle even if it is available
   --without-nss          Don't use NSS even if it is available
   --without-tomcrypt     Don't use libtomcrypt even if it is available
+  --disable-nts          Disable NTS support
+  --without-gnutls       Don't use gnutls even if it is available
   --disable-cmdmon       Disable command and monitoring support
   --disable-ntp          Disable NTP support
   --disable-refclock     Disable reference clock support
@@ -203,6 +205,8 @@ feat_sechash=1
 try_nettle=1
 try_nss=1
 try_tomcrypt=1
+feat_nts=1
+try_gnutls=1
 feat_rtc=1
 try_rtc=0
 feat_droproot=1
@@ -373,6 +377,12 @@ do
     --without-tomcrypt )
       try_tomcrypt=0
     ;;
+    --disable-nts )
+      feat_nts=0
+    ;;
+    --without-gnutls )
+      try_gnutls=0
+    ;;
     --host-system=* )
       OPERATINGSYSTEM=`echo $option | sed -e 's/^.*=//;'`
     ;;
@@ -920,18 +930,28 @@ EXTRA_OBJECTS="$EXTRA_OBJECTS $HASH_OBJ"
 EXTRA_CLI_OBJECTS="$EXTRA_CLI_OBJECTS $HASH_OBJ"
 LIBS="$LIBS $HASH_LINK"
 
-if true && \
+if [ $feat_ntp = "1" ] && [ $feat_nts = "1" ] && [ $try_gnutls = "1" ] && \
     echo "$HASH_LINK" | grep 'nettle' > /dev/null; then
-
+  test_cflags="`pkg_config --cflags gnutls`"
+  test_link="`pkg_config --libs gnutls`"
+  if test_code 'gnutls' 'gnutls/gnutls.h' \
+    "$test_cflags" "$test_link" \
+    'gnutls_init(NULL, 0);'
+  then
+    EXTRA_OBJECTS="$EXTRA_OBJECTS nts_ke_client.o nts_ke_server.o nts_ke_session.o"
     EXTRA_OBJECTS="$EXTRA_OBJECTS siv_nettle.o"
-    add_def HAVE_SIV
+    LIBS="$LIBS $test_link"
+    MYCPPFLAGS="$MYCPPFLAGS $test_cflags"
+    add_def FEAT_NTS
 
+    add_def HAVE_SIV
     if test_code 'SIV in nettle' \
       'nettle/siv-cmac.h' "" "$LIBS" \
       'siv_cmac_aes128_set_key(NULL, NULL);'
     then
       add_def HAVE_NETTLE_SIV_CMAC
     fi
+  fi
 fi
 
 if [ $use_pthread = "1" ]; then
@@ -1003,7 +1023,7 @@ add_def MAIL_PROGRAM "\"$mail_program\""
 
 common_features="`get_features SECHASH IPV6 DEBUG`"
 chronyc_features="`get_features READLINE`"
-chronyd_features="`get_features CMDMON NTP REFCLOCK RTC PRIVDROP SCFILTER SIGND ASYNCDNS`"
+chronyd_features="`get_features CMDMON NTP REFCLOCK RTC PRIVDROP SCFILTER SIGND ASYNCDNS NTS`"
 add_def CHRONYC_FEATURES "\"$chronyc_features $common_features\""
 add_def CHRONYD_FEATURES "\"$chronyd_features $common_features\""
 echo "Features : $chronyd_features $chronyc_features $common_features"
diff --git a/main.c b/main.c
index b32cd52905eb0cd5d898087ca2a43221e28917c9..b08d7bd4be907f60cb74aaea40b6c35ed681a9c2 100644 (file)
--- a/main.c
+++ b/main.c
@@ -38,6 +38,8 @@
 #include "ntp_signd.h"
 #include "ntp_sources.h"
 #include "ntp_core.h"
+#include "nts_ke_client.h"
+#include "nts_ke_server.h"
 #include "socket.h"
 #include "sources.h"
 #include "sourcestats.h"
@@ -113,6 +115,8 @@ MAI_CleanupAndExit(void)
   TMC_Finalise();
   MNL_Finalise();
   CLG_Finalise();
+  NKC_Finalise();
+  NKS_Finalise();
   NSD_Finalise();
   NSR_Finalise();
   SST_Finalise();
@@ -583,6 +587,8 @@ int main
   SST_Initialise();
   NSR_Initialise();
   NSD_Initialise();
+  NKS_Initialise(scfilter_level);
+  NKC_Initialise();
   CLG_Initialise();
   MNL_Initialise();
   TMC_Initialise();
diff --git a/nts_ke.h b/nts_ke.h
new file mode 100644 (file)
index 0000000..f00fcef
--- /dev/null
+++ b/nts_ke.h
@@ -0,0 +1,69 @@
+/*
+  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.
+ * 
+ **********************************************************************
+
+  =======================================================================
+
+  Header file for the NTS Key Establishment protocol
+  */
+
+#ifndef GOT_NTS_KE_H
+#define GOT_NTS_KE_H
+
+#include "siv.h"
+
+#define NKE_RECORD_CRITICAL_BIT         (1U << 15)
+#define NKE_RECORD_END_OF_MESSAGE       0
+#define NKE_RECORD_NEXT_PROTOCOL        1
+#define NKE_RECORD_ERROR                2
+#define NKE_RECORD_WARNING              3
+#define NKE_RECORD_AEAD_ALGORITHM       4
+#define NKE_RECORD_COOKIE               5
+#define NKE_RECORD_NTPV4_SERVER_NEGOTIATION 6
+#define NKE_RECORD_NTPV4_PORT_NEGOTIATION 7
+
+#define NKE_NEXT_PROTOCOL_NTPV4         0
+
+#define NKE_ERROR_UNRECOGNIZED_CRITICAL_RECORD 0
+#define NKE_ERROR_BAD_REQUEST           1
+#define NKE_ERROR_INTERNAL_SERVER_ERROR 2
+
+#define NKE_ALPN_NAME                   "ntske/1"
+#define NKE_EXPORTER_LABEL              "EXPORTER-network-time-security/1"
+#define NKE_EXPORTER_CONTEXT_C2S        "\x0\x0\x0\xf\x0"
+#define NKE_EXPORTER_CONTEXT_S2C        "\x0\x0\x0\xf\x1"
+
+#define NKE_MAX_MESSAGE_LENGTH          16384
+#define NKE_MAX_RECORD_BODY_LENGTH      256
+#define NKE_MAX_COOKIE_LENGTH           256
+#define NKE_MAX_COOKIES                 8
+#define NKE_MAX_KEY_LENGTH SIV_MAX_KEY_LENGTH
+
+typedef struct {
+  int length;
+  unsigned char key[NKE_MAX_KEY_LENGTH];
+} NKE_Key;
+
+typedef struct {
+  int length;
+  unsigned char cookie[NKE_MAX_COOKIE_LENGTH];
+} NKE_Cookie;
+
+#endif
diff --git a/nts_ke_client.c b/nts_ke_client.c
new file mode 100644 (file)
index 0000000..090d713
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+  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 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_Instance session;
+  int destroying;
+  int got_response;
+  int resolving_name;
+
+  SIV_Algorithm siv_algorithm;
+  NKE_Key c2s, s2c;
+  NKE_Cookie cookies[NKE_MAX_COOKIES];
+  int num_cookies;
+  char server_name[NKE_MAX_RECORD_BODY_LENGTH + 1];
+  IPSockAddr ntp_address;
+};
+
+/* ================================================== */
+
+static void *client_credentials;
+
+/* ================================================== */
+
+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) {
+    NKC_DestroyInstance(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 of the same family as NTS-KE */
+  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 datum;
+
+  NKSN_BeginMessage(session);
+
+  datum = htons(NKE_NEXT_PROTOCOL_NTPV4);
+  if (!NKSN_AddRecord(session, 1, NKE_RECORD_NEXT_PROTOCOL, &datum, sizeof (datum)))
+    return 0;
+
+  datum = htons(AEAD_AES_SIV_CMAC_256);
+  if (!NKSN_AddRecord(session, 1, NKE_RECORD_AEAD_ALGORITHM, &datum, sizeof (datum)))
+    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_COOKIE_LENGTH / sizeof (uint16_t)];
+
+  assert(NKE_MAX_COOKIE_LENGTH % 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;
+
+    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) {
+          DEBUG_LOG("Unexpected NTS-KE AEAD algorithm");
+          error = 1;
+          break;
+        }
+        aead_algorithm = AEAD_AES_SIV_CMAC_256;
+        inst->siv_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 #%d length=%d", inst->num_cookies + 1, length);
+        assert(NKE_MAX_COOKIE_LENGTH == sizeof (inst->cookies[inst->num_cookies].cookie));
+        if (length <= NKE_MAX_COOKIE_LENGTH && inst->num_cookies < NKE_MAX_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(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 != AEAD_AES_SIV_CMAC_256)
+    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->siv_algorithm, &inst->c2s, &inst->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)) {
+      DNS_Name2IPAddressAsync(inst->server_name, name_resolve_handler, inst);
+      inst->resolving_name = 1;
+    }
+  }
+
+  inst->got_response = 1;
+
+  return 1;
+}
+
+/* ================================================== */
+
+void
+NKC_Initialise(void)
+{
+  client_credentials = NULL;
+}
+
+/* ================================================== */
+
+void
+NKC_Finalise(void)
+{
+  if (client_credentials)
+    NKSN_DestroyCertCredentials(client_credentials);
+}
+
+/* ================================================== */
+
+NKC_Instance
+NKC_CreateInstance(IPSockAddr *address, const char *name)
+{
+  NKC_Instance inst;
+
+  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;
+
+  /* Create the credentials with the first client instance and share them
+     with other instances */
+  if (!client_credentials)
+    client_credentials = NKSN_CreateCertCredentials(NULL, NULL, CNF_GetNtsTrustedCertFile());
+
+  return inst;
+}
+
+/* ================================================== */
+
+void
+NKC_DestroyInstance(NKC_Instance inst)
+{
+  /* If the resolver is running, destroy the instance later when finished */
+  if (inst->resolving_name) {
+    inst->destroying = 1;
+    return;
+  }
+
+  NKSN_DestroyInstance(inst->session);
+
+  Free(inst->name);
+  Free(inst);
+}
+
+/* ================================================== */
+
+int
+NKC_Start(NKC_Instance inst)
+{
+  IPSockAddr local_addr;
+  int sock_fd;
+
+  assert(!NKC_IsActive(inst));
+
+  if (!client_credentials) {
+    DEBUG_LOG("Missing client credentials");
+    return 0;
+  }
+
+  /* Follow the bindacqaddress setting */
+  CNF_GetBindAcquisitionAddress(inst->address.ip_addr.family, &local_addr.ip_addr);
+  if (local_addr.ip_addr.family != inst->address.ip_addr.family)
+    SCK_GetAnyLocalIPAddress(inst->address.ip_addr.family, &local_addr.ip_addr);
+
+  local_addr.port = 0;
+
+  sock_fd = SCK_OpenTcpSocket(&inst->address, &local_addr, 0);
+  if (sock_fd < 0)
+    return 0;
+
+  /* Start a NTS-KE session */
+  if (!NKSN_StartSession(inst->session, sock_fd, client_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,
+               SIV_Algorithm *siv_algorithm, NKE_Key *c2s, NKE_Key *s2c,
+               NKE_Cookie *cookies, int *num_cookies, int max_cookies,
+               IPSockAddr *ntp_address)
+{
+  int i;
+
+  if (!inst->got_response || inst->resolving_name)
+    return 0;
+
+  *siv_algorithm = inst->siv_algorithm;
+  *c2s = inst->c2s;
+  *s2c = inst->s2c;
+
+  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 i;
+}
diff --git a/nts_ke_client.h b/nts_ke_client.h
new file mode 100644 (file)
index 0000000..8d58775
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+  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.
+ * 
+ **********************************************************************
+
+  =======================================================================
+
+  Header file for the NTS-KE client
+  */
+
+#ifndef GOT_NTS_KE_CLIENT_H
+#define GOT_NTS_KE_CLIENT_H
+
+#include "addressing.h"
+#include "nts_ke.h"
+
+typedef struct NKC_Instance_Record *NKC_Instance;
+
+/* Init and fini functions */
+extern void NKC_Initialise(void);
+extern void NKC_Finalise(void);
+
+/* Create a client NTS-KE instance */
+extern NKC_Instance NKC_CreateInstance(IPSockAddr *address, const char *name);
+
+/* Destroy an instance */
+extern void NKC_DestroyInstance(NKC_Instance inst);
+
+/* Connect to the server, start an NTS-KE session, send an NTS-KE request, and
+   process the response (asynchronously) */
+extern int NKC_Start(NKC_Instance inst);
+
+/* Check if the client is still running */
+extern int NKC_IsActive(NKC_Instance inst);
+
+/* Get the NTS data if the session was successful */
+extern int NKC_GetNtsData(NKC_Instance inst,
+                          SIV_Algorithm *siv_algorithm, NKE_Key *c2s, NKE_Key *s2c,
+                          NKE_Cookie *cookies, int *num_cookies, int max_cookies,
+                          IPSockAddr *ntp_address);
+
+#endif
diff --git a/nts_ke_server.c b/nts_ke_server.c
new file mode 100644 (file)
index 0000000..14bb621
--- /dev/null
@@ -0,0 +1,785 @@
+/*
+  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 "logging.h"
+#include "memory.h"
+#include "ntp_core.h"
+#include "nts_ke_session.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 MIN_KEY_ROTATE_INTERVAL 1.0
+
+#define INVALID_SOCK_FD (-7)
+
+typedef struct {
+  uint32_t key_id;
+  uint8_t 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 int server_sock_fd4;
+static int server_sock_fd6;
+
+static int helper_sock_fd;
+
+static int initialised = 0;
+
+/* Array of NKSN instances */
+static ARR_Instance sessions;
+static void *server_credentials;
+
+/* ================================================== */
+
+static int handle_message(void *arg);
+
+/* ================================================== */
+
+static int
+handle_client(int sock_fd, IPSockAddr *addr)
+{
+  NKSN_Instance inst, *instp;
+  int i;
+
+  if (sock_fd > FD_SETSIZE / 2) {
+    DEBUG_LOG("Rejected connection from %s (%s)",
+              UTI_IPSockAddrToString(addr), "too many descriptors");
+    return 0;
+  }
+
+  /* Find a slot which is free or has a 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, UTI_IPSockAddrToString(addr), 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;
+  }
+
+  if (!NKSN_StartSession(inst, sock_fd, 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;
+
+  if (!SCK_ReceiveMessage(fd, &message, SCK_FLAG_MSG_DESCRIPTOR))
+    return;
+
+  sock_fd = message.descriptor;
+  if (sock_fd < 0) {
+    /* Message with no descriptor is a shutdown command */
+    SCH_QuitProgram();
+    return;
+  }
+
+  if (message.length != sizeof (HelperRequest)) {
+    DEBUG_LOG("Unexpected message length");
+    SCK_CloseSocket(sock_fd);
+    return;
+  }
+
+  req = message.data;
+
+  /* Extract the server key and client address from the request */
+  server_keys[current_server_key].id = ntohl(req->key_id);
+  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)))
+    assert(0);
+
+  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 server_fd, int event, void *arg)
+{
+  SCK_Message message;
+  IPSockAddr addr;
+  int log_index, sock_fd;
+  struct timespec now;
+
+  sock_fd = SCK_AcceptConnection(server_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_LogNTPAccess(&addr.ip_addr, &now);
+  if (log_index >= 0 && CLG_LimitNTPResponseRate(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;
+
+    /* Include the current server key and client address in the request */
+    memset(&req, 0, sizeof (req));
+    req.key_id = htonl(server_keys[current_server_key].id);
+    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;
+
+    if (!SCK_SendMessage(helper_sock_fd, &message, SCK_FLAG_MSG_DESCRIPTOR)) {
+      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, int port)
+{
+  IPSockAddr local_addr;
+  int sock_fd;
+
+  if (!SCK_IsFamilySupported(family))
+    return INVALID_SOCK_FD;
+
+  CNF_GetBindAddress(family, &local_addr.ip_addr);
+
+  if (local_addr.ip_addr.family != family)
+    SCK_GetAnyLocalIPAddress(family, &local_addr.ip_addr);
+
+  local_addr.port = port;
+
+  sock_fd = SCK_OpenTcpSocket(NULL, &local_addr, 0);
+  if (sock_fd < 0) {
+    LOG(LOGS_ERR, "Could not open NTS-KE socket on %s", UTI_IPSockAddrToString(&local_addr));
+    return INVALID_SOCK_FD;
+  }
+
+  if (!SCK_ListenOnSocket(sock_fd, CNF_GetNtsServerConnections())) {
+    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_Cookie cookie;
+  NKE_Key c2s, s2c;
+  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 {
+    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;
+    }
+
+    /* This should be configurable */
+    if (0) {
+      const char server[] = "::1";
+      if (!NKSN_AddRecord(session, 1, NKE_RECORD_NTPV4_SERVER_NEGOTIATION, server,
+                          sizeof (server) - 1))
+        return 0;
+    }
+
+    if (!NKSN_GetKeys(session, aead_algorithm, &c2s, &s2c))
+      return 0;
+
+    for (i = 0; i < NKE_MAX_COOKIES; i++) {
+      if (!NKS_GenerateCookie(&c2s, &s2c, &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 = -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 == -1) {
+    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;
+        }
+        for (i = 0; i < MIN(length, sizeof (data)) / 2; i++) {
+          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;
+        }
+        for (i = 0; i < MIN(length, sizeof (data)) / 2; i++) {
+          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 (aead_algorithm < 0 || next_protocol < 0)
+    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;
+
+  assert(index < MAX_SERVER_KEYS);
+
+  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 (!SIV_SetKey(server_keys[index].siv, server_keys[index].key, key_length))
+    assert(0);
+
+  UTI_GetRandomBytes(&server_keys[index].id, sizeof (server_keys[index].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);
+}
+
+/* ================================================== */
+
+static void
+save_keys(void)
+{
+  char hex_key[SIV_MAX_KEY_LENGTH * 2 + 1];
+  int i, index, key_length;
+  char *cachedir;
+  FILE *f;
+
+  cachedir = CNF_GetNtsCacheDir();
+  if (!cachedir)
+    return;
+
+  f = UTI_OpenFile(cachedir, "ntskeys", ".tmp", 'w', 0600);
+  if (!f)
+    return;
+
+  key_length = SIV_GetKeyLength(SERVER_COOKIE_SIV);
+
+  for (i = 0; i < MAX_SERVER_KEYS; i++) {
+    index = (current_server_key + i + 1) % MAX_SERVER_KEYS;
+
+    if (key_length > sizeof (server_keys[index].key) ||
+        !UTI_BytesToHex(server_keys[index].key, key_length, hex_key, sizeof (hex_key))) {
+      assert(0);
+      break;
+    }
+
+    fprintf(f, "%08"PRIX32" %s\n", server_keys[index].id, hex_key);
+  }
+
+  fclose(f);
+
+  if (!UTI_RenameTempFile(cachedir, "ntskeys", ".tmp", NULL))
+    ;
+}
+
+/* ================================================== */
+
+static void
+load_keys(void)
+{
+  int i, index, line_length, key_length, n;
+  char *cachedir, line[1024];
+  FILE *f;
+  uint32_t id;
+
+  cachedir = CNF_GetNtsCacheDir();
+  if (!cachedir)
+    return;
+
+  f = UTI_OpenFile(cachedir, "ntskeys", NULL, 'r', 0);
+  if (!f)
+    return;
+
+  key_length = SIV_GetKeyLength(SERVER_COOKIE_SIV);
+
+  for (i = 0; i < MAX_SERVER_KEYS; i++) {
+    if (!fgets(line, sizeof (line), f))
+      break;
+
+    line_length = strlen(line);
+    if (line_length < 10)
+      break;
+    /* Drop '\n' */
+    line[line_length - 1] = '\0';
+
+    if (sscanf(line, "%"PRIX32"%n", &id, &n) != 1 || line[n] != ' ')
+      break;
+
+    index = id % MAX_SERVER_KEYS;
+
+    if (UTI_HexToBytes(line + n + 1, server_keys[index].key,
+                       sizeof (server_keys[index].key)) != key_length)
+      break;
+
+    server_keys[index].id = id;
+    if (!SIV_SetKey(server_keys[index].siv, server_keys[index].key, key_length))
+      assert(0);
+
+    DEBUG_LOG("Loaded key %"PRIX32, id);
+
+    current_server_key = index;
+  }
+
+  fclose(f);
+}
+
+/* ================================================== */
+
+static void
+key_timeout(void *arg)
+{
+  current_server_key = (current_server_key + 1) % MAX_SERVER_KEYS;
+  generate_key(current_server_key);
+  save_keys();
+
+  SCH_AddTimeoutByDelay(MAX(CNF_GetNtsRotate(), MIN_KEY_ROTATE_INTERVAL),
+                        key_timeout, NULL);
+}
+
+/* ================================================== */
+
+static void
+start_helper(int id, int scfilter_level, int main_fd, int helper_fd)
+{
+  pid_t pid;
+
+  pid = fork();
+
+  if (pid < 0)
+    LOG_FATAL("fork() failed : %s", strerror(errno));
+
+  if (pid > 0)
+    return;
+
+  SCK_CloseSocket(main_fd);
+
+  LOG_CloseParentFd();
+  SCH_Reset();
+  SCH_AddFileHandler(helper_fd, SCH_FILE_INPUT, handle_helper_request, NULL);
+  UTI_SetQuitSignalsHandler(helper_signal, 1);
+  if (scfilter_level != 0)
+    SYS_EnableSystemCallFilter(scfilter_level, SYS_NTSKE_HELPER);
+
+  initialised = 1;
+
+  DEBUG_LOG("NTS-KE helper #%d started", id);
+
+  SCH_MainLoop();
+
+  NKS_Finalise();
+
+  DEBUG_LOG("NTS-KE helper #%d exiting", id);
+
+  exit(0);
+}
+
+/* ================================================== */
+
+void
+NKS_Initialise(int scfilter_level)
+{
+  char *cert, *key;
+  int i, processes;
+
+  server_sock_fd4 = INVALID_SOCK_FD;
+  server_sock_fd6 = INVALID_SOCK_FD;
+  helper_sock_fd = INVALID_SOCK_FD;
+
+  cert = CNF_GetNtsServerCertFile();
+  key = CNF_GetNtsServerKeyFile();
+
+  if (!cert || !key)
+    return;
+
+  server_credentials = NKSN_CreateCertCredentials(cert, key, NULL);
+  if (!server_credentials)
+    return;
+
+  sessions = ARR_CreateInstance(sizeof (NKSN_Instance));
+  for (i = 0; i < CNF_GetNtsServerConnections(); i++)
+    *(NKSN_Instance *)ARR_GetNewElement(sessions) = NULL;
+  for (i = 0; i < MAX_SERVER_KEYS; i++)
+    server_keys[i].siv = NULL;
+
+  server_sock_fd4 = open_socket(IPADDR_INET4, CNF_GetNtsServerPort());
+  server_sock_fd6 = open_socket(IPADDR_INET6, CNF_GetNtsServerPort());
+
+  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;
+
+  load_keys();
+
+  key_timeout(NULL);
+
+  processes = CNF_GetNtsServerProcesses();
+
+  if (processes > 0) {
+    int sock_fd1, sock_fd2;
+
+    sock_fd1 = SCK_OpenUnixSocketPair(0, &sock_fd2);
+
+    for (i = 0; i < processes; i++)
+      start_helper(i + 1, scfilter_level, sock_fd1, sock_fd2);
+
+    SCK_CloseSocket(sock_fd2);
+    helper_sock_fd = sock_fd1;
+  }
+
+  initialised = 1;
+}
+
+/* ================================================== */
+
+void
+NKS_Finalise(void)
+{
+  int i;
+
+  if (!initialised)
+    return;
+
+  if (helper_sock_fd != INVALID_SOCK_FD) {
+    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);
+
+  save_keys();
+  for (i = 0; i < MAX_SERVER_KEYS; i++) {
+    if (server_keys[i].siv != NULL)
+      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);
+
+  NKSN_DestroyCertCredentials(server_credentials);
+}
+
+/* ================================================== */
+
+/* A server cookie consists of key ID, nonce, and encrypted C2S+S2C keys */
+
+int
+NKS_GenerateCookie(NKE_Key *c2s, NKE_Key *s2c, 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;
+  }
+
+  if (c2s->length < 0 || c2s->length > NKE_MAX_KEY_LENGTH ||
+      s2c->length < 0 || s2c->length > NKE_MAX_KEY_LENGTH) {
+    DEBUG_LOG("Invalid key length");
+    return 0;
+  }
+
+  key = &server_keys[current_server_key];
+
+  header = (ServerCookieHeader *)cookie->cookie;
+
+  /* Keep the fields in the host byte order */
+  header->key_id = key->id;
+  UTI_GetRandomBytes(header->nonce, sizeof (header->nonce));
+
+  plaintext_length = c2s->length + s2c->length;
+  assert(plaintext_length <= sizeof (plaintext));
+  memcpy(plaintext, c2s->key, c2s->length);
+  memcpy(plaintext + c2s->length, s2c->key, 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_Key *c2s, NKE_Key *s2c)
+{
+  unsigned char plaintext[2 * NKE_MAX_KEY_LENGTH], *ciphertext;
+  int ciphertext_length, plaintext_length, tag_length;
+  ServerCookieHeader *header;
+  ServerKey *key;
+
+  if (!initialised) {
+    DEBUG_LOG("NTS server disabled");
+    return 0;
+  }
+
+  if (cookie->length <= 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 = &server_keys[header->key_id % MAX_SERVER_KEYS];
+  if (header->key_id != key->id) {
+    DEBUG_LOG("Unknown key %"PRIX32, header->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;
+  }
+
+  c2s->length = plaintext_length / 2;
+  s2c->length = plaintext_length / 2;
+  assert(c2s->length <= sizeof (c2s->key));
+
+  memcpy(c2s->key, plaintext, c2s->length);
+  memcpy(s2c->key, plaintext + c2s->length, s2c->length);
+
+  return 1;
+}
diff --git a/nts_ke_server.h b/nts_ke_server.h
new file mode 100644 (file)
index 0000000..558dd11
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+  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.
+ * 
+ **********************************************************************
+
+  =======================================================================
+
+  Header file for the NTS-KE server
+  */
+
+#ifndef GOT_NTS_KE_SERVER_H
+#define GOT_NTS_KE_SERVER_H
+
+#include "nts_ke.h"
+
+/* Init and fini functions */
+extern void NKS_Initialise(int scfilter_level);
+extern void NKS_Finalise(void);
+
+/* Generate a new NTS cookie containing the C2S and S2C keys */
+extern int NKS_GenerateCookie(NKE_Key *c2s, NKE_Key *s2c, NKE_Cookie *cookie);
+
+/* Validate a cookie and extract the C2S and S2C keys */
+extern int NKS_DecodeCookie(NKE_Cookie *cookie, NKE_Key *c2s, NKE_Key *s2c);
+
+#endif
diff --git a/nts_ke_session.c b/nts_ke_session.c
new file mode 100644 (file)
index 0000000..d9b0d4b
--- /dev/null
@@ -0,0 +1,779 @@
+/*
+  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 session used by server and client
+  */
+
+#include "config.h"
+
+#include "sysincl.h"
+
+#include "nts_ke_session.h"
+
+#include "conf.h"
+#include "logging.h"
+#include "memory.h"
+#include "siv.h"
+#include "socket.h"
+#include "sched.h"
+#include "util.h"
+
+#include <gnutls/gnutls.h>
+
+#define INVALID_SOCK_FD (-8)
+
+struct RecordHeader {
+  uint16_t type;
+  uint16_t body_length;
+};
+
+struct Message {
+  int length;
+  int sent;
+  int parsed;
+  int complete;
+  unsigned char data[NKE_MAX_MESSAGE_LENGTH];
+};
+
+typedef enum {
+  KE_WAIT_CONNECT,
+  KE_HANDSHAKE,
+  KE_SEND,
+  KE_RECEIVE,
+  KE_SHUTDOWN,
+  KE_STOPPED,
+} KeState;
+
+struct NKSN_Instance_Record {
+  int server;
+  char *name;
+  NKSN_MessageHandler handler;
+  void *handler_arg;
+
+  KeState state;
+  int sock_fd;
+  gnutls_session_t tls_session;
+  SCH_TimeoutID timeout_id;
+
+  struct Message message;
+  int new_message;
+  int ended_message;
+};
+
+/* ================================================== */
+
+static gnutls_priority_t priority_cache;
+
+static int credentials_counter = 0;
+
+/* ================================================== */
+
+static void
+reset_message(struct Message *message)
+{
+  message->length = 0;
+  message->sent = 0;
+  message->parsed = 0;
+  message->complete = 0;
+}
+
+/* ================================================== */
+
+static int
+add_record(struct Message *message, int critical, int type, const void *body, int body_length)
+{
+  struct RecordHeader header;
+
+  if (body_length < 0 || body_length > 0xffff || type < 0 || type > 0x7fff ||
+      message->length + sizeof (header) + body_length > sizeof (message->data))
+    return 0;
+
+  header.type = htons(!!critical * NKE_RECORD_CRITICAL_BIT | type);
+  header.body_length = htons(body_length);
+
+  memcpy(&message->data[message->length], &header, sizeof (header));
+  message->length += sizeof (header);
+
+  if (body_length > 0) {
+    memcpy(&message->data[message->length], body, body_length);
+    message->length += body_length;
+  }
+
+  return 1;
+}
+
+/* ================================================== */
+
+static void
+reset_message_parsing(struct Message *message)
+{
+  message->parsed = 0;
+}
+
+/* ================================================== */
+
+static int
+get_record(struct Message *message, int *critical, int *type, int *body_length,
+           void *body, int buffer_length)
+{
+  struct RecordHeader header;
+  int blen, rlen;
+
+  if (message->length < message->parsed + sizeof (header) ||
+      buffer_length < 0)
+    return 0;
+
+  memcpy(&header, &message->data[message->parsed], sizeof (header));
+
+  blen = ntohs(header.body_length);
+  rlen = sizeof (header) + blen;
+
+  if (message->length < message->parsed + rlen)
+    return 0;
+
+  if (critical)
+    *critical = !!(ntohs(header.type) & NKE_RECORD_CRITICAL_BIT);
+  if (type)
+    *type = ntohs(header.type) & ~NKE_RECORD_CRITICAL_BIT;
+  if (body)
+    memcpy(body, &message->data[message->parsed + sizeof (header)], MIN(buffer_length, blen));
+  if (body_length)
+    *body_length = blen;
+
+  message->parsed += rlen;
+
+  return 1;
+}
+
+/* ================================================== */
+
+static int
+check_message_format(struct Message *message, int eof)
+{
+  int critical = 0, type = -1, length = -1, ends = 0;
+
+  reset_message_parsing(message);
+  message->complete = 0;
+
+  while (get_record(message, &critical, &type, &length, NULL, 0)) {
+    if (type == NKE_RECORD_END_OF_MESSAGE) {
+      if (!critical || length != 0 || ends > 0)
+        return 0;
+      ends++;
+    }
+  }
+
+  /* If the message cannot be fully parsed, but more data may be coming,
+     consider the format to be ok */
+  if (message->length == 0 || message->parsed < message->length)
+    return !eof;
+
+  if (type != NKE_RECORD_END_OF_MESSAGE)
+    return !eof;
+
+  message->complete = 1;
+
+  return 1;
+}
+
+/* ================================================== */
+
+static gnutls_session_t
+create_tls_session(int server_mode, int sock_fd, const char *server_name,
+                   gnutls_certificate_credentials_t credentials,
+                   gnutls_priority_t priority)
+{
+  unsigned char alpn_name[sizeof (NKE_ALPN_NAME)];
+  gnutls_session_t session;
+  gnutls_datum_t alpn;
+  int r;
+
+  r = gnutls_init(&session, GNUTLS_NONBLOCK | (server_mode ? GNUTLS_SERVER : GNUTLS_CLIENT));
+  if (r < 0) {
+    LOG(LOGS_ERR, "Could not %s TLS session : %s", "create", gnutls_strerror(r));
+    return NULL;
+  }
+
+  if (!server_mode) {
+    r = gnutls_server_name_set(session, GNUTLS_NAME_DNS, server_name, strlen(server_name));
+    if (r < 0)
+      goto error;
+    gnutls_session_set_verify_cert(session, server_name, 0);
+  }
+
+  r = gnutls_priority_set(session, priority);
+  if (r < 0)
+    goto error;
+
+  r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, credentials);
+  if (r < 0)
+    goto error;
+
+  memcpy(alpn_name, NKE_ALPN_NAME, sizeof (alpn_name));
+  alpn.data = alpn_name;
+  alpn.size = sizeof (alpn_name) - 1;
+
+  r = gnutls_alpn_set_protocols(session, &alpn, 1, 0);
+  if (r < 0)
+    goto error;
+
+  gnutls_transport_set_int(session, sock_fd);
+
+  return session;
+
+error:
+  LOG(LOGS_ERR, "Could not %s TLS session : %s", "set", gnutls_strerror(r));
+  gnutls_deinit(session);
+  return NULL;
+}
+
+/* ================================================== */
+
+static void
+stop_session(NKSN_Instance inst)
+{
+  if (inst->state == KE_STOPPED)
+    return;
+
+  inst->state = KE_STOPPED;
+
+  SCH_RemoveFileHandler(inst->sock_fd);
+  SCK_CloseSocket(inst->sock_fd);
+  inst->sock_fd = INVALID_SOCK_FD;
+
+  gnutls_deinit(inst->tls_session);
+  inst->tls_session = NULL;
+
+  SCH_RemoveTimeout(inst->timeout_id);
+  inst->timeout_id = 0;
+}
+
+/* ================================================== */
+
+static void
+session_timeout(void *arg)
+{
+  NKSN_Instance inst = arg;
+
+  LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE session with %s timed out", inst->name);
+
+  inst->timeout_id = 0;
+  stop_session(inst);
+}
+
+/* ================================================== */
+
+static int
+get_socket_error(int sock_fd)
+{
+  int optval;
+  socklen_t optlen = sizeof (optval);
+
+  if (getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0) {
+    DEBUG_LOG("getsockopt() failed : %s", strerror(errno));
+    return EINVAL;
+  }
+
+  return optval;
+}
+
+/* ================================================== */
+
+static int
+check_alpn(NKSN_Instance inst)
+{
+  gnutls_datum_t alpn;
+  int r;
+
+  r = gnutls_alpn_get_selected_protocol(inst->tls_session, &alpn);
+  if (r < 0 || alpn.size != sizeof (NKE_ALPN_NAME) - 1 ||
+      strncmp((const char *)alpn.data, NKE_ALPN_NAME, sizeof (NKE_ALPN_NAME) - 1))
+    return 0;
+
+  return 1;
+}
+
+/* ================================================== */
+
+static void
+change_state(NKSN_Instance inst, KeState state)
+{
+  int output;
+
+  switch (state) {
+    case KE_HANDSHAKE:
+      output = !inst->server;
+      break;
+    case KE_WAIT_CONNECT:
+    case KE_SEND:
+    case KE_SHUTDOWN:
+      output = 1;
+      break;
+    case KE_RECEIVE:
+      output = 0;
+      break;
+    default:
+      assert(0);
+  }
+
+  SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_OUTPUT, output);
+
+  inst->state = state;
+}
+
+/* ================================================== */
+
+static int
+handle_event(NKSN_Instance inst, int event)
+{
+  struct Message *message = &inst->message;
+  int r;
+
+  DEBUG_LOG("Session event %d fd=%d state=%d", event, inst->sock_fd, (int)inst->state);
+
+  switch (inst->state) {
+    case KE_WAIT_CONNECT:
+      /* Check if connect() succeeded */
+      if (event != SCH_FILE_OUTPUT)
+        return 0;
+
+      r = get_socket_error(inst->sock_fd);
+
+      if (r) {
+        LOG(LOGS_ERR, "Could not connect to %s : %s", inst->name, strerror(r));
+        stop_session(inst);
+        return 0;
+      }
+
+      DEBUG_LOG("Connected to %s", inst->name);
+
+      change_state(inst, KE_HANDSHAKE);
+      return 0;
+
+    case KE_HANDSHAKE:
+      r = gnutls_handshake(inst->tls_session);
+
+      if (r < 0) {
+        if (gnutls_error_is_fatal(r)) {
+          LOG(inst->server ? LOGS_DEBUG : LOGS_ERR,
+              "TLS handshake with %s failed : %s", inst->name, gnutls_strerror(r));
+          stop_session(inst);
+          return 0;
+        }
+
+        /* Disable output when the handshake is trying to receive data */
+        SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_OUTPUT,
+                                gnutls_record_get_direction(inst->tls_session));
+        return 0;
+      }
+
+      if (DEBUG) {
+        char *description = gnutls_session_get_desc(inst->tls_session);
+        DEBUG_LOG("Handshake with %s completed %s",
+                  inst->name, description ? description : "");
+        gnutls_free(description);
+      }
+
+      if (!check_alpn(inst)) {
+        LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE not supported by %s", inst->name);
+        stop_session(inst);
+        return 0;
+      }
+
+      /* Client will send a request to the server */
+      change_state(inst, inst->server ? KE_RECEIVE : KE_SEND);
+      return 0;
+
+    case KE_SEND:
+      assert(inst->new_message && message->complete);
+
+      r = gnutls_record_send(inst->tls_session, &message->data[message->sent],
+                             message->length - message->sent);
+
+      if (r < 0) {
+        if (gnutls_error_is_fatal(r)) {
+          LOG(inst->server ? LOGS_DEBUG : LOGS_ERR,
+              "Could not send NTS-KE message to %s : %s", inst->name, gnutls_strerror(r));
+          stop_session(inst);
+        }
+        return 0;
+      }
+
+      DEBUG_LOG("Sent %d bytes to %s", r, inst->name);
+
+      message->sent += r;
+      if (message->sent < message->length)
+        return 0;
+
+      /* Client will receive a response */
+      change_state(inst, inst->server ? KE_SHUTDOWN : KE_RECEIVE);
+      reset_message(&inst->message);
+      inst->new_message = 0;
+      return 0;
+
+    case KE_RECEIVE:
+      do {
+        if (message->length >= sizeof (message->data)) {
+          DEBUG_LOG("Message is too long");
+          stop_session(inst);
+          return 0;
+        }
+
+        r = gnutls_record_recv(inst->tls_session, &message->data[message->length],
+                               sizeof (message->data) - message->length);
+
+        if (r < 0) {
+          /* Handle a renegotiation request on both client and server as
+             a protocol error */
+          if (gnutls_error_is_fatal(r) || r == GNUTLS_E_REHANDSHAKE) {
+            LOG(inst->server ? LOGS_DEBUG : LOGS_ERR,
+                "Could not receive NTS-KE message from %s : %s",
+                inst->name, gnutls_strerror(r));
+            stop_session(inst);
+          }
+          return 0;
+        }
+
+        DEBUG_LOG("Received %d bytes from %s", r, inst->name);
+
+        message->length += r;
+
+      } while (gnutls_record_check_pending(inst->tls_session) > 0);
+
+      if (!check_message_format(message, r == 0)) {
+        LOG(inst->server ? LOGS_DEBUG : LOGS_ERR,
+            "Received invalid NTS-KE message from %s", inst->name);
+        stop_session(inst);
+        return 0;
+      }
+
+      /* Wait for more data if the message is not complete yet */
+      if (!message->complete)
+        return 0;
+
+      /* Server will send a response to the client */
+      change_state(inst, inst->server ? KE_SEND : KE_SHUTDOWN);
+      break;
+
+    case KE_SHUTDOWN:
+      r = gnutls_bye(inst->tls_session, GNUTLS_SHUT_RDWR);
+
+      if (r < 0) {
+        if (gnutls_error_is_fatal(r)) {
+          DEBUG_LOG("Shutdown with %s failed : %s", inst->name, gnutls_strerror(r));
+          stop_session(inst);
+          return 0;
+        }
+
+        /* Disable output when the TLS shutdown is trying to receive data */
+        SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_OUTPUT,
+                                gnutls_record_get_direction(inst->tls_session));
+        return 0;
+      }
+
+      SCK_ShutdownConnection(inst->sock_fd);
+      stop_session(inst);
+
+      DEBUG_LOG("Shutdown completed");
+      return 0;
+
+    default:
+      assert(0);
+  }
+
+  return 1;
+}
+
+/* ================================================== */
+
+static void
+read_write_socket(int fd, int event, void *arg)
+{
+  NKSN_Instance inst = arg;
+
+  if (!handle_event(inst, event))
+    return;
+
+  reset_message_parsing(&inst->message);
+
+  if (!(inst->handler)(inst->handler_arg)) {
+    stop_session(inst);
+    return;
+  }
+}
+
+/* ================================================== */
+
+static int gnutls_initialised = 0;
+
+static void
+init_gnutls(void)
+{
+  int r;
+
+  if (gnutls_initialised)
+    return;
+
+  r = gnutls_global_init();
+  if (r < 0)
+    LOG_FATAL("Could not initialise %s : %s", "gnutls", gnutls_strerror(r));
+
+  /* NTS specification requires TLS1.2 or later */
+  r = gnutls_priority_init2(&priority_cache, "-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1",
+                            NULL, GNUTLS_PRIORITY_INIT_DEF_APPEND);
+  if (r < 0)
+    LOG_FATAL("Could not initialise %s : %s", "priority cache", gnutls_strerror(r));
+
+  gnutls_initialised = 1;
+}
+
+/* ================================================== */
+
+static void
+deinit_gnutls(void)
+{
+  assert(gnutls_initialised);
+
+  gnutls_priority_deinit(priority_cache);
+  gnutls_global_deinit();
+  gnutls_initialised = 0;
+}
+
+/* ================================================== */
+
+void *
+NKSN_CreateCertCredentials(char *cert, char *key, char *trusted_certs)
+{
+  gnutls_certificate_credentials_t credentials = NULL;
+  int r;
+
+  init_gnutls();
+
+  r = gnutls_certificate_allocate_credentials(&credentials);
+  if (r < 0)
+    goto error;
+
+  if (cert && key) {
+    r = gnutls_certificate_set_x509_key_file(credentials, cert, key,
+                                             GNUTLS_X509_FMT_PEM);
+    if (r < 0)
+      goto error;
+  } else {
+    if (!CNF_GetNoSystemCert()) {
+      r = gnutls_certificate_set_x509_system_trust(credentials);
+      if (r < 0)
+        goto error;
+    }
+
+    if (trusted_certs) {
+      r = gnutls_certificate_set_x509_trust_file(credentials, trusted_certs,
+                                                 GNUTLS_X509_FMT_PEM);
+      if (r < 0)
+        goto error;
+    }
+  }
+
+  credentials_counter++;
+
+  return credentials;
+
+error:
+  LOG(LOGS_ERR, "Could not set credentials : %s", gnutls_strerror(r));
+  if (credentials)
+    gnutls_certificate_free_credentials(credentials);
+  return NULL;
+}
+
+/* ================================================== */
+
+void
+NKSN_DestroyCertCredentials(void *credentials)
+{
+  gnutls_certificate_free_credentials(credentials);
+  credentials_counter--;
+  if (credentials_counter != 0)
+    return;
+
+  deinit_gnutls();
+}
+
+/* ================================================== */
+
+NKSN_Instance
+NKSN_CreateInstance(int server_mode, const char *name,
+                    NKSN_MessageHandler handler, void *handler_arg)
+{
+  NKSN_Instance inst;
+
+  inst = MallocNew(struct NKSN_Instance_Record);
+
+  inst->server = server_mode;
+  inst->name = Strdup(name);
+  inst->handler = handler;
+  inst->handler_arg = handler_arg;
+  /* Replace NULL arg with the session itself */
+  if (!inst->handler_arg)
+    inst->handler_arg = inst;
+
+  inst->state = KE_STOPPED;
+  inst->sock_fd = INVALID_SOCK_FD;
+  inst->tls_session = NULL;
+  inst->timeout_id = 0;
+
+  return inst;
+}
+
+/* ================================================== */
+
+void
+NKSN_DestroyInstance(NKSN_Instance inst)
+{
+  stop_session(inst);
+
+  Free(inst->name);
+  Free(inst);
+}
+
+/* ================================================== */
+
+int
+NKSN_StartSession(NKSN_Instance inst, int sock_fd, void *credentials, double timeout)
+{
+  assert(inst->state == KE_STOPPED);
+
+  inst->tls_session = create_tls_session(inst->server, sock_fd,
+                                         inst->server ? NULL : inst->name,
+                                         credentials, priority_cache);
+  if (!inst->tls_session)
+    return 0;
+
+  inst->sock_fd = sock_fd;
+  SCH_AddFileHandler(sock_fd, SCH_FILE_INPUT, read_write_socket, inst);
+
+  inst->timeout_id = SCH_AddTimeoutByDelay(timeout, session_timeout, inst);
+
+  reset_message(&inst->message);
+  inst->new_message = 0;
+  inst->ended_message = 0;
+
+  change_state(inst, inst->server ? KE_HANDSHAKE : KE_WAIT_CONNECT);
+
+  return 1;
+}
+
+/* ================================================== */
+
+void
+NKSN_BeginMessage(NKSN_Instance inst)
+{
+  reset_message(&inst->message);
+  inst->new_message = 1;
+}
+
+/* ================================================== */
+
+int
+NKSN_AddRecord(NKSN_Instance inst, int critical, int type, const void *body, int body_length)
+{
+  assert(inst->new_message && !inst->message.complete);
+  assert(type != NKE_RECORD_END_OF_MESSAGE);
+
+  return add_record(&inst->message, critical, type, body, body_length);
+}
+
+/* ================================================== */
+
+int
+NKSN_EndMessage(NKSN_Instance inst)
+{
+  assert(!inst->message.complete);
+
+  if (!add_record(&inst->message, 1, NKE_RECORD_END_OF_MESSAGE, NULL, 0))
+    return 0;
+
+  inst->message.complete = 1;
+
+  return 1;
+}
+
+/* ================================================== */
+
+int
+NKSN_GetRecord(NKSN_Instance inst, int *critical, int *type, int *body_length,
+               void *body, int buffer_length)
+{
+  int type2;
+
+  assert(inst->message.complete);
+
+  if (!get_record(&inst->message, critical, &type2, body_length, body, buffer_length))
+    return 0;
+
+  if (type2 == NKE_RECORD_END_OF_MESSAGE)
+    return 0;
+
+  if (type)
+    *type = type2;
+
+  return 1;
+}
+
+/* ================================================== */
+
+int
+NKSN_GetKeys(NKSN_Instance inst, SIV_Algorithm siv, NKE_Key *c2s, NKE_Key *s2c)
+{
+  c2s->length = SIV_GetKeyLength(siv);
+  s2c->length = SIV_GetKeyLength(siv);
+  assert(c2s->length <= sizeof (c2s->key));
+  assert(s2c->length <= sizeof (s2c->key));
+
+  if (gnutls_prf_rfc5705(inst->tls_session,
+                         sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL,
+                         sizeof (NKE_EXPORTER_CONTEXT_C2S) - 1, NKE_EXPORTER_CONTEXT_C2S,
+                         c2s->length, (char *)c2s->key) < 0)
+    return 0;
+  if (gnutls_prf_rfc5705(inst->tls_session,
+                         sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL,
+                         sizeof (NKE_EXPORTER_CONTEXT_S2C) - 1, NKE_EXPORTER_CONTEXT_S2C,
+                         s2c->length, (char *)s2c->key) < 0)
+    return 0;
+
+  return 1;
+}
+
+/* ================================================== */
+
+int
+NKSN_IsStopped(NKSN_Instance inst)
+{
+  return inst->state == KE_STOPPED;
+}
+
+/* ================================================== */
+
+void
+NKSN_StopSession(NKSN_Instance inst)
+{
+  stop_session(inst);
+}
diff --git a/nts_ke_session.h b/nts_ke_session.h
new file mode 100644 (file)
index 0000000..cbfb7f5
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+  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.
+ * 
+ **********************************************************************
+
+  =======================================================================
+
+  Header file for the NTS-KE session
+  */
+
+#ifndef GOT_NTS_KE_SESSION_H
+#define GOT_NTS_KE_SESSION_H
+
+#include "nts_ke.h"
+#include "siv.h"
+
+typedef struct NKSN_Instance_Record *NKSN_Instance;
+
+/* Handler for received NTS-KE messages.  A non-zero return code stops
+   the session. */
+typedef int (*NKSN_MessageHandler)(void *arg);
+
+/* Get client or server credentials using certificates of trusted CAs,
+   or a server certificate and key.  The credentials may be shared between
+   different clients or servers. */
+extern void *NKSN_CreateCertCredentials(char *cert, char *key, char *trusted_certs);
+
+/* Destroy the credentials */
+extern void NKSN_DestroyCertCredentials(void *credentials);
+
+/* Create an instance */
+extern NKSN_Instance NKSN_CreateInstance(int server_mode, const char *name,
+                                         NKSN_MessageHandler handler, void *handler_arg);
+
+/* Destroy an instance */
+extern void NKSN_DestroyInstance(NKSN_Instance inst);
+
+/* Start a new NTS-KE session */
+extern int NKSN_StartSession(NKSN_Instance inst, int sock_fd, void *credentials,
+                             double timeout);
+
+/* Begin an NTS-KE message.  A request should be made right after starting
+   the session and response should be made in the message handler. */
+extern void NKSN_BeginMessage(NKSN_Instance inst);
+
+/* Add a record to the message */
+extern int NKSN_AddRecord(NKSN_Instance inst, int critical, int type,
+                          const void *body, int body_length);
+
+/* Terminate the message */
+extern int NKSN_EndMessage(NKSN_Instance inst);
+
+/* Get the next record from the received message.  This function should be
+   called from the message handler. */
+extern int NKSN_GetRecord(NKSN_Instance inst, int *critical, int *type, int *body_length,
+                          void *body, int buffer_length);
+
+/* Export NTS keys for a specified algorithm */
+extern int NKSN_GetKeys(NKSN_Instance inst, SIV_Algorithm siv, NKE_Key *c2s, NKE_Key *s2c);
+
+/* Check if the session has stopped */
+extern int NKSN_IsStopped(NKSN_Instance inst);
+
+/* Stop the session */
+extern void NKSN_StopSession(NKSN_Instance inst);
+
+#endif
diff --git a/stubs.c b/stubs.c
index 41b8dcc97b7000d690a6142e9698a0910447ddca..d9dce17682e81be032cd92359ccbf8a8d1f06e54 100644 (file)
--- a/stubs.c
+++ b/stubs.c
@@ -40,6 +40,8 @@
 #include "ntp_io.h"
 #include "ntp_sources.h"
 #include "ntp_signd.h"
+#include "nts_ke_client.h"
+#include "nts_ke_server.h"
 #include "privops.h"
 #include "refclock.h"
 #include "sched.h"
@@ -447,3 +449,27 @@ CMC_DestroyInstance(CMC_Instance inst)
 }
 
 #endif /* !HAVE_CMAC */
+
+#ifndef FEAT_NTS
+
+void
+NKC_Initialise(void)
+{
+}
+
+void
+NKC_Finalise(void)
+{
+}
+
+void
+NKS_Initialise(int scfilter_level)
+{
+}
+
+void
+NKS_Finalise(void)
+{
+}
+
+#endif /* !FEAT_NTS */