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();