From bddb3b32285115e2bba55390e2a63967bcf5d395 Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Wed, 6 May 2020 13:02:45 +0200 Subject: [PATCH] sources: enable selection options with authentication When authentication is enabled for an NTP source, unauthenticated NTP sources need to be disabled or limited in selection. That might be difficult to do when the configuration comes from different sources (e.g. networking scripts adding servers from DHCP). Define four modes for the source selection to consider authentication: require, prefer, mix, ignore. In different modes different selection options (require, trust, noselect) are added to authenticated and unauthenticated sources. The mode can be selected by the authselectmode directive. The mix mode is the default. The ignore mode enables the old behavior, where all sources are used exactly as specified in the configuration. --- conf.c | 29 +++++++++ conf.h | 2 + doc/chrony.conf.adoc | 68 ++++++++++++++++++++ sources.c | 56 ++++++++++++++++- sources.h | 8 +++ test/simulation/120-selectoptions | 24 +++++++ test/unit/sources.c | 100 +++++++++++++++++++++++++++--- 7 files changed, 278 insertions(+), 9 deletions(-) diff --git a/conf.c b/conf.c index a604357..b32e4ff 100644 --- a/conf.c +++ b/conf.c @@ -51,6 +51,7 @@ static int parse_double(char *line, double *result); static int parse_null(char *line); static void parse_allow_deny(char *line, ARR_Instance restrictions, int allow); +static void parse_authselectmode(char *); static void parse_bindacqaddress(char *); static void parse_bindaddress(char *); static void parse_bindcmdaddress(char *); @@ -89,6 +90,7 @@ static double max_clock_error = 1.0; /* in ppm */ static double max_drift = 500000.0; /* in ppm */ static double max_slew_rate = 1e6 / 12.0; /* in ppm */ +static SRC_AuthSelectMode authselect_mode = SRC_AUTHSELECT_MIX; static double max_distance = 3.0; static double max_jitter = 1.0; static double reselect_distance = 1e-4; @@ -461,6 +463,8 @@ CNF_ParseLine(const char *filename, int number, char *line) parse_int(p, &acquisition_port); } else if (!strcasecmp(command, "allow")) { parse_allow_deny(p, ntp_restrictions, 1); + } else if (!strcasecmp(command, "authselectmode")) { + parse_authselectmode(p); } else if (!strcasecmp(command, "bindacqaddress")) { parse_bindacqaddress(p); } else if (!strcasecmp(command, "bindaddress")) { @@ -1141,6 +1145,23 @@ parse_allow_deny(char *line, ARR_Instance restrictions, int allow) /* ================================================== */ +static void +parse_authselectmode(char *line) +{ + if (!strcasecmp(line, "require")) + authselect_mode = SRC_AUTHSELECT_REQUIRE; + else if (!strcasecmp(line, "prefer")) + authselect_mode = SRC_AUTHSELECT_PREFER; + else if (!strcasecmp(line, "mix")) + authselect_mode = SRC_AUTHSELECT_MIX; + else if (!strcasecmp(line, "ignore")) + authselect_mode = SRC_AUTHSELECT_IGNORE; + else + command_parse_error(); +} + +/* ================================================== */ + static void parse_bindacqaddress(char *line) { @@ -1680,6 +1701,14 @@ CNF_GetCorrectionTimeRatio(void) /* ================================================== */ +SRC_AuthSelectMode +CNF_GetAuthSelectMode(void) +{ + return authselect_mode; +} + +/* ================================================== */ + double CNF_GetMaxSlewRate(void) { diff --git a/conf.h b/conf.h index 4d27ef1..045e8c3 100644 --- a/conf.h +++ b/conf.h @@ -30,6 +30,7 @@ #include "addressing.h" #include "reference.h" +#include "sources.h" extern void CNF_Initialise(int restarted, int client_only); extern void CNF_Finalise(void); @@ -87,6 +88,7 @@ extern double CNF_GetMaxDrift(void); extern double CNF_GetCorrectionTimeRatio(void); extern double CNF_GetMaxSlewRate(void); +extern SRC_AuthSelectMode CNF_GetAuthSelectMode(void); extern double CNF_GetMaxDistance(void); extern double CNF_GetMaxJitter(void); extern double CNF_GetReselectDistance(void); diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc index f393da0..462e98f 100644 --- a/doc/chrony.conf.adoc +++ b/doc/chrony.conf.adoc @@ -739,6 +739,74 @@ with correct time. === Source selection +[[authselectmode]]*authselectmode* _mode_:: +NTP sources can be specified with the *key* or *nts* option to enable +authentication to limit the impact of man-in-the-middle attacks. The +attackers can drop or delay NTP packets (up to the *maxdelay* and +<> limits), but they cannot modify the timestamps +contained in the packets. The attack can cause only a limited slew or step, and +also cause the clock to run faster or slower than real time (up to double of +the <> limit). ++ +When authentication is enabled for an NTP source, it is important to disable +unauthenticated NTP sources which could be exploited in the attack, e.g. if +they are not reachable only over a trusted network. Alternatively, the source +selection can be configured with the *require* and *trust* options to +synchronise to the unauthenticated sources only if they agree with the +authenticated sources and might have a positive impact on the accuracy of the +clock. Note that in this case the impact of the attack is higher. The attackers +cannot cause an arbitrarily large step or slew, but they have more control over +the frequency of the clock and can cause *chronyd* to report false information, +e.g. a significantly smaller root delay and dispersion. ++ +This directive determines the default selection options for authenticated and +unauthenticated sources in order to simplify the configuration with the +configuration file and *chronyc* commands. It sets a policy for authentication. ++ +There are four modes: ++ +*require*::: +Authentication is strictly required for NTP sources in this mode. If any +unauthenticated NTP sources are specified, they will automatically get the +*noselect* option to prevent them from being selected for synchronisation. +*prefer*::: +In this mode, authentication is optional and preferred. If it is enabled for at +least one NTP source, all unauthenticated NTP sources will get the *noselect* +option. +*mix*::: +In this mode, authentication is optional and synchronisation to a mix of +authenticated and unauthenticated NTP sources is allowed. If both authenticated +and unauthenticated NTP sources are specified, all authenticated NTP sources +and reference clocks will get the *require* and *trust* options to prevent +synchronisation to unauthenticated NTP sources if they do not agree with a +majority of the authenticated sources and reference clocks. This is the default +mode. +*ignore*::: +In this mode, authentication is ignored in the source selection. All sources +will have only the selection options that were specified in the configuration +file, or *chronyc* command. This was the behaviour of *chronyd* in versions +before 4.0. +:: ++ +As an example, the following configuration using the default *mix* mode: ++ +---- +server foo.example.net nts +server bar.example.net nts +server baz.example.net +refclock SHM 0 +---- ++ +is equivalent to the following configuration using the *ignore* mode: ++ +---- +authselectmode ignore +server foo.example.net nts require trust +server bar.example.net nts require trust +server baz.example.net +refclock SHM 0 require trust +---- + [[combinelimit]]*combinelimit* _limit_:: When *chronyd* has multiple sources available for synchronisation, it has to select one source as the synchronisation source. The measured offsets and diff --git a/sources.c b/sources.c index 4ea64d6..edc5358 100644 --- a/sources.c +++ b/sources.c @@ -495,10 +495,62 @@ SRC_ResetReachability(SRC_Instance inst) static void update_sel_options(void) { - int i; + int options, auth_ntp_options, unauth_ntp_options, refclk_options; + int i, auth_ntp_sources, unauth_ntp_sources; + + auth_ntp_sources = unauth_ntp_sources = 0; for (i = 0; i < n_sources; i++) { - sources[i]->sel_options = sources[i]->conf_sel_options; + if (sources[i]->type != SRC_NTP) + continue; + if (sources[i]->authenticated) + auth_ntp_sources++; + else + unauth_ntp_sources++; + } + + auth_ntp_options = unauth_ntp_options = refclk_options = 0; + + /* Determine which selection options need to be added to authenticated NTP + sources, unauthenticated NTP sources, and refclocks, to follow the + configured selection mode */ + switch (CNF_GetAuthSelectMode()) { + case SRC_AUTHSELECT_IGNORE: + break; + case SRC_AUTHSELECT_MIX: + if (auth_ntp_sources > 0 && unauth_ntp_sources > 0) + auth_ntp_options = refclk_options = SRC_SELECT_REQUIRE | SRC_SELECT_TRUST; + break; + case SRC_AUTHSELECT_PREFER: + if (auth_ntp_sources > 0) + unauth_ntp_options = SRC_SELECT_NOSELECT; + break; + case SRC_AUTHSELECT_REQUIRE: + unauth_ntp_options = SRC_SELECT_NOSELECT; + break; + default: + assert(0); + } + + for (i = 0; i < n_sources; i++) { + options = sources[i]->conf_sel_options; + + switch (sources[i]->type) { + case SRC_NTP: + options |= sources[i]->authenticated ? auth_ntp_options : unauth_ntp_options; + break; + case SRC_REFCLOCK: + options |= refclk_options; + break; + default: + assert(0); + } + + if (sources[i]->sel_options != options) { + DEBUG_LOG("changing %s from %x to %x", source_to_string(sources[i]), + (unsigned int)sources[i]->sel_options, (unsigned int)options); + sources[i]->sel_options = options; + } } } diff --git a/sources.h b/sources.h index 6a6b870..d4c6f55 100644 --- a/sources.h +++ b/sources.h @@ -51,6 +51,14 @@ extern void SRC_Initialise(void); /* Finalisation function */ extern void SRC_Finalise(void); +/* Modes for selecting NTP sources based on their authentication status */ +typedef enum { + SRC_AUTHSELECT_IGNORE, + SRC_AUTHSELECT_MIX, + SRC_AUTHSELECT_PREFER, + SRC_AUTHSELECT_REQUIRE, +} SRC_AuthSelectMode; + typedef enum { SRC_NTP, /* NTP client/peer */ SRC_REFCLOCK /* Rerefence clock */ diff --git a/test/simulation/120-selectoptions b/test/simulation/120-selectoptions index 7e10293..a6b1e14 100755 --- a/test/simulation/120-selectoptions +++ b/test/simulation/120-selectoptions @@ -65,4 +65,28 @@ check_packet_interval || test_fail check_source_selection && test_fail check_sync && test_fail +cat > tmp/keys <<-EOF +1 MD5 HEX:1B81CBF88D4A73F2E8CE59647F6E5C1719B6CAF5 +EOF + +server_conf="keyfile tmp/keys" +client_server_conf=" +server 192.168.123.1 key 1 +server 192.168.123.2 +server 192.168.123.3" + +for authselectmode in require prefer mix ignore; do + client_conf="keyfile tmp/keys + authselectmode $authselectmode" + run_test || test_fail + check_chronyd_exit || test_fail + check_source_selection || test_fail + check_packet_interval || test_fail + if [ $authselectmode = ignore ]; then + check_sync || test_fail + else + check_sync && test_fail + fi +done + test_pass diff --git a/test/unit/sources.c b/test/unit/sources.c index 6f7f3d5..3d97a99 100644 --- a/test/unit/sources.c +++ b/test/unit/sources.c @@ -21,14 +21,27 @@ #include #include "test.h" +static SRC_Instance +create_source(SRC_Type type, int authenticated, int sel_options) +{ + static IPAddr addr; + + TST_GetRandomAddress(&addr, IPADDR_UNSPEC, -1); + + return SRC_CreateNewInstance(UTI_IPToRefid(&addr), type, authenticated, sel_options, + type == SRC_NTP ? &addr : NULL, + SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES, 0.0, 1.0); +} + void test_unit(void) { + SRC_AuthSelectMode sel_mode; SRC_Instance srcs[16]; RPT_SourceReport report; NTP_Sample sample; - IPAddr addr; - int i, j, k, l, samples, sel_options; + int i, j, k, l, n1, n2, n3, samples, sel_options; + char conf[128]; CNF_Initialise(0, 0); LCL_Initialise(); @@ -45,15 +58,11 @@ test_unit(void) for (j = 0; j < sizeof (srcs) / sizeof (srcs[0]); j++) { TEST_CHECK(n_sources == j); - TST_GetRandomAddress(&addr, IPADDR_UNSPEC, -1); - sel_options = i & random() & (SRC_SELECT_NOSELECT | SRC_SELECT_PREFER | SRC_SELECT_TRUST | SRC_SELECT_REQUIRE); DEBUG_LOG("added source %d options %d", j, sel_options); - srcs[j] = SRC_CreateNewInstance(UTI_IPToRefid(&addr), SRC_NTP, 0, sel_options, &addr, - SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES, - 0.0, 1.0); + srcs[j] = create_source(SRC_NTP, 0, sel_options); SRC_UpdateReachability(srcs[j], 1); samples = (i + j) % 5 + 3; @@ -132,6 +141,83 @@ test_unit(void) } } + TEST_CHECK(CNF_GetAuthSelectMode() == SRC_AUTHSELECT_MIX); + + for (i = 0; i < 1000; i++) { + DEBUG_LOG("iteration %d", i); + + switch (i % 4) { + case 0: + snprintf(conf, sizeof (conf), "authselectmode require"); + sel_mode = SRC_AUTHSELECT_REQUIRE; + break; + case 1: + snprintf(conf, sizeof (conf), "authselectmode prefer"); + sel_mode = SRC_AUTHSELECT_PREFER; + break; + case 2: + snprintf(conf, sizeof (conf), "authselectmode mix"); + sel_mode = SRC_AUTHSELECT_MIX; + break; + case 3: + snprintf(conf, sizeof (conf), "authselectmode ignore"); + sel_mode = SRC_AUTHSELECT_IGNORE; + break; + } + + CNF_ParseLine(NULL, 0, conf); + TEST_CHECK(CNF_GetAuthSelectMode() == sel_mode); + + sel_options = random() & (SRC_SELECT_NOSELECT | SRC_SELECT_PREFER | + SRC_SELECT_TRUST | SRC_SELECT_REQUIRE); + + n1 = random() % 3; + n2 = random() % 3; + n3 = random() % 3; + assert(n1 + n2 + n3 < sizeof (srcs) / sizeof (srcs[0])); + + for (j = 0; j < n1; j++) + srcs[j] = create_source(SRC_REFCLOCK, random() % 2, sel_options); + for (; j < n1 + n2; j++) + srcs[j] = create_source(SRC_NTP, 1, sel_options); + for (; j < n1 + n2 + n3; j++) + srcs[j] = create_source(SRC_NTP, 0, sel_options); + + switch (sel_mode) { + case SRC_AUTHSELECT_IGNORE: + for (j = 0; j < n1 + n2 + n3; j++) + TEST_CHECK(srcs[j]->sel_options == sel_options); + break; + case SRC_AUTHSELECT_MIX: + for (j = 0; j < n1 + n2; j++) + TEST_CHECK(srcs[j]->sel_options == + (sel_options | (n2 > 0 && n3 > 0 ? SRC_SELECT_REQUIRE | SRC_SELECT_TRUST : 0))); + for (; j < n1 + n2 + n3; j++) + TEST_CHECK(srcs[j]->sel_options == sel_options); + break; + case SRC_AUTHSELECT_PREFER: + for (j = 0; j < n1 + n2; j++) + TEST_CHECK(srcs[j]->sel_options == sel_options); + for (; j < n1 + n2 + n3; j++) + TEST_CHECK(srcs[j]->sel_options == (sel_options | (n2 > 0 ? SRC_SELECT_NOSELECT : 0))); + break; + case SRC_AUTHSELECT_REQUIRE: + for (j = 0; j < n1 + n2; j++) + TEST_CHECK(srcs[j]->sel_options == sel_options); + for (; j < n1 + n2 + n3; j++) + TEST_CHECK(srcs[j]->sel_options == (sel_options | SRC_SELECT_NOSELECT)); + break; + default: + assert(0); + } + + for (j = n1 + n2 + n3 - 1; j >= 0; j--) { + if (j < n1 + n2) + TEST_CHECK(srcs[j]->sel_options == sel_options); + SRC_DestroyInstance(srcs[j]); + } + } + REF_Finalise(); SRC_Finalise(); SCH_Finalise();