]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
wip: child-create: Add support for KE negotiation during IKE_AUTH
authorTobias Brunner <tobias@strongswan.org>
Wed, 22 Oct 2025 15:29:07 +0000 (17:29 +0200)
committerTobias Brunner <tobias@strongswan.org>
Mon, 27 Oct 2025 10:29:53 +0000 (11:29 +0100)
We prevent handling any KE payloads, just in case a peer incorrectly
sends us one.

wip: it might be a bit confusing that a proposal with KE methods is logged
and shown in the status output while the key is not actually based on them.
maybe documenting this thoroughly is enough. but perhaps we need a flag
to mark the sa (like we did for the initial simplified rekey approach)
so we could at least change the status display...(question would be how
that's conveyed e.g. via vici and what swanctl would do with it)

src/libcharon/sa/ikev2/tasks/child_create.c
src/libcharon/tests/suites/test_child_create.c
src/libcharon/tests/suites/test_child_rekey.c

index 37575f57fd1cb23f4e9636d0353f83b016440c48..0729b4477026d105a35655a079d38903f6d3f15e 100644 (file)
@@ -187,6 +187,18 @@ struct private_child_create_t {
         */
        bool ke_failed;
 
+       /**
+        * How to handle key exchange methods
+        */
+       enum {
+               /** Ignore KE methods and don't derive a key */
+               KE_SKIP = 0,
+               /** Negotiate KE methods during the exchange */
+               KE_NEGOTIATE_METHODS = 1,
+               /** Establish a key if KE methods are selected */
+               KE_ESTABLISH_KEY = 2,
+       } ke_handling;
+
        /**
         * Link value for current key exchange
         */
@@ -888,7 +900,7 @@ static status_t install_child_sa(private_child_create_t *this)
 /**
  * Select a proposal
  */
-static bool select_proposal(private_child_create_t *this, bool no_ke)
+static bool select_proposal(private_child_create_t *this)
 {
        proposal_selection_flag_t flags = 0;
 
@@ -898,7 +910,7 @@ static bool select_proposal(private_child_create_t *this, bool no_ke)
                return FALSE;
        }
 
-       if (no_ke)
+       if (!(this->ke_handling & KE_NEGOTIATE_METHODS))
        {
                flags |= PROPOSAL_SKIP_KE;
        }
@@ -1245,8 +1257,9 @@ static void determine_key_exchanges(private_child_create_t *this)
        uint16_t alg;
        int i = 1;
 
-       if (!this->proposal->get_algorithm(this->proposal, t, &alg, NULL))
-       {       /* no PFS */
+       if (!(this->ke_handling & KE_ESTABLISH_KEY) ||
+               !this->proposal->get_algorithm(this->proposal, t, &alg, NULL))
+       {       /* no key establishment or no PFS */
                return;
        }
 
@@ -1308,6 +1321,13 @@ static void process_ke_payload(private_child_create_t *this, ke_payload_t *ke)
        key_exchange_method_t method = this->key_exchanges[this->ke_index].method;
        key_exchange_method_t received = ke->get_key_exchange_method(ke);
 
+       if (!(this->ke_handling & KE_ESTABLISH_KEY))
+       {
+               DBG1(DBG_IKE, "ignore unexpected KE payload with method %N",
+                        key_exchange_method_names, received);
+               return;
+       }
+
        /* the proposal is selected after processing the KE payload, so this is
         * only relevant for additional key exchanges */
        if (method && method != received)
@@ -1361,6 +1381,11 @@ static bool check_ke_method(private_child_create_t *this, uint16_t *req)
 {
        uint16_t alg;
 
+       if (!(this->ke_handling & KE_ESTABLISH_KEY))
+       {
+               return TRUE;
+       }
+
        if (!this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD,
                                                                           this->ke_method))
        {
@@ -1711,20 +1736,10 @@ static void prepare_proposed_ts(private_child_create_t *this)
 METHOD(task_t, build_i, status_t,
        private_child_create_t *this, message_t *message)
 {
-       bool no_ke = TRUE;
-
        switch (message->get_exchange_type(message))
        {
                case IKE_SA_INIT:
                        return get_nonce(message, &this->my_nonce);
-               case CREATE_CHILD_SA:
-                       if (!generate_nonce(this))
-                       {
-                               message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED);
-                               return SUCCESS;
-                       }
-                       no_ke = FALSE;
-                       break;
                case IKE_AUTH:
                        switch (defer_child_sa(this))
                        {
@@ -1739,9 +1754,24 @@ METHOD(task_t, build_i, status_t,
                                        /* just continue to establish the CHILD_SA */
                                        break;
                        }
+                       /* negotiate KE methods if supported but don't establish a key */
+                       if (this->ike_sa->supports_extension(this->ike_sa,
+                                                                                                EXT_CHILD_SA_PFS_INFO))
+                       {
+                               this->ke_handling = KE_NEGOTIATE_METHODS;
+                       }
                        /* send only in the first request, not in subsequent rounds */
                        this->public.task.build = (void*)return_need_more;
                        break;
+               case CREATE_CHILD_SA:
+                       if (!generate_nonce(this))
+                       {
+                               message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED);
+                               return SUCCESS;
+                       }
+                       /* establish a key if KE methods are selected */
+                       this->ke_handling = KE_NEGOTIATE_METHODS | KE_ESTABLISH_KEY;
+                       break;
                default:
                        return NEED_MORE;
        }
@@ -1768,7 +1798,8 @@ METHOD(task_t, build_i, status_t,
                                                                                                           OPT_PER_CPU_SAS);
        }
 
-       this->proposals = this->config->get_proposals(this->config, no_ke);
+       this->proposals = this->config->get_proposals(this->config,
+                                                               !(this->ke_handling & KE_NEGOTIATE_METHODS));
        this->mode = this->config->get_mode(this->config);
 
        this->child.if_id_in_def = this->ike_sa->get_if_id(this->ike_sa, TRUE);
@@ -1806,8 +1837,8 @@ METHOD(task_t, build_i, status_t,
                return FAILED;
        }
 
-       if (no_ke)
-       {       /* we might have one set if we are recreating this SA */
+       if (!(this->ke_handling & KE_NEGOTIATE_METHODS))
+       {       /* make sure we don't have one set if we are recreating this SA */
                this->ke_method = KE_NONE;
        }
        else if (!this->retry)
@@ -1826,7 +1857,7 @@ METHOD(task_t, build_i, status_t,
                return FAILED;
        }
 
-       if (this->ke_method != KE_NONE)
+       if (this->ke_handling & KE_ESTABLISH_KEY && this->ke_method != KE_NONE)
        {
                this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat,
                                                                                                  this->ke_method);
@@ -1940,8 +1971,16 @@ METHOD(task_t, process_r, status_t,
                        return get_nonce(message, &this->other_nonce);
                case CREATE_CHILD_SA:
                        get_nonce(message, &this->other_nonce);
+                       /* establish a key if KE methods are selected */
+                       this->ke_handling = KE_NEGOTIATE_METHODS | KE_ESTABLISH_KEY;
                        break;
                case IKE_AUTH:
+                       /* negotiate KE methods if supported but don't establish a key */
+                       if (this->ike_sa->supports_extension(this->ike_sa,
+                                                                                                EXT_CHILD_SA_PFS_INFO))
+                       {
+                               this->ke_handling = KE_NEGOTIATE_METHODS;
+                       }
                        /* only handle first AUTH payload, not additional rounds */
                        this->public.task.process = (void*)return_need_more;
                        break;
@@ -2198,7 +2237,7 @@ static bool key_exchange_done(private_child_create_t *this)
  * as responder.
  */
 static bool key_exchange_done_and_install_r(private_child_create_t *this,
-                                                                                       message_t *message, bool ike_auth)
+                                                                                       message_t *message)
 {
        bool all_done = FALSE;
 
@@ -2268,7 +2307,7 @@ METHOD(task_t, build_r_multi_ke, status_t,
                handle_child_sa_failure(this, message);
                return SUCCESS;
        }
-       if (!key_exchange_done_and_install_r(this, message, FALSE))
+       if (!key_exchange_done_and_install_r(this, message))
        {
                return NEED_MORE;
        }
@@ -2280,7 +2319,7 @@ METHOD(task_t, build_r, status_t,
 {
        payload_t *payload;
        enumerator_t *enumerator;
-       bool no_ke = TRUE, ike_auth = FALSE;
+       bool ike_auth = FALSE;
 
        switch (message->get_exchange_type(message))
        {
@@ -2293,7 +2332,6 @@ METHOD(task_t, build_r, status_t,
                                                                        chunk_empty);
                                return SUCCESS;
                        }
-                       no_ke = FALSE;
                        break;
                case IKE_AUTH:
                        if (!this->ike_sa->has_condition(this->ike_sa, COND_AUTHENTICATED))
@@ -2384,7 +2422,7 @@ METHOD(task_t, build_r, status_t,
        }
        enumerator->destroy(enumerator);
 
-       if (!select_proposal(this, no_ke))
+       if (!select_proposal(this))
        {
                message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty);
                handle_child_sa_failure(this, message);
@@ -2467,7 +2505,7 @@ METHOD(task_t, build_r, status_t,
                        return SUCCESS;
        }
 
-       if (!key_exchange_done_and_install_r(this, message, ike_auth))
+       if (!key_exchange_done_and_install_r(this, message))
        {
                this->public.task.build = _build_r_multi_ke;
                this->public.task.process = _process_r_multi_ke;
@@ -2539,7 +2577,7 @@ static status_t delete_failed_sa(private_child_create_t *this)
  * as initiator.
  */
 static status_t key_exchange_done_and_install_i(private_child_create_t *this,
-                                                                                               message_t *message, bool ike_auth)
+                                                                                               message_t *message)
 {
        if (key_exchange_done(this))
        {
@@ -2580,7 +2618,7 @@ METHOD(task_t, process_i_multi_ke, status_t,
                return delete_failed_sa(this);
        }
 
-       return key_exchange_done_and_install_i(this, message, FALSE);
+       return key_exchange_done_and_install_i(this, message);
 }
 
 METHOD(task_t, process_i, status_t,
@@ -2588,7 +2626,7 @@ METHOD(task_t, process_i, status_t,
 {
        enumerator_t *enumerator;
        payload_t *payload;
-       bool no_ke = TRUE, ike_auth = FALSE;
+       bool ike_auth = FALSE;
 
        switch (message->get_exchange_type(message))
        {
@@ -2596,7 +2634,6 @@ METHOD(task_t, process_i, status_t,
                        return get_nonce(message, &this->other_nonce);
                case CREATE_CHILD_SA:
                        get_nonce(message, &this->other_nonce);
-                       no_ke = FALSE;
                        break;
                case IKE_AUTH:
                        if (!this->ike_sa->has_condition(this->ike_sa, COND_AUTHENTICATED))
@@ -2657,7 +2694,20 @@ METHOD(task_t, process_i, status_t,
                                        chunk_t data;
                                        uint16_t alg = KE_NONE;
 
-                                       if (this->aborted)
+                                       data = notify->get_notification_data(notify);
+                                       if (data.len == sizeof(alg))
+                                       {
+                                               alg = untoh16(data.ptr);
+                                       }
+                                       if (ike_auth)
+                                       {       /* make sure we ignore this notify during IKE_AUTH */
+                                               DBG1(DBG_IKE, "ignore %N notify with key exchange "
+                                                        "method %N during IKE_AUTH",
+                                                        key_exchange_method_names, alg,
+                                                        notify_type_names, type);
+                                               break;
+                                       }
+                                       else if (this->aborted)
                                        {       /* nothing to do if the task was aborted */
                                                DBG1(DBG_IKE, "received %N notify in aborted %N task",
                                                         notify_type_names, type, task_type_names,
@@ -2665,12 +2715,7 @@ METHOD(task_t, process_i, status_t,
                                                enumerator->destroy(enumerator);
                                                return SUCCESS;
                                        }
-                                       data = notify->get_notification_data(notify);
-                                       if (data.len == sizeof(alg))
-                                       {
-                                               alg = untoh16(data.ptr);
-                                       }
-                                       if (this->retry)
+                                       else if (this->retry)
                                        {
                                                DBG1(DBG_IKE, "already retried with key exchange method "
                                                         "%N, ignore requested %N", key_exchange_method_names,
@@ -2712,7 +2757,7 @@ METHOD(task_t, process_i, status_t,
 
        process_payloads(this, message);
 
-       if (!select_proposal(this, no_ke))
+       if (!select_proposal(this))
        {
                handle_child_sa_failure(this, message);
                return delete_failed_sa(this);
@@ -2786,7 +2831,7 @@ METHOD(task_t, process_i, status_t,
                this->child_sa->set_per_cpu(this->child_sa, this->child.per_cpu);
        }
 
-       if (key_exchange_done_and_install_i(this, message, ike_auth) == NEED_MORE)
+       if (key_exchange_done_and_install_i(this, message) == NEED_MORE)
        {
                /* if the installation failed, we delete the failed SA, i.e. build() was
                 * changed, otherwise, we switch to multi-KE mode */
@@ -2989,6 +3034,7 @@ METHOD(task_t, migrate, void,
        DESTROY_IF(this->nonceg);
        DESTROY_IF(this->ke);
        this->ke_failed = FALSE;
+       this->ke_handling = KE_SKIP;
        clear_key_exchanges(this);
        DESTROY_OFFSET_IF(this->proposals, offsetof(proposal_t, destroy));
        if (!this->rekey && !this->retry)
index bc855f76d6c578067cddaf8a4df1dbef613e2443..4a1d9415af4a279466928ae067f7c29c5c56816e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016-2020 Tobias Brunner
+ * Copyright (C) 2016-2025 Tobias Brunner
  *
  * Copyright (C) secunet Security Networks AG
  *
 #include <tests/utils/job_asserts.h>
 #include <tests/utils/sa_asserts.h>
 
+struct {
+       char *init;
+       char *resp;
+       char *enabled;
+       char *disabled;
+} ike_auth_ke[] = {
+       { "aes128-sha256", "aes128-sha256", "aes128-sha256", "aes128-sha256" },
+       { "aes128-sha256-curve25519", "aes128-sha256-curve25519",
+               "aes128-sha256-curve25519", "aes128-sha256" },
+       { "aes128-sha256-curve25519-none", "aes128-sha256",
+               "aes128-sha256", "aes128-sha256" },
+       { "aes128-sha256", "aes128-sha256-curve25519-none", "aes128-sha256",
+               "aes128-sha256" },
+       { "aes128-sha256-curve25519", "aes128-sha256", NULL, "aes128-sha256"  },
+       { "aes128-sha256", "aes128-sha256-curve25519", NULL, "aes128-sha256" },
+};
+
+/**
+ * KE method negotiation during IKE_AUTH, which results in a selected KE method
+ * or a mismatch.
+ */
+START_TEST(test_ike_auth_ke_enabled)
+{
+       exchange_test_sa_conf_t conf = {
+               .initiator = {
+                       .esp = ike_auth_ke[_i].init,
+               },
+               .responder = {
+                       .esp = ike_auth_ke[_i].resp,
+               },
+       };
+       ike_sa_t *a, *b;
+       ike_sa_id_t *id_a, *id_b;
+       child_cfg_t *child_cfg;
+       child_sa_t *child_sa;
+       proposal_t *selected;
+
+       child_cfg = exchange_test_helper->create_sa(exchange_test_helper, &a, &b,
+                                                                                               &conf);
+       id_a = a->get_id(a);
+       id_b = b->get_id(b);
+
+       call_ikesa(a, initiate, child_cfg, NULL);
+
+       /* IKE_SA_INIT --> */
+       assert_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+       id_b->set_initiator_spi(id_b, id_a->get_initiator_spi(id_a));
+       exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+       /* <-- IKE_SA_INIT */
+       assert_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+       id_a->set_responder_spi(id_a, id_b->get_responder_spi(id_b));
+       exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+
+       if (!ike_auth_ke[_i].enabled)
+       {
+               /* IKE_AUTH --> */
+               assert_hook_not_called(child_updown);
+               assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+               exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+
+               /* <-- IKE_AUTH */
+               assert_notify(IN, NO_PROPOSAL_CHOSEN);
+               exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+               assert_hook();
+       }
+       else
+       {
+               /* IKE_AUTH --> */
+               assert_hook_called(child_updown);
+               assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+               exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+               assert_child_sa_count(b, 1);
+               assert_hook();
+
+               /* <-- IKE_AUTH */
+               assert_hook_called(child_updown);
+               assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+               exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+               assert_child_sa_count(a, 1);
+               assert_hook();
+
+               child_sa = a->get_child_sa(a, PROTO_ESP, 1, TRUE);
+               selected = proposal_create_from_string(PROTO_ESP, ike_auth_ke[_i].enabled);
+               ck_assert(selected->equals(selected, child_sa->get_proposal(child_sa)));
+               selected->destroy(selected);
+       }
+
+       assert_sa_idle(a);
+       assert_sa_idle(b);
+
+       call_ikesa(a, destroy);
+       call_ikesa(b, destroy);
+}
+END_TEST
+
+/**
+ * With KE method negotiation during IKE_AUTH disabled, we don't get any KE
+ * methods or mismatches (until the SA is later rekeyed).
+ */
+START_TEST(test_ike_auth_ke_disabled)
+{
+       bool disable_init = _i > countof(ike_auth_ke);
+       _i = _i % countof(ike_auth_ke);
+       exchange_test_sa_conf_t conf = {
+               .initiator = {
+                       .esp = ike_auth_ke[_i].init,
+               },
+               .responder = {
+                       .esp = ike_auth_ke[_i].resp,
+               },
+       };
+       ike_sa_t *a, *b;
+       ike_sa_id_t *id_a, *id_b;
+       child_cfg_t *child_cfg;
+       child_sa_t *child_sa;
+       proposal_t *selected;
+
+       child_cfg = exchange_test_helper->create_sa(exchange_test_helper, &a, &b,
+                                                                                               &conf);
+       id_a = a->get_id(a);
+       id_b = b->get_id(b);
+
+       if (disable_init)
+       {
+               lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                               FALSE, lib->ns);
+       }
+
+       call_ikesa(a, initiate, child_cfg, NULL);
+
+       /* IKE_SA_INIT --> */
+       if (disable_init)
+       {
+               assert_no_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+       }
+       else
+       {
+               lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                               FALSE, lib->ns);
+       }
+       id_b->set_initiator_spi(id_b, id_a->get_initiator_spi(id_a));
+       exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+
+       /* <-- IKE_SA_INIT */
+       assert_no_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+       id_a->set_responder_spi(id_a, id_b->get_responder_spi(id_b));
+       exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+
+       /* IKE_AUTH --> */
+       assert_hook_called(child_updown);
+       assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+       exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+       assert_child_sa_count(b, 1);
+       assert_hook();
+
+       /* <-- IKE_AUTH */
+       assert_hook_called(child_updown);
+       assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+       exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+       assert_child_sa_count(a, 1);
+       assert_hook();
+
+       child_sa = a->get_child_sa(a, PROTO_ESP, 1, TRUE);
+       selected = proposal_create_from_string(PROTO_ESP, ike_auth_ke[_i].disabled);
+       ck_assert(selected->equals(selected, child_sa->get_proposal(child_sa)));
+       selected->destroy(selected);
+
+       assert_sa_idle(a);
+       assert_sa_idle(b);
+
+       call_ikesa(a, destroy);
+       call_ikesa(b, destroy);
+}
+END_TEST
+
 /**
  * The peers try to create a new CHILD_SA that looks exactly the same
  * as the existing one, so it won't get initiated.
@@ -209,6 +384,11 @@ Suite *child_create_suite_create()
 
        s = suite_create("child create");
 
+       tc = tcase_create("ike_auth ke");
+       tcase_add_loop_test(tc, test_ike_auth_ke_enabled, 0, countof(ike_auth_ke));
+       tcase_add_loop_test(tc, test_ike_auth_ke_disabled, 0, 2 * countof(ike_auth_ke));
+       suite_add_tcase(s, tc);
+
        tc = tcase_create("initiate duplicate");
        tcase_add_test(tc, test_duplicate);
        suite_add_tcase(s, tc);
index b61f31c7cb179d8475ada76d2261821281bb3336..f1a1943fcb8de7270155031a37117ee52b59bcd6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016-2022 Tobias Brunner
+ * Copyright (C) 2016-2025 Tobias Brunner
  *
  * Copyright (C) secunet Security Networks AG
  *
@@ -251,6 +251,126 @@ START_TEST(test_regular_multi_ke)
 }
 END_TEST
 
+
+/**
+ * CHILD_SA rekey where the responder does not agree with the DH group selected
+ * by the initiator.  However, because they negotiate the KE methods during
+ * IKE_AUTH, the rekeying initiated by the original initiator can be completed
+ * without INVALID_KE_PAYLOAD notify.  If it's initiated by the original
+ * responder that's currently not the case as the responder strictly prefers
+ * its own configuration.
+ */
+START_TEST(test_regular_ke_mismatch)
+{
+       exchange_test_sa_conf_t conf = {
+               .initiator = {
+                       .esp = "aes128-sha256-modp2048-modp3072",
+               },
+               .responder = {
+                       .esp = "aes128-sha256-modp3072-modp2048",
+               },
+       };
+       ike_sa_t *a, *b;
+       uint32_t spi_a = _i+1, spi_b = 2-_i, spi_i = 3, spi_r = 4;
+
+       assert_track_sas_start();
+
+       if (_i)
+       {       /* responder rekeys the CHILD_SA (SPI 2) */
+               assert_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+               exchange_test_helper->establish_sa(exchange_test_helper,
+                                                                                  &b, &a, &conf);
+       }
+       else
+       {       /* initiator rekeys the CHILD_SA (SPI 1) */
+               assert_notify(IN, CHILD_SA_PFS_INFO_SUPPORTED);
+               exchange_test_helper->establish_sa(exchange_test_helper,
+                                                                                  &a, &b, &conf);
+       }
+       initiate_rekey(a, spi_a);
+       assert_ipsec_sas_installed(a, spi_a, spi_b);
+
+       /* this should never get called as this results in a successful rekeying */
+       assert_hook_not_called(child_updown);
+
+       if (_i)
+       {
+               /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */
+               assert_hook_not_called(child_rekey);
+               assert_notify(IN, REKEY_SA);
+               exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+               assert_child_sa_state(b, spi_b, CHILD_INSTALLED);
+               assert_child_sa_count(b, 1);
+               assert_ipsec_sas_installed(b, spi_a, spi_b);
+               assert_hook();
+
+               /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */
+               assert_hook_not_called(child_rekey);
+               assert_single_notify(IN, INVALID_KE_PAYLOAD);
+               exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+               assert_child_sa_state(a, spi_a, CHILD_REKEYING);
+               assert_child_sa_count(a, 1);
+               assert_ipsec_sas_installed(a, spi_a, spi_b);
+               assert_hook();
+
+               spi_i = spi_r++;
+       }
+
+       /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */
+       assert_hook_not_called(child_rekey);
+       assert_notify(IN, REKEY_SA);
+       assert_no_notify(OUT, INVALID_KE_PAYLOAD);
+       exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+       assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED);
+       assert_child_sa_state(b, spi_r, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
+       assert_ipsec_sas_installed(b, spi_a, spi_b, spi_r);
+       assert_hook();
+
+       /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */
+       assert_hook_rekey(child_rekey, spi_a, spi_i);
+       assert_no_notify(IN, REKEY_SA);
+       exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+       assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_NONE);
+       assert_child_sa_state(a, spi_i, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(a, spi_a, spi_i, spi_r);
+       assert_hook();
+
+       /* INFORMATIONAL { D } --> */
+       assert_hook_rekey(child_rekey, spi_b, spi_r);
+       assert_single_payload(IN, PLV2_DELETE);
+       exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+       assert_child_sa_state(b, spi_b, CHILD_DELETED, CHILD_OUTBOUND_NONE);
+       assert_child_sa_state(b, spi_r, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+       assert_child_sa_count(b, 2);
+       assert_ipsec_sas_installed(b, spi_b, spi_i, spi_r);
+       assert_hook();
+       /* <-- INFORMATIONAL { D } */
+       assert_hook_not_called(child_rekey);
+       assert_single_payload(IN, PLV2_DELETE);
+       exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+       assert_child_sa_state(a, spi_a, CHILD_DELETED, CHILD_OUTBOUND_NONE);
+       assert_child_sa_state(a, spi_i, CHILD_INSTALLED);
+       assert_child_sa_count(a, 2);
+       assert_ipsec_sas_installed(a, spi_a, spi_i, spi_r);
+       assert_hook();
+
+       /* simulate the execution of the scheduled jobs */
+       destroy_rekeyed(a, spi_a);
+       assert_child_sa_count(a, 1);
+       assert_ipsec_sas_installed(a, spi_i, spi_r);
+       destroy_rekeyed(b, spi_b);
+       assert_child_sa_count(b, 1);
+       assert_ipsec_sas_installed(b, spi_i, spi_r);
+
+       /* child_updown */
+       assert_hook();
+       assert_track_sas(2, 2);
+
+       call_ikesa(a, destroy);
+       call_ikesa(b, destroy);
+}
+END_TEST
+
 /**
  * CHILD_SA rekey where the responder does not agree with the DH group selected
  * by the initiator, either initiated by the original initiator or responder of
@@ -269,6 +389,10 @@ START_TEST(test_regular_ke_invalid)
        ike_sa_t *a, *b;
        uint32_t spi_a = _i+1, spi_b = 2-_i;
 
+       /* disable KE negotiation during IKE_AUTH, which prevents this mismatch */
+       lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                       FALSE, lib->ns);
+
        assert_track_sas_start();
 
        if (_i)
@@ -432,6 +556,10 @@ START_TEST(test_regular_ke_invalid_multi_ke)
        ike_sa_t *a, *b;
        uint32_t spi_a = _i+1, spi_b = 2-_i;
 
+       /* disable KE negotiation during IKE_AUTH, which prevents this mismatch */
+       lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                       FALSE, lib->ns);
+
        assert_track_sas_start();
 
        if (_i)
@@ -3144,7 +3272,7 @@ START_TEST(test_collision_delayed_request_multi_ke)
 END_TEST
 
 /**
- * Both peers initiate the CHILD_SA reekying concurrently but the proposed DH
+ * Both peers initiate the CHILD_SA rekeying concurrently but the proposed DH
  * groups are not the same after handling the INVALID_KE_PAYLOAD they should
  * still handle the collision properly depending on the nonces.
  */
@@ -3160,6 +3288,10 @@ START_TEST(test_collision_ke_invalid)
        };
        ike_sa_t *a, *b;
 
+       /* disable KE negotiation during IKE_AUTH, which prevents this mismatch */
+       lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                       FALSE, lib->ns);
+
        assert_track_sas_start();
 
        exchange_test_helper->establish_sa(exchange_test_helper,
@@ -3400,6 +3532,10 @@ START_TEST(test_collision_ke_invalid_delayed_retry)
        ike_sa_t *a, *b;
        message_t *msg;
 
+       /* disable KE negotiation during IKE_AUTH, which prevents this mismatch */
+       lib->settings->set_bool(lib->settings, "%s.child_sa_pfs_info",
+                                                       FALSE, lib->ns);
+
        assert_track_sas_start();
 
        exchange_test_helper->establish_sa(exchange_test_helper,
@@ -4361,6 +4497,7 @@ Suite *child_rekey_suite_create()
        tc = tcase_create("regular");
        tcase_add_loop_test(tc, test_regular, 0, 2);
        tcase_add_loop_test(tc, test_regular_multi_ke, 0, 2);
+       tcase_add_loop_test(tc, test_regular_ke_mismatch, 0, 2);
        tcase_add_loop_test(tc, test_regular_ke_invalid, 0, 2);
        tcase_add_loop_test(tc, test_regular_ke_invalid_multi_ke, 0, 2);
        tcase_add_test(tc, test_regular_responder_ignore_soft_expire);