Very similar to the recent addition of nbd_opt_structured_reply, with the new nbd_opt_starttls() finally giving us fine-grained control over NBD_OPT_STARTTLS, and allowing productive handshaking with a server in SELECTIVETLS mode.
With this patch, it is now easy to reproduce the scenario of nbdkit's CVE-2021-3716, where a plaintext meddler-in-the-middle attacker could cause client denial of service when a --tls=on server failed to reset state after NBD_OPT_STARTTLS. This also exposed the fact that nbdkit was not gracefully handling NBD_OPT_INFO from a plaintext client connecting to a --tls=require server; the testsuite is skipped unless using a fixed nbdkit (v1.33.2 or later). --- generator/API.ml | 98 +++++++++++-- generator/states-newstyle-opt-starttls.c | 40 ++++-- generator/states-newstyle.c | 3 + lib/opt.c | 50 +++++++ tests/Makefile.am | 5 + tests/opt-starttls.c | 166 +++++++++++++++++++++++ .gitignore | 1 + 7 files changed, 343 insertions(+), 20 deletions(-) create mode 100644 tests/opt-starttls.c diff --git a/generator/API.ml b/generator/API.ml index 69849102..8301ec9f 100644 --- a/generator/API.ml +++ b/generator/API.ml @@ -599,7 +599,11 @@ "set_tls", { =item C<LIBNBD_TLS_DISABLE> Disable TLS. (The default setting, unless using L<nbd_connect_uri(3)> with -a URI that requires TLS) +a URI that requires TLS). + +This setting is also useful during integration testing when using +L<nbd_set_opt_mode(3)> and L<nbd_opt_starttls(3)> for manual +control over when to request TLS negotiation. =item C<LIBNBD_TLS_ALLOW> @@ -632,7 +636,8 @@ "set_tls", { test whether this is the case with L<nbd_supports_tls(3)>."; example = Some "examples/encryption.c"; see_also = [SectionLink "ENCRYPTION AND AUTHENTICATION"; - Link "get_tls"; Link "get_tls_negotiated"]; + Link "get_tls"; Link "get_tls_negotiated"; + Link "opt_starttls"]; }; "get_tls", { @@ -657,7 +662,7 @@ "get_tls_negotiated", { After connecting you may call this to find out if the connection is using TLS. -This is only really useful if you set the TLS request mode +This is normally useful only if you set the TLS request mode to C<LIBNBD_TLS_ALLOW> (see L<nbd_set_tls(3)>), because in this mode we try to use TLS but fall back to unencrypted if it was not available. This function will tell you if TLS was @@ -665,8 +670,14 @@ "get_tls_negotiated", { In C<LIBNBD_TLS_REQUIRE> mode (the most secure) the connection would have failed if TLS could not be negotiated, and in -C<LIBNBD_TLS_DISABLE> mode TLS is not tried."; - see_also = [Link "set_tls"; Link "get_tls"]; +C<LIBNBD_TLS_DISABLE> mode TLS is not tried automatically. + +However, when doing manual integration testing of server +behavior, when you use L<nbd_set_opt_mode(3)> along with TLS +request mode C<LIBNBD_TLS_DISABLE> before triggering the TLS +handshake with L<nbd_opt_starttls(3)>, then this will report +the result of that manual effort."; + see_also = [Link "set_tls"; Link "get_tls"; Link "opt_starttls"]; }; "set_tls_certificates", { @@ -1092,11 +1103,12 @@ "set_opt_mode", { newstyle server. This setting has no effect when connecting to an oldstyle server. -Note that libnbd defaults to attempting +Note that libnbd defaults to attempting C<NBD_OPT_STARTTLS> and C<NBD_OPT_STRUCTURED_REPLY> before letting you control remaining -negotiation steps; if you need control over this step as well, -first set L<nbd_set_request_structured_replies(3)> to false before -starting the connection attempt. +negotiation steps; if you need control over these steps as well, +first set L<nbd_set_tls(3)> to C<LIBNBD_TLS_DISABLE> and +L<nbd_set_request_structured_replies(3)> to false before starting +the connection attempt. When option mode is enabled, you have fine-grained control over which options are negotiated, compared to the default of the server @@ -1110,9 +1122,9 @@ "set_opt_mode", { see_also = [Link "get_opt_mode"; Link "aio_is_negotiating"; Link "opt_abort"; Link "opt_go"; Link "opt_list"; Link "opt_info"; Link "opt_list_meta_context"; - Link "opt_set_meta_context"; + Link "opt_set_meta_context"; Link "opt_starttls"; Link "opt_structured_reply"; - Link "set_request_structured_replies"; + Link "set_tls"; Link "set_request_structured_replies"; Link "aio_connect"]; }; @@ -1166,6 +1178,44 @@ "opt_abort", { see_also = [Link "set_opt_mode"; Link "aio_opt_abort"; Link "opt_go"]; }; + "opt_starttls", { + default_call with + args = []; ret = RBool; + permitted_states = [ Negotiating ]; + shortdesc = "request the server to initiate TLS"; + longdesc = "\ +Request that the server initiate a secure TLS connection, by +sending C<NBD_OPT_STARTTLS>. This can only be used if +L<nbd_set_opt_mode(3)> enabled option mode; furthermore, if you +use L<nbd_set_tls(3)> to request anything other than the default +of C<LIBNBD_TLS_DISABLE>, then libnbd will have already attempted +a TLS connection prior to allowing you control over option +negotiation. This command is disabled if L<nbd_supports_tls(3)> +reports false. + +This function is mainly useful for integration testing of corner +cases in server handling; in particular, misuse of this function +when coupled with a server that is not careful about resetting +stateful commands such as L<nbd_opt_structured_reply(3)> could +result in a security hole (see CVE-2021-3716 against nbdkit, for +example). Thus, when security is a concern, you should instead +prefer to use L<nbd_set_tls(3)> with C<LIBNBD_TLS_REQUIRE> and +let libnbd negotiate TLS automatically. + +This function returns true if the server replies with success, +false if the server replies with an error, and fails only if +the server does not reply (such as for a loss of connection, +which can include when the server rejects credentials supplied +during the TLS handshake). Note that the NBD protocol documents +that requesting TLS after it is already enabled is a client +error; most servers will gracefully fail a second request, but +that does not downgrade a TLS session that has already been +established, as reported by L<nbd_get_tls_negotiated(3)>."; + see_also = [Link "set_opt_mode"; Link "aio_opt_starttls"; + Link "set_tls"; Link "get_tls_negotiated"; + Link "supports_tls"] + }; + "opt_structured_reply", { default_call with args = []; ret = RBool; @@ -2815,6 +2865,30 @@ "aio_opt_abort", { see_also = [Link "set_opt_mode"; Link "opt_abort"]; }; + "aio_opt_starttls", { + default_call with + args = []; + optargs = [ OClosure completion_closure ]; + ret = RErr; + permitted_states = [ Negotiating ]; + shortdesc = "request the server to initiate TLS"; + longdesc = "\ +Request that the server initiate a secure TLS connection, by +sending C<NBD_OPT_STARTTLS>. This behaves like the synchronous +counterpart L<nbd_opt_starttls(3)>, except that it does +not wait for the server's response. + +To determine when the request completes, wait for +L<nbd_aio_is_connecting(3)> to return false. Or supply the optional +C<completion_callback> which will be invoked as described in +L<libnbd(3)/Completion callbacks>, except that it is automatically +retired regardless of return value. Note that detecting whether the +server returns an error (as is done by the return value of the +synchronous counterpart) is only possible with a completion +callback."; + see_also = [Link "set_opt_mode"; Link "opt_starttls"]; + }; + "aio_opt_structured_reply", { default_call with args = []; @@ -3744,6 +3818,8 @@ let first_version = "aio_opt_set_meta_context_queries", (1, 16); "opt_structured_reply", (1, 16); "aio_opt_structured_reply", (1, 16); + "opt_starttls", (1, 16); + "aio_opt_starttls", (1, 16); (* These calls are proposed for a future version of libnbd, but * have not been added to any released version so far. diff --git a/generator/states-newstyle-opt-starttls.c b/generator/states-newstyle-opt-starttls.c index 1233263b..fc72c43c 100644 --- a/generator/states-newstyle-opt-starttls.c +++ b/generator/states-newstyle-opt-starttls.c @@ -21,10 +21,15 @@ STATE_MACHINE { NEWSTYLE.OPT_STARTTLS.START: assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE); - /* If TLS was not requested we skip this option and go to the next one. */ - if (h->tls == LIBNBD_TLS_DISABLE) { - SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); - return 0; + if (h->opt_current == NBD_OPT_STARTTLS) + assert (h->opt_mode); + else { + /* If TLS was not requested we skip this option and go to the next one. */ + if (h->tls == LIBNBD_TLS_DISABLE) { + SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); + return 0; + } + assert (CALLBACK_IS_NULL (h->opt_cb.completion)); } h->sbuf.option.version = htobe64 (NBD_NEW_VERSION); @@ -68,12 +73,14 @@ NEWSTYLE.OPT_STARTTLS.RECV_REPLY_PAYLOAD: NEWSTYLE.OPT_STARTTLS.CHECK_REPLY: uint32_t reply; struct socket *new_sock; + int err = ENOTSUP; reply = be32toh (h->sbuf.or.option_reply.reply); switch (reply) { case NBD_REP_ACK: nbd_internal_reset_size_and_flags (h); h->structured_replies = false; + h->meta_valid = false; new_sock = nbd_internal_crypto_create_session (h, h->sock); if (new_sock == NULL) { SET_NEXT_STATE (%.DEAD); @@ -86,6 +93,9 @@ NEWSTYLE.OPT_STARTTLS.CHECK_REPLY: SET_NEXT_STATE (%TLS_HANDSHAKE_WRITE); return 0; + case NBD_REP_ERR_INVALID: + err = EINVAL; + /* fallthrough */ default: if (handle_reply_error (h) == -1) { SET_NEXT_STATE (%.DEAD); @@ -102,10 +112,16 @@ NEWSTYLE.OPT_STARTTLS.CHECK_REPLY: return 0; } - debug (h, - "server refused TLS (%s), continuing with unencrypted connection", - reply == NBD_REP_ERR_POLICY ? "policy" : "not supported"); - SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); + debug (h, "server refused TLS (%s)", + reply == NBD_REP_ERR_POLICY ? "policy" : + reply == NBD_REP_ERR_INVALID ? "invalid request" : "not supported"); + CALL_CALLBACK (h->opt_cb.completion, &err); + if (h->opt_current == NBD_OPT_STARTTLS) + SET_NEXT_STATE (%.NEGOTIATING); + else { + debug (h, "continuing with unencrypted connection"); + SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); + } return 0; } return 0; @@ -149,12 +165,18 @@ NEWSTYLE.OPT_STARTTLS.TLS_HANDSHAKE_WRITE: return 0; NEWSTYLE.OPT_STARTTLS.TLS_HANDSHAKE_DONE: + int err = 0; + /* Finished handshake. */ h->tls_negotiated = true; nbd_internal_crypto_debug_tls_enabled (h); + CALL_CALLBACK (h->opt_cb.completion, &err); /* Continue with option negotiation. */ - SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); + if (h->opt_current == NBD_OPT_STARTTLS) + SET_NEXT_STATE (%.NEGOTIATING); + else + SET_NEXT_STATE (%^OPT_STRUCTURED_REPLY.START); return 0; } /* END STATE MACHINE */ diff --git a/generator/states-newstyle.c b/generator/states-newstyle.c index 4652bc21..c88430e2 100644 --- a/generator/states-newstyle.c +++ b/generator/states-newstyle.c @@ -146,6 +146,9 @@ NEWSTYLE.START: case NBD_OPT_STRUCTURED_REPLY: SET_NEXT_STATE (%OPT_STRUCTURED_REPLY.START); return 0; + case NBD_OPT_STARTTLS: + SET_NEXT_STATE (%OPT_STARTTLS.START); + return 0; case 0: break; default: diff --git a/lib/opt.c b/lib/opt.c index 18bb4086..4ebb689e 100644 --- a/lib/opt.c +++ b/lib/opt.c @@ -139,6 +139,31 @@ nbd_unlocked_opt_abort (struct nbd_handle *h) return wait_for_option (h); } +/* Issue NBD_OPT_STARTTLS and wait for the reply. */ +int +nbd_unlocked_opt_starttls (struct nbd_handle *h) +{ + int err; + nbd_completion_callback c = { .callback = go_complete, .user_data = &err }; + int r = nbd_unlocked_aio_opt_starttls (h, &c); + + if (r == -1) + return r; + + r = wait_for_option (h); + if (r == 0) { + if (nbd_internal_is_state_negotiating (get_next_state (h))) + r = err == 0; + else { + assert (nbd_internal_is_state_dead (get_next_state (h))); + set_error (err, + "failed to get response to opt_starttls request"); + r = -1; + } + } + return r; +} + /* Issue NBD_OPT_STRUCTURED_REPLY and wait for the reply. */ int nbd_unlocked_opt_structured_reply (struct nbd_handle *h) @@ -336,6 +361,31 @@ nbd_unlocked_aio_opt_abort (struct nbd_handle *h) return 0; } +/* Issue NBD_OPT_STARTTLS without waiting. */ +int +nbd_unlocked_aio_opt_starttls (struct nbd_handle *h, + nbd_completion_callback *complete) +{ + if ((h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE) == 0) { + set_error (ENOTSUP, "server is not using fixed newstyle protocol"); + return -1; + } + +#ifndef HAVE_GNUTLS + set_error (ENOTSUP, "libnbd was compiled without TLS support"); + return -1; + +#else + h->opt_current = NBD_OPT_STARTTLS; + h->opt_cb.completion = *complete; + SET_CALLBACK_TO_NULL (*complete); + + if (nbd_internal_run (h, cmd_issue) == -1) + debug (h, "option queued, ignoring state machine failure"); + return 0; +#endif +} + /* Issue NBD_OPT_STRUCTURED_REPLY without waiting. */ int nbd_unlocked_aio_opt_structured_reply (struct nbd_handle *h, diff --git a/tests/Makefile.am b/tests/Makefile.am index 49ea6864..dd157518 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -658,12 +658,14 @@ if HAVE_PSKTOOL check_PROGRAMS += \ connect-tls-psk \ + opt-starttls \ aio-parallel-tls \ aio-parallel-load-tls \ synch-parallel-tls \ $(NULL) TESTS += \ connect-tls-psk \ + opt-starttls \ aio-parallel-tls.sh \ aio-parallel-load-tls.sh \ synch-parallel-tls.sh \ @@ -677,6 +679,9 @@ connect_tls_psk_CPPFLAGS = \ $(NULL) connect_tls_psk_LDADD = $(top_builddir)/lib/libnbd.la +opt_starttls_SOURCES = opt-starttls.c requires.c requires.h +opt_starttls_LDADD = $(top_builddir)/lib/libnbd.la + aio_parallel_tls_SOURCES = aio-parallel.c aio_parallel_tls_CPPFLAGS = \ $(AM_CPPFLAGS) \ diff --git a/tests/opt-starttls.c b/tests/opt-starttls.c new file mode 100644 index 00000000..b90a0dcd --- /dev/null +++ b/tests/opt-starttls.c @@ -0,0 +1,166 @@ +/* NBD client library in userspace + * Copyright (C) 2013-2022 Red Hat Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* Test nbd_opt_starttls to nbdkit in permissive mode. */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <inttypes.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> + +#include <libnbd.h> + +#include "requires.h" + +struct expected { + int first_opt_sr; + int64_t first_size; + int first_opt_tls; + int first_get_sr; + int second_opt_sr; + int second_opt_tls; + int get_tls; + int second_get_sr; + int64_t second_size; +}; + +#define check(got, exp) do_check (#got, got, exp) + +static void +do_check (const char *act, int64_t got, int64_t exp) +{ + fprintf (stderr, "trying %s\n", act); + if (got == -1) + fprintf (stderr, "%s\n", nbd_get_error ()); + else + fprintf (stderr, "succeeded, result %" PRId64 "\n", got); + if (got != exp) { + fprintf (stderr, "got %" PRId64 ", but expected %" PRId64 "\n", got, exp); + exit (EXIT_FAILURE); + } +} + +static void +do_test (const char *server_tls, struct expected exp) +{ + struct nbd_handle *nbd = nbd_create (); + + if (nbd == NULL) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } + + if (nbd_supports_tls (nbd) != 1) { + fprintf (stderr, "SKIP: missing TLS support in libnbd\n"); + exit (77); + } + + if (nbd_set_opt_mode (nbd, true) == -1 || + nbd_set_request_structured_replies (nbd, false) == -1 || + nbd_set_tls_username (nbd, "alice") == -1 || + nbd_set_tls_psk_file (nbd, "keys.psk") == -1) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } + + /* Run nbdkit as a subprocess. */ + const char *args[] = { "nbdkit", "-sv", "--exit-with-parent", server_tls, + "--tls-verify-peer", "--tls-psk=keys.psk", + "--filter=tls-fallback", "pattern", + "size=1M", "tlsreadme=fallback", NULL }; + + if (nbd_connect_command (nbd, (char **) args) == -1) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } + + check (nbd_opt_structured_reply (nbd), exp.first_opt_sr); + check (nbd_opt_info (nbd), exp.first_size > 0 ? 0 : -1); + check (nbd_get_size (nbd), exp.first_size); + check (nbd_opt_starttls (nbd), exp.first_opt_tls); + check (nbd_get_structured_replies_negotiated (nbd), exp.first_get_sr); + check (nbd_opt_structured_reply (nbd), exp.second_opt_sr); + check (nbd_opt_starttls (nbd), exp.second_opt_tls); + check (nbd_get_tls_negotiated (nbd), exp.get_tls); + check (nbd_get_structured_replies_negotiated (nbd), exp.second_get_sr); + check (nbd_opt_info (nbd), 0); + check (nbd_get_size (nbd), exp.second_size); + check (nbd_opt_abort (nbd), 0); + + nbd_close (nbd); +} + +int +main (int argc, char *argv[]) +{ + /* Check --tls-verify-peer option is supported. */ + requires ("nbdkit --tls-verify-peer -U - null --run 'exit 0'"); + /* Check for nbdkit tls-fallback filter. */ + requires ("nbdkit --filter=tls-fallback null --dump-plugin"); + + /* Reject older nbdkit that chokes on NBD_OPT_INFO to --tls=require */ + requires ("test 0 = \"$(nbdkit --tls=require --tls-psk=keys.psk -U - null " + "--run 'nbdinfo \"nbd+unix://?socket=$unixsocket\"' 2>&1 |" + "grep -c 'nbdkit.*unknown option version')\""); + + /* Behavior of a server with no TLS support */ + do_test ("--tls=no", (struct expected) { + .first_opt_sr = 1, /* Structured reply succeeds */ + .first_size = 512, /* Sees the tls-fallback safe size */ + .first_opt_tls = 0, /* Server lacks TLS, but connection stays up */ + .first_get_sr = 1, /* Structured reply still good */ + .second_opt_sr = 0, /* Server rejects second SR as redundant */ + .second_opt_tls = 0, /* Server still lacks TLS */ + .get_tls = 0, /* Final state of TLS - not secure */ + .second_get_sr = 1, /* Structured reply still good */ + .second_size = 512, /* Still the tls-fallback safe size */ + }); + + /* Behavior of a server with opportunistic TLS support */ + do_test ("--tls=on", (struct expected) { + .first_opt_sr = 1, /* Structured reply succeeds */ + .first_size = 512, /* Sees the tls-fallback safe size */ + .first_opt_tls = 1, /* Server takes TLS */ + .first_get_sr = 0, /* Structured reply wiped by TLS */ + .second_opt_sr = 1, /* Server accepts second SR */ + .second_opt_tls = 0, /* Server rejects second TLS as redundant */ + .get_tls = 1, /* Final state of TLS - secure */ + .second_get_sr = 1, /* Structured reply still good */ + .second_size = 1024*1024, /* Sees the actual size */ + }); + + /* Behavior of a server that requires TLS support */ + do_test ("--tls=require", (struct expected) { + .first_opt_sr = 0, /* Structured reply fails without TLS first */ + .first_size = -1, /* Cannot request info */ + .first_opt_tls = 1, /* Server takes TLS */ + .first_get_sr = 0, /* Structured reply hasn't been requested */ + .second_opt_sr = 1, /* Server accepts second SR */ + .second_opt_tls = 0, /* Server rejects second TLS as redundant */ + .get_tls = 1, /* Final state of TLS - secure */ + .second_get_sr = 1, /* Structured reply still good */ + .second_size = 1024*1024, /* Sees the actual size */ + }); + + exit (EXIT_SUCCESS); +} diff --git a/.gitignore b/.gitignore index 37166b73..fe929d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -236,6 +236,7 @@ Makefile.in /tests/opt-list-meta-queries /tests/opt-set-meta /tests/opt-set-meta-queries +/tests/opt-starttls /tests/opt-structured-twice /tests/pki/ /tests/pread-initialize -- 2.37.3 _______________________________________________ Libguestfs mailing list Libguestfs@redhat.com https://listman.redhat.com/mailman/listinfo/libguestfs