From 6043632f80679029a158a8eecfe1b3eb8aa9116f Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Tue, 4 Feb 2020 15:15:03 +0100 Subject: [PATCH] nts: add NTS-NTP server and client Add support for the NTS NTP extension fields. --- conf.c | 11 ++ conf.h | 1 + configure | 1 + main.c | 3 + nts_ntp.h | 41 +++++ nts_ntp_auth.c | 174 ++++++++++++++++++ nts_ntp_auth.h | 43 +++++ nts_ntp_client.c | 445 +++++++++++++++++++++++++++++++++++++++++++++++ nts_ntp_client.h | 46 +++++ nts_ntp_server.c | 253 +++++++++++++++++++++++++++ nts_ntp_server.h | 40 +++++ stubs.c | 63 +++++++ 12 files changed, 1121 insertions(+) create mode 100644 nts_ntp.h create mode 100644 nts_ntp_auth.c create mode 100644 nts_ntp_auth.h create mode 100644 nts_ntp_client.c create mode 100644 nts_ntp_client.h create mode 100644 nts_ntp_server.c create mode 100644 nts_ntp_server.h diff --git a/conf.c b/conf.c index 6d79c04..8bc59a4 100644 --- a/conf.c +++ b/conf.c @@ -230,6 +230,7 @@ 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_refresh = 2419200; /* 4 weeks */ static int nts_rotate = 604800; /* 1 week */ static char *nts_trusted_cert_file = NULL; @@ -556,6 +557,8 @@ CNF_ParseLine(const char *filename, int number, char *line) parse_int(p, &nts_server_port); } else if (!strcasecmp(command, "ntsprocesses")) { parse_int(p, &nts_server_processes); + } else if (!strcasecmp(command, "ntsrefresh")) { + parse_int(p, &nts_refresh); } else if (!strcasecmp(command, "ntsrotate")) { parse_int(p, &nts_rotate); } else if (!strcasecmp(command, "ntsservercert")) { @@ -2113,6 +2116,14 @@ CNF_GetNtsServerConnections(void) /* ================================================== */ +int +CNF_GetNtsRefresh(void) +{ + return nts_refresh; +} + +/* ================================================== */ + int CNF_GetNtsRotate(void) { diff --git a/conf.h b/conf.h index cc34791..697f111 100644 --- a/conf.h +++ b/conf.h @@ -145,6 +145,7 @@ extern char *CNF_GetNtsServerKeyFile(void); extern int CNF_GetNtsServerPort(void); extern int CNF_GetNtsServerProcesses(void); extern int CNF_GetNtsServerConnections(void); +extern int CNF_GetNtsRefresh(void); extern int CNF_GetNtsRotate(void); extern char *CNF_GetNtsTrustedCertFile(void); extern int CNF_GetNoSystemCert(void); diff --git a/configure b/configure index 5af221e..4a70118 100755 --- a/configure +++ b/configure @@ -939,6 +939,7 @@ if [ $feat_ntp = "1" ] && [ $feat_nts = "1" ] && [ $try_gnutls = "1" ] && \ '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 nts_ntp_auth.o nts_ntp_client.o nts_ntp_server.o" EXTRA_OBJECTS="$EXTRA_OBJECTS siv_nettle.o" LIBS="$LIBS $test_link" MYCPPFLAGS="$MYCPPFLAGS $test_cflags" diff --git a/main.c b/main.c index b08d7bd..c5181ee 100644 --- a/main.c +++ b/main.c @@ -40,6 +40,7 @@ #include "ntp_core.h" #include "nts_ke_client.h" #include "nts_ke_server.h" +#include "nts_ntp_server.h" #include "socket.h" #include "sources.h" #include "sourcestats.h" @@ -117,6 +118,7 @@ MAI_CleanupAndExit(void) CLG_Finalise(); NKC_Finalise(); NKS_Finalise(); + NNS_Finalise(); NSD_Finalise(); NSR_Finalise(); SST_Finalise(); @@ -587,6 +589,7 @@ int main SST_Initialise(); NSR_Initialise(); NSD_Initialise(); + NNS_Initialise(); NKS_Initialise(scfilter_level); NKC_Initialise(); CLG_Initialise(); diff --git a/nts_ntp.h b/nts_ntp.h new file mode 100644 index 0000000..a34eee1 --- /dev/null +++ b/nts_ntp.h @@ -0,0 +1,41 @@ +/* + 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-NTP protocol + */ + +#ifndef GOT_NTS_NTP_H +#define GOT_NTS_NTP_H + +#define NTP_EF_NTS_UNIQUE_IDENTIFIER 0x0104 +#define NTP_EF_NTS_COOKIE 0x0204 +#define NTP_EF_NTS_COOKIE_PLACEHOLDER 0x0304 +#define NTP_EF_NTS_AUTH_AND_EEF 0x0404 + +#define NTP_KOD_NTS_NAK 0x4e54534e + +#define NTS_MIN_UNIQ_ID_LENGTH 32 +#define NTS_MIN_UNPADDED_NONCE_LENGTH 16 +#define NTS_MAX_COOKIES 8 + +#endif diff --git a/nts_ntp_auth.c b/nts_ntp_auth.c new file mode 100644 index 0000000..8896d8e --- /dev/null +++ b/nts_ntp_auth.c @@ -0,0 +1,174 @@ +/* + 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 Authenticator and Encrypted Extension Fields extension field + */ + +#include "config.h" + +#include "sysincl.h" + +#include "nts_ntp_auth.h" + +#include "logging.h" +#include "ntp_ext.h" +#include "nts_ntp.h" +#include "siv.h" +#include "util.h" + +struct AuthHeader { + uint16_t nonce_length; + uint16_t ciphertext_length; +}; + +/* ================================================== */ + +static int +get_padding_length(int length) +{ + return length % 4U ? 4 - length % 4U : 0; +} + +/* ================================================== */ + +static int +get_padded_length(int length) +{ + return length + get_padding_length(length); +} + +/* ================================================== */ + +int +NNA_GenerateAuthEF(NTP_Packet *packet, NTP_PacketInfo *info, SIV_Instance siv, + const unsigned char *nonce, int nonce_length, + const unsigned char *plaintext, int plaintext_length, + int min_ef_length) +{ + int auth_length, ciphertext_length, assoc_length; + int nonce_padding, ciphertext_padding, additional_padding; + unsigned char *ciphertext, *body; + struct AuthHeader *header; + + assert(sizeof (*header) == 4); + + if (nonce_length <= 0 || plaintext_length < 0) { + DEBUG_LOG("Invalid nonce/plaintext length"); + return 0; + } + + assoc_length = info->length; + ciphertext_length = SIV_GetTagLength(siv) + plaintext_length; + nonce_padding = get_padding_length(nonce_length); + ciphertext_padding = get_padding_length(ciphertext_length); + min_ef_length = get_padded_length(min_ef_length); + + auth_length = sizeof (*header) + nonce_length + nonce_padding + + ciphertext_length + ciphertext_padding; + additional_padding = MAX(min_ef_length - auth_length - 4, 0); + additional_padding = MAX(NTS_MIN_UNPADDED_NONCE_LENGTH - nonce_length - nonce_padding, + additional_padding); + auth_length += additional_padding; + + if (!NEF_AddBlankField(packet, info, NTP_EF_NTS_AUTH_AND_EEF, auth_length, + (void **)&header)) { + DEBUG_LOG("Could not add EF"); + return 0; + } + + header->nonce_length = htons(nonce_length); + header->ciphertext_length = htons(ciphertext_length); + + body = (unsigned char *)(header + 1); + ciphertext = body + nonce_length + nonce_padding; + + memcpy(body, nonce, nonce_length); + memset(body + nonce_length, 0, nonce_padding); + + if (!SIV_Encrypt(siv, nonce, nonce_length, packet, assoc_length, + plaintext, plaintext_length, ciphertext, ciphertext_length)) { + DEBUG_LOG("SIV encrypt failed"); + return 0; + } + + memset(ciphertext + ciphertext_length, 0, ciphertext_padding + additional_padding); + + return 1; +} + +/* ================================================== */ + +int +NNA_DecryptAuthEF(NTP_Packet *packet, NTP_PacketInfo *info, SIV_Instance siv, int ef_start, + unsigned char *plaintext, int buffer_length, int *plaintext_length) +{ + unsigned int siv_tag_length, nonce_length, ciphertext_length; + unsigned char *nonce, *ciphertext; + int ef_type, ef_body_length; + void *ef_body; + struct AuthHeader *header; + + if (!NEF_ParseField(packet, info->length, ef_start, + NULL, &ef_type, &ef_body, &ef_body_length)) + return 0; + + if (ef_type != NTP_EF_NTS_AUTH_AND_EEF) + return 0; + + header = ef_body; + + nonce_length = ntohs(header->nonce_length); + ciphertext_length = ntohs(header->ciphertext_length); + + if (get_padded_length(nonce_length) + + get_padded_length(ciphertext_length) > ef_body_length) + return 0; + + nonce = (unsigned char *)(header + 1); + ciphertext = (unsigned char *)(header + 1) + get_padded_length(nonce_length); + + siv_tag_length = SIV_GetTagLength(siv); + + if (nonce_length < 1 || + ciphertext_length < siv_tag_length || + ciphertext_length - siv_tag_length > buffer_length) { + DEBUG_LOG("Unexpected nonce/ciphertext length"); + return 0; + } + + if (ef_body_length < sizeof (*header) + + NTS_MIN_UNPADDED_NONCE_LENGTH + get_padded_length(ciphertext_length)) { + DEBUG_LOG("Missing padding"); + return 0; + } + + *plaintext_length = ciphertext_length - siv_tag_length; + + if (!SIV_Decrypt(siv, nonce, nonce_length, packet, info->length - ef_body_length - 4, + ciphertext, ciphertext_length, plaintext, *plaintext_length)) { + DEBUG_LOG("SIV decrypt failed"); + return 0; + } + + return 1; +} diff --git a/nts_ntp_auth.h b/nts_ntp_auth.h new file mode 100644 index 0000000..856beb3 --- /dev/null +++ b/nts_ntp_auth.h @@ -0,0 +1,43 @@ +/* + 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 for NTS Authenticator and Encrypted Extension Fields + extension field + */ + +#ifndef GOT_NTS_NTP_AUTH_H +#define GOT_NTS_NTP_AUTH_H + +#include "ntp.h" +#include "siv.h" + +extern int NNA_GenerateAuthEF(NTP_Packet *packet, NTP_PacketInfo *info, SIV_Instance siv, + const unsigned char *nonce, int nonce_length, + const unsigned char *plaintext, int plaintext_length, + int min_ef_length); + +extern int NNA_DecryptAuthEF(NTP_Packet *packet, NTP_PacketInfo *info, SIV_Instance siv, + int ef_start, unsigned char *plaintext, int buffer_length, + int *plaintext_length); + +#endif diff --git a/nts_ntp_client.c b/nts_ntp_client.c new file mode 100644 index 0000000..9b324c1 --- /dev/null +++ b/nts_ntp_client.c @@ -0,0 +1,445 @@ +/* + 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. + * + ********************************************************************** + + ======================================================================= + + Client NTS-NTP authentication + */ + +#include "config.h" + +#include "sysincl.h" + +#include "nts_ntp_client.h" + +#include "conf.h" +#include "logging.h" +#include "memory.h" +#include "ntp.h" +#include "ntp_ext.h" +#include "ntp_sources.h" +#include "nts_ke_client.h" +#include "nts_ntp.h" +#include "nts_ntp_auth.h" +#include "sched.h" +#include "siv.h" +#include "util.h" + +#define MAX_TOTAL_COOKIE_LENGTH (8 * 108) +#define MIN_NKE_RETRY_INTERVAL 1000 + +struct NNC_Instance_Record { + const IPSockAddr *ntp_address; + IPSockAddr nts_address; + char *name; + SIV_Instance siv_c2s; + SIV_Instance siv_s2c; + NKC_Instance nke; + + struct timespec last_nke_attempt; + struct timespec last_nke_success; + NKE_Cookie cookies[NTS_MAX_COOKIES]; + int num_cookies; + int cookie_index; + int nak_response; + int ok_response; + unsigned char nonce[NTS_MIN_UNPADDED_NONCE_LENGTH]; + unsigned char uniq_id[NTS_MIN_UNIQ_ID_LENGTH]; +}; + +/* ================================================== */ + +static void +reset_instance(NNC_Instance inst) +{ + UTI_ZeroTimespec(&inst->last_nke_attempt); + UTI_ZeroTimespec(&inst->last_nke_success); + inst->num_cookies = 0; + inst->cookie_index = 0; + inst->nak_response = 0; + inst->ok_response = 1; + memset(inst->nonce, 0, sizeof (inst->nonce)); + memset(inst->uniq_id, 0, sizeof (inst->uniq_id)); +} + +/* ================================================== */ + +NNC_Instance +NNC_CreateInstance(IPSockAddr *nts_address, const char *name, const IPSockAddr *ntp_address) +{ + NNC_Instance inst; + + inst = MallocNew(struct NNC_Instance_Record); + + inst->ntp_address = ntp_address; + inst->nts_address = *nts_address; + inst->name = name ? Strdup(name) : NULL; + inst->siv_c2s = NULL; + inst->siv_s2c = NULL; + inst->nke = NULL; + + reset_instance(inst); + + return inst; +} + +/* ================================================== */ + +void +NNC_DestroyInstance(NNC_Instance inst) +{ + if (inst->nke) + NKC_DestroyInstance(inst->nke); + if (inst->siv_c2s) + SIV_DestroyInstance(inst->siv_c2s); + if (inst->siv_s2c) + SIV_DestroyInstance(inst->siv_s2c); + + Free(inst->name); + Free(inst); +} + +/* ================================================== */ + +static int +is_nke_needed(NNC_Instance inst) +{ + struct timespec now; + + /* Force NKE if a NAK was received since last valid auth */ + if (inst->nak_response && !inst->ok_response && inst->num_cookies > 0) { + inst->num_cookies = 0; + DEBUG_LOG("Dropped cookies"); + } + + /* Force NKE if the keys encrypting the cookies are too old */ + if (inst->num_cookies > 0) { + SCH_GetLastEventTime(&now, NULL, NULL); + if (fabs(UTI_DiffTimespecsToDouble(&inst->last_nke_success, &now)) > CNF_GetNtsRefresh()) + inst->num_cookies = 0; + } + + return inst->num_cookies == 0; +} + +/* ================================================== */ + +static int +set_ntp_address(NNC_Instance inst, NTP_Remote_Address *negotiated_address) +{ + NTP_Remote_Address old_address, new_address; + + old_address = *inst->ntp_address; + new_address = *negotiated_address; + + if (new_address.ip_addr.family == IPADDR_UNSPEC) + new_address.ip_addr = old_address.ip_addr; + if (new_address.port == 0) + new_address.port = old_address.port; + + if (UTI_CompareIPs(&old_address.ip_addr, &new_address.ip_addr, NULL) == 0 && + old_address.port == new_address.port) + /* Nothing to do */ + return 1; + + if (NSR_UpdateSourceNtpAddress(&old_address, &new_address) != NSR_Success) { + LOG(LOGS_ERR, "Could not change %s to negotiated address %s", + UTI_IPToString(&old_address.ip_addr), UTI_IPToString(&new_address.ip_addr)); + return 0; + } + + return 1; +} + +/* ================================================== */ + +static int +get_nke_data(NNC_Instance inst) +{ + NTP_Remote_Address ntp_address; + SIV_Algorithm siv; + NKE_Key c2s, s2c; + struct timespec now; + int got_data; + + assert(is_nke_needed(inst)); + + if (!inst->nke) { + SCH_GetLastEventTime(&now, NULL, NULL); + if (fabs(UTI_DiffTimespecsToDouble(&inst->last_nke_attempt, &now)) < + MIN_NKE_RETRY_INTERVAL) { + DEBUG_LOG("Limiting NTS-KE request rate"); + return 0; + } + + if (!inst->name) { + LOG(LOGS_ERR, "Missing name of %s for NTS-KE", + UTI_IPToString(&inst->nts_address.ip_addr)); + return 0; + } + + inst->nke = NKC_CreateInstance(&inst->nts_address, inst->name); + + if (!NKC_Start(inst->nke)) + return 0; + + inst->last_nke_attempt = now; + } + + if (NKC_IsActive(inst->nke)) + return 0; + + got_data = NKC_GetNtsData(inst->nke, &siv, &c2s, &s2c, + inst->cookies, &inst->num_cookies, NTS_MAX_COOKIES, + &ntp_address); + + NKC_DestroyInstance(inst->nke); + inst->nke = NULL; + + if (!got_data) + return 0; + + if (!set_ntp_address(inst, &ntp_address)) { + inst->num_cookies = 0; + return 0; + } + + inst->cookie_index = 0; + + if (inst->siv_c2s) + SIV_DestroyInstance(inst->siv_c2s); + if (inst->siv_s2c) + SIV_DestroyInstance(inst->siv_s2c); + + inst->siv_c2s = SIV_CreateInstance(siv); + inst->siv_s2c = SIV_CreateInstance(siv); + + if (!inst->siv_c2s || !inst->siv_s2c || + !SIV_SetKey(inst->siv_c2s, c2s.key, c2s.length) || + !SIV_SetKey(inst->siv_s2c, s2c.key, s2c.length)) { + DEBUG_LOG("Could not initialise SIV"); + inst->num_cookies = 0; + return 0; + } + + inst->nak_response = 0; + + SCH_GetLastEventTime(&inst->last_nke_success, NULL, NULL); + + return 1; +} + +/* ================================================== */ + +int +NNC_PrepareForAuth(NNC_Instance inst) +{ + if (is_nke_needed(inst)) { + if (!get_nke_data(inst)) + return 0; + } + + UTI_GetRandomBytes(&inst->uniq_id, sizeof (inst->uniq_id)); + UTI_GetRandomBytes(&inst->nonce, sizeof (inst->nonce)); + + return 1; +} + +/* ================================================== */ + +int +NNC_GenerateRequestAuth(NNC_Instance inst, NTP_Packet *packet, + NTP_PacketInfo *info) +{ + NKE_Cookie *cookie; + int i, req_cookies; + + if (inst->num_cookies == 0 || !inst->siv_c2s) + return 0; + + if (info->mode != MODE_CLIENT) + return 0; + + cookie = &inst->cookies[inst->cookie_index]; + req_cookies = MIN(NTS_MAX_COOKIES - inst->num_cookies + 1, + MAX_TOTAL_COOKIE_LENGTH / (cookie->length + 4)); + + if (!NEF_AddField(packet, info, NTP_EF_NTS_UNIQUE_IDENTIFIER, + &inst->uniq_id, sizeof (inst->uniq_id))) + return 0; + + if (!NEF_AddField(packet, info, NTP_EF_NTS_COOKIE, + cookie->cookie, cookie->length)) + return 0; + + for (i = 0; i < req_cookies - 1; i++) { + if (!NEF_AddField(packet, info, NTP_EF_NTS_COOKIE_PLACEHOLDER, + cookie->cookie, cookie->length)) + return 0; + } + + if (!NNA_GenerateAuthEF(packet, info, inst->siv_c2s, inst->nonce, sizeof (inst->nonce), + (const unsigned char *)"", 0, NTP_MAX_V4_MAC_LENGTH + 4)) + return 0; + + inst->num_cookies--; + inst->cookie_index = (inst->cookie_index + 1) % NTS_MAX_COOKIES; + inst->ok_response = 0; + + return 1; +} + +/* ================================================== */ + +static int +extract_cookies(NNC_Instance inst, unsigned char *plaintext, int length) +{ + int ef_type, ef_body_length, ef_length, parsed, index, acceptable, saved; + void *ef_body; + + acceptable = saved = 0; + + for (parsed = 0; parsed < length; parsed += ef_length) { + if (!NEF_ParseSingleField(plaintext, length, parsed, + &ef_length, &ef_type, &ef_body, &ef_body_length)) + break; + + if (ef_type != NTP_EF_NTS_COOKIE) + continue; + + if (ef_length < NTP_MIN_EF_LENGTH || ef_body_length > sizeof (inst->cookies[0].cookie)) { + DEBUG_LOG("Unexpected cookie length %d", ef_body_length); + continue; + } + + acceptable++; + + if (inst->num_cookies >= NTS_MAX_COOKIES) + continue; + + index = (inst->cookie_index + inst->num_cookies) % NTS_MAX_COOKIES; + memcpy(inst->cookies[index].cookie, ef_body, ef_body_length); + inst->cookies[index].length = ef_body_length; + inst->num_cookies++; + + saved++; + } + + DEBUG_LOG("Extracted %d cookies (saved %d)", acceptable, saved); + + return acceptable > 0; +} + +/* ================================================== */ + +int +NNC_CheckResponseAuth(NNC_Instance inst, NTP_Packet *packet, + NTP_PacketInfo *info) +{ + int ef_type, ef_body_length, ef_length, parsed, plaintext_length; + int has_valid_uniq_id = 0, has_valid_auth = 0; + unsigned char plaintext[NTP_MAX_EXTENSIONS_LENGTH]; + void *ef_body; + + if (info->ext_fields == 0 || info->mode != MODE_SERVER) + return 0; + + /* Accept only one response per request */ + if (inst->ok_response) + return 0; + + if (!inst->siv_s2c) + return 0; + + for (parsed = NTP_HEADER_LENGTH; parsed < info->length; parsed += ef_length) { + if (!NEF_ParseField(packet, info->length, parsed, + &ef_length, &ef_type, &ef_body, &ef_body_length)) + break; + + switch (ef_type) { + case NTP_EF_NTS_UNIQUE_IDENTIFIER: + if (ef_body_length != sizeof (inst->uniq_id) || + memcmp(ef_body, inst->uniq_id, sizeof (inst->uniq_id)) != 0) { + DEBUG_LOG("Invalid uniq id"); + return 0; + } + has_valid_uniq_id = 1; + break; + case NTP_EF_NTS_COOKIE: + DEBUG_LOG("Unencrypted cookie"); + break; + case NTP_EF_NTS_AUTH_AND_EEF: + if (parsed + ef_length != info->length) { + DEBUG_LOG("Auth not last EF"); + return 0; + } + + if (!NNA_DecryptAuthEF(packet, info, inst->siv_s2c, parsed, + plaintext, sizeof (plaintext), &plaintext_length)) + return 0; + + has_valid_auth = 1; + break; + default: + break; + } + } + + if (!has_valid_uniq_id || !has_valid_auth) { + if (has_valid_uniq_id && packet->stratum == NTP_INVALID_STRATUM && + ntohl(packet->reference_id) == NTP_KOD_NTS_NAK) { + DEBUG_LOG("NTS NAK"); + inst->nak_response = 1; + return 0; + } + + DEBUG_LOG("Missing NTS EF"); + return 0; + } + + if (!extract_cookies(inst, plaintext, plaintext_length)) + return 0; + + inst->ok_response = 1; + + /* At this point we know the client interoperates with the server. Allow a + new NTS-KE session to be started as soon as the cookies run out. */ + UTI_ZeroTimespec(&inst->last_nke_attempt); + + return 1; +} + +/* ================================================== */ + +void +NNC_ChangeAddress(NNC_Instance inst, IPAddr *address) +{ + if (inst->nke) + NKC_DestroyInstance(inst->nke); + + inst->nke = NULL; + inst->num_cookies = 0; + inst->nts_address.ip_addr = *address; + + reset_instance(inst); + + DEBUG_LOG("NTS reset"); +} diff --git a/nts_ntp_client.h b/nts_ntp_client.h new file mode 100644 index 0000000..23e7721 --- /dev/null +++ b/nts_ntp_client.h @@ -0,0 +1,46 @@ +/* + 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 client NTS-NTP authentication + */ + +#ifndef GOT_NTS_NTP_CLIENT_H +#define GOT_NTS_NTP_CLIENT_H + +#include "addressing.h" +#include "ntp.h" + +typedef struct NNC_Instance_Record *NNC_Instance; + +extern NNC_Instance NNC_CreateInstance(IPSockAddr *nts_address, const char *name, + const IPSockAddr *ntp_address); +extern void NNC_DestroyInstance(NNC_Instance inst); +extern int NNC_PrepareForAuth(NNC_Instance inst); +extern int NNC_GenerateRequestAuth(NNC_Instance inst, NTP_Packet *packet, + NTP_PacketInfo *info); +extern int NNC_CheckResponseAuth(NNC_Instance inst, NTP_Packet *packet, + NTP_PacketInfo *info); + +extern void NNC_ChangeAddress(NNC_Instance inst, IPAddr *address); + +#endif diff --git a/nts_ntp_server.c b/nts_ntp_server.c new file mode 100644 index 0000000..ecdaa5b --- /dev/null +++ b/nts_ntp_server.c @@ -0,0 +1,253 @@ +/* + 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. + * + ********************************************************************** + + ======================================================================= + + Server NTS-NTP authentication + */ + +#include "config.h" + +#include "sysincl.h" + +#include "nts_ntp_server.h" + +#include "conf.h" +#include "logging.h" +#include "memory.h" +#include "ntp.h" +#include "ntp_ext.h" +#include "nts_ke_server.h" +#include "nts_ntp.h" +#include "nts_ntp_auth.h" +#include "siv.h" +#include "util.h" + +struct NtsServer { + SIV_Instance siv; + unsigned char nonce[NTS_MIN_UNPADDED_NONCE_LENGTH]; + NKE_Cookie cookies[NTS_MAX_COOKIES]; + int num_cookies; + NTP_int64 req_tx; +}; + +/* The server instance handling all requests */ +struct NtsServer *server; + +/* ================================================== */ + +void +NNS_Initialise(void) +{ + /* Create an NTS-NTP server instance only if NTS-KE server is enabled */ + if (!CNF_GetNtsServerCertFile() || !CNF_GetNtsServerKeyFile()) { + server = NULL; + return; + } + + server = Malloc(sizeof (struct NtsServer)); + server->siv = SIV_CreateInstance(AEAD_AES_SIV_CMAC_256); +} + +/* ================================================== */ + +void +NNS_Finalise(void) +{ + if (!server) + return; + + SIV_DestroyInstance(server->siv); + Free(server); + server = NULL; +} + +/* ================================================== */ + +int +NNS_CheckRequestAuth(NTP_Packet *packet, NTP_PacketInfo *info, uint32_t *kod) +{ + int ef_type, ef_body_length, ef_length, has_uniq_id = 0, has_auth = 0, has_cookie = 0; + int i, plaintext_length, parsed, requested_cookies, cookie_length = -1, auth_start = 0; + unsigned char plaintext[NTP_MAX_EXTENSIONS_LENGTH]; + NKE_Cookie cookie; + NKE_Key c2s, s2c; + void *ef_body; + + if (!server) + return 0; + + *kod = 0; + + server->num_cookies = 0; + server->req_tx = packet->transmit_ts; + + if (info->ext_fields == 0 || info->mode != MODE_CLIENT) + return 0; + + requested_cookies = 0; + + for (parsed = NTP_HEADER_LENGTH; parsed < info->length; parsed += ef_length) { + if (!NEF_ParseField(packet, info->length, parsed, + &ef_length, &ef_type, &ef_body, &ef_body_length)) + break; + + switch (ef_type) { + case NTP_EF_NTS_UNIQUE_IDENTIFIER: + has_uniq_id = 1; + break; + case NTP_EF_NTS_COOKIE: + if (has_cookie || ef_body_length > sizeof (cookie.cookie)) + return 0; + cookie.length = ef_body_length; + memcpy(cookie.cookie, ef_body, ef_body_length); + has_cookie = 1; + /* Fall through */ + case NTP_EF_NTS_COOKIE_PLACEHOLDER: + requested_cookies++; + + if (cookie_length >= 0 && cookie_length != ef_body_length) { + DEBUG_LOG("Invalid cookie/placeholder length"); + return 0; + } + cookie_length = ef_body_length; + break; + case NTP_EF_NTS_AUTH_AND_EEF: + auth_start = parsed; + has_auth = 1; + break; + default: + break; + } + } + + if (!has_uniq_id || !has_cookie || !has_auth) { + DEBUG_LOG("Missing an NTS EF"); + return 0; + } + + if (!NKS_DecodeCookie(&cookie, &c2s, &s2c)) { + *kod = NTP_KOD_NTS_NAK; + return 0; + } + + if (!SIV_SetKey(server->siv, c2s.key, c2s.length)) { + DEBUG_LOG("Could not set C2S key"); + return 0; + } + + if (!NNA_DecryptAuthEF(packet, info, server->siv, auth_start, + plaintext, sizeof (plaintext), &plaintext_length)) { + *kod = NTP_KOD_NTS_NAK; + return 0; + } + + for (parsed = 0; parsed < plaintext_length; parsed += ef_length) { + if (!NEF_ParseSingleField(plaintext, plaintext_length, parsed, + &ef_length, &ef_type, &ef_body, &ef_body_length)) + break; + + switch (ef_type) { + case NTP_EF_NTS_COOKIE_PLACEHOLDER: + if (cookie_length != ef_body_length) { + DEBUG_LOG("Invalid cookie/placeholder length"); + return 0; + } + requested_cookies++; + break; + default: + break; + } + } + + if (!SIV_SetKey(server->siv, s2c.key, s2c.length)) { + DEBUG_LOG("Could not set S2C key"); + return 0; + } + + UTI_GetRandomBytes(server->nonce, sizeof (server->nonce)); + + server->num_cookies = MIN(NTS_MAX_COOKIES, requested_cookies); + for (i = 0; i < server->num_cookies; i++) + if (!NKS_GenerateCookie(&c2s, &s2c, &server->cookies[i])) + return 0; + + return 1; +} + +/* ================================================== */ + +int +NNS_GenerateResponseAuth(NTP_Packet *request, NTP_PacketInfo *req_info, + NTP_Packet *response, NTP_PacketInfo *res_info, + uint32_t kod) +{ + int i, ef_type, ef_body_length, ef_length, parsed; + void *ef_body; + unsigned char plaintext[NTP_MAX_EXTENSIONS_LENGTH]; + int plaintext_length; + + if (!server || req_info->mode != MODE_CLIENT || res_info->mode != MODE_SERVER) + return 0; + + /* Make sure this is a response to the expected request */ + if (UTI_CompareNtp64(&server->req_tx, &request->transmit_ts) != 0) + assert(0); + + for (parsed = NTP_HEADER_LENGTH; parsed < req_info->length; parsed += ef_length) { + if (!NEF_ParseField(request, req_info->length, parsed, + &ef_length, &ef_type, &ef_body, &ef_body_length)) + break; + + switch (ef_type) { + case NTP_EF_NTS_UNIQUE_IDENTIFIER: + /* Copy the ID from the request */ + if (!NEF_AddField(response, res_info, ef_type, ef_body, ef_body_length)) + return 0; + default: + break; + } + } + + /* NTS NAK response does not have any other fields */ + if (kod) + return 1; + + for (i = 0, plaintext_length = 0; i < server->num_cookies; i++) { + if (!NEF_SetField(plaintext, sizeof (plaintext), plaintext_length, + NTP_EF_NTS_COOKIE, &server->cookies[i].cookie, + server->cookies[i].length, &ef_length)) + return 0; + + plaintext_length += ef_length; + assert(plaintext_length <= sizeof (plaintext)); + } + + server->num_cookies = 0; + + if (!NNA_GenerateAuthEF(response, res_info, server->siv, + server->nonce, sizeof (server->nonce), + plaintext, plaintext_length, + req_info->length - res_info->length)) + return 0; + + return 1; +} diff --git a/nts_ntp_server.h b/nts_ntp_server.h new file mode 100644 index 0000000..fea28f2 --- /dev/null +++ b/nts_ntp_server.h @@ -0,0 +1,40 @@ +/* + 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 server NTS-NTP authentication + */ + +#ifndef GOT_NTS_NTP_SERVER_H +#define GOT_NTS_NTP_SERVER_H + +#include "ntp.h" + +extern void NNS_Initialise(void); +extern void NNS_Finalise(void); + +extern int NNS_CheckRequestAuth(NTP_Packet *packet, NTP_PacketInfo *info, uint32_t *kod); +extern int NNS_GenerateResponseAuth(NTP_Packet *request, NTP_PacketInfo *req_info, + NTP_Packet *response, NTP_PacketInfo *res_info, + uint32_t kod); + +#endif diff --git a/stubs.c b/stubs.c index d9dce17..63aea14 100644 --- a/stubs.c +++ b/stubs.c @@ -42,6 +42,8 @@ #include "ntp_signd.h" #include "nts_ke_client.h" #include "nts_ke_server.h" +#include "nts_ntp_client.h" +#include "nts_ntp_server.h" #include "privops.h" #include "refclock.h" #include "sched.h" @@ -452,6 +454,67 @@ CMC_DestroyInstance(CMC_Instance inst) #ifndef FEAT_NTS +void +NNS_Initialise(void) +{ +} + +void +NNS_Finalise(void) +{ +} + +int +NNS_CheckRequestAuth(NTP_Packet *packet, NTP_PacketInfo *info, uint32_t *kod) +{ + *kod = 0; + return 0; +} + +int +NNS_GenerateResponseAuth(NTP_Packet *request, NTP_PacketInfo *req_info, + NTP_Packet *response, NTP_PacketInfo *res_info, + uint32_t kod) +{ + return 0; +} + +NNC_Instance +NNC_CreateInstance(IPSockAddr *nts_address, const char *name, const IPSockAddr *ntp_address) +{ + return NULL; +} + +void +NNC_DestroyInstance(NNC_Instance inst) +{ +} + +int +NNC_PrepareForAuth(NNC_Instance inst) +{ + return 1; +} + +int +NNC_GenerateRequestAuth(NNC_Instance inst, NTP_Packet *packet, NTP_PacketInfo *info) +{ + DEBUG_LOG("NTS support disabled"); + return 0; +} + +int +NNC_CheckResponseAuth(NNC_Instance inst, NTP_Packet *packet, NTP_PacketInfo *info) +{ + DEBUG_LOG("NTS support disabled"); + return 0; +} + +void +NNC_ChangeAddress(NNC_Instance inst, IPAddr *address) +{ +} + void NKC_Initialise(void) {