From: Tobias Brunner Date: Fri, 17 Mar 2023 15:40:48 +0000 (+0100) Subject: wip: ike-init: Add support for optimized rekeying X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=063ac85c7c531b5ee8989d313b515d8a7602f330;p=thirdparty%2Fstrongswan.git wip: ike-init: Add support for optimized rekeying wip: should we handle the case of a responder returning an SA payload instead of an OPTIMIZED_REKEY notify as error (or just accept it if we get a valid proposal from it)? --- diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index d795b6cde9..e1f88aabc2 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2020 Tobias Brunner + * Copyright (C) 2008-2023 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -143,6 +143,11 @@ struct private_ike_init_t { * Whether to follow IKEv2 redirects as per RFC 5685 */ bool follow_redirects; + + /** + * Whether to use optimized rekeying + */ + bool optimized_rekeying; }; /** @@ -336,24 +341,22 @@ static bool send_use_ppk(private_ike_init_t *this) } /** - * build the payloads for the message + * Add an SA payload to the message, either generated from configured proposals + * or returning the selected proposal. + * + * The optional SPI of the new SA is encoded during a rekeying. + * + * Returns TRUE if additional KEs are proposed. */ -static bool build_payloads(private_ike_init_t *this, message_t *message) +static bool build_sa_payload(private_ike_init_t *this, ike_cfg_t *ike_cfg, + uint64_t new_spi, message_t *message) { sa_payload_t *sa_payload; - ke_payload_t *ke_payload; - nonce_payload_t *nonce_payload; linked_list_t *proposal_list, *other_dh_groups; - ike_sa_id_t *id; - proposal_t *proposal; enumerator_t *enumerator; - ike_cfg_t *ike_cfg; + proposal_t *proposal; bool additional_ke = FALSE; - id = this->ike_sa->get_id(this->ike_sa); - - ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa); - if (this->initiator) { proposal_list = ike_cfg->get_proposals(ike_cfg); @@ -361,11 +364,7 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) enumerator = proposal_list->create_enumerator(proposal_list); while (enumerator->enumerate(enumerator, (void**)&proposal)) { - /* include SPI of new IKE_SA when we are rekeying */ - if (this->old_sa) - { - proposal->set_spi(proposal, id->get_initiator_spi(id)); - } + proposal->set_spi(proposal, new_spi); /* move the selected DH group to the front of the proposal */ if (!proposal->promote_transform(proposal, KEY_EXCHANGE_METHOD, this->ke_method)) @@ -391,15 +390,47 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) } else { - if (this->old_sa) - { - /* include SPI of new IKE_SA when we are rekeying */ - this->proposal->set_spi(this->proposal, id->get_responder_spi(id)); - } + this->proposal->set_spi(this->proposal, new_spi); sa_payload = sa_payload_create_from_proposal_v2(this->proposal); additional_ke = proposal_has_additional_ke(this->proposal); } message->add_payload(message, (payload_t*)sa_payload); + return additional_ke; +} + +/** + * build the payloads for the message + */ +static bool build_payloads(private_ike_init_t *this, message_t *message) +{ + ke_payload_t *ke_payload; + notify_payload_t *notify; + nonce_payload_t *nonce_payload; + ike_sa_id_t *id; + ike_cfg_t *ike_cfg; + uint64_t my_new_spi = 0; + bool additional_ke = FALSE; + + id = this->ike_sa->get_id(this->ike_sa); + ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa); + + if (this->old_sa) + { + my_new_spi = this->initiator ? id->get_initiator_spi(id) + : id->get_responder_spi(id); + } + + if (this->optimized_rekeying) + { + notify = notify_payload_create_from_protocol_and_type(PLV2_NOTIFY, + PROTO_IKE, OPTIMIZED_REKEY); + notify->set_ike_spi(notify, my_new_spi); + message->add_payload(message, (payload_t*)notify); + } + else + { + additional_ke = build_sa_payload(this, ike_cfg, my_new_spi, message); + } ke_payload = ke_payload_create_from_key_exchange(PLV2_KEY_EXCHANGE, this->ke); @@ -661,6 +692,7 @@ static void process_payloads(private_ike_init_t *this, message_t *message) payload_t *payload; ike_sa_id_t *id; ke_payload_t *ke_pld = NULL; + uint64_t new_spi = 0; enumerator = message->create_payload_enumerator(message); while (enumerator->enumerate(enumerator, &payload)) @@ -692,6 +724,18 @@ static void process_payloads(private_ike_init_t *this, message_t *message) switch (notify->get_notify_type(notify)) { + case OPTIMIZED_REKEY: + if (this->optimized_rekeying) + { + new_spi = notify->get_ike_spi(notify); + if (!new_spi) + { + DBG1(DBG_IKE, "received invalid %N notify, " + "ignored", notify_type_names, + OPTIMIZED_REKEY); + } + } + break; case FRAGMENTATION_SUPPORTED: this->ike_sa->enable_extension(this->ike_sa, EXT_IKE_FRAGMENTATION); @@ -759,6 +803,38 @@ static void process_payloads(private_ike_init_t *this, message_t *message) } enumerator->destroy(enumerator); + if (this->optimized_rekeying) + { + if (new_spi) + { + /* when using optimized rekeying, we use the original proposal but + * with the new SPI the peer supplied via notify */ + if (this->proposal) + { + DBG1(DBG_IKE, "peer sent unexpected SA payload during " + "optimized rekeying, ignored"); + this->proposal->destroy(this->proposal); + } + this->proposal = this->old_sa->get_proposal(this->old_sa); + this->proposal = this->proposal->clone(this->proposal, 0); + this->proposal->set_spi(this->proposal, new_spi); + } + else + { + if (this->initiator) + { + DBG1(DBG_IKE, "peer didn't reply with expected %N notify," + "rekeying may fail", notify_type_names, OPTIMIZED_REKEY); + } + else + { + DBG2(DBG_IKE, "peer requested a regular rekeying, even though " + "optimized rekeying is supported"); + } + this->optimized_rekeying = FALSE; + } + } + if (this->proposal) { this->ike_sa->set_proposal(this->ike_sa, this->proposal); @@ -851,8 +927,15 @@ METHOD(task_t, build_i, status_t, if (!this->ke) { if (this->old_sa && - lib->settings->get_bool(lib->settings, - "%s.prefer_previous_dh_group", TRUE, lib->ns)) + this->old_sa->supports_extension(this->old_sa, + EXT_OPTIMIZED_REKEY)) + { + this->optimized_rekeying = TRUE; + } + if (this->old_sa && + (this->optimized_rekeying || + lib->settings->get_bool(lib->settings, "%s.prefer_previous_dh_group", + TRUE, lib->ns))) { /* reuse the DH group we used for the old IKE_SA when rekeying */ proposal_t *proposal; uint16_t dh_group; @@ -885,6 +968,12 @@ METHOD(task_t, build_i, status_t, } else if (this->ke->get_method(this->ke) != this->ke_method) { /* reset DH instance if group changed (INVALID_KE_PAYLOAD) */ + if (this->optimized_rekeying) + { + DBG1(DBG_IKE, "peer rejected our DH group during optimized " + "rekeying, switch to regular rekeying"); + this->optimized_rekeying = FALSE; + } this->ke->destroy(this->ke); this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, this->ke_method); @@ -981,6 +1070,12 @@ METHOD(task_t, process_r, status_t, } #endif /* ME */ + if (this->old_sa && + this->old_sa->supports_extension(this->old_sa, EXT_OPTIMIZED_REKEY)) + { /* we expect an optimized rekeying if both peers support it */ + this->optimized_rekeying = TRUE; + } + process_payloads(this, message); return NEED_MORE; diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index c6691acf44..bb5375b203 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -61,6 +61,9 @@ START_TEST(test_regular) /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ assert_hook_rekey(ike_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); + assert_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_ike_sa_state(b, IKE_REKEYED); assert_child_sa_count(b, 0); @@ -73,6 +76,9 @@ START_TEST(test_regular) /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ assert_hook_rekey(ike_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); + assert_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_ike_sa_state(a, IKE_DELETING); assert_child_sa_count(a, 0); @@ -148,6 +154,9 @@ START_TEST(test_regular_multi_ke) /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ assert_hook_not_called(ike_rekey); assert_no_notify(IN, REKEY_SA); + assert_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_ike_sa_state(b, IKE_REKEYING); assert_child_sa_count(b, 1); @@ -157,6 +166,9 @@ START_TEST(test_regular_multi_ke) /* <-- CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } */ assert_hook_not_called(ike_rekey); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + assert_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_ike_sa_state(a, IKE_REKEYING); assert_child_sa_count(a, 1); @@ -166,6 +178,8 @@ START_TEST(test_regular_multi_ke) /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ assert_hook_rekey(ike_rekey, 1, 3); assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_no_payload(IN, PLV2_NONCE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_ike_sa_state(b, IKE_REKEYED); @@ -179,6 +193,8 @@ START_TEST(test_regular_multi_ke) /* <-- IKE_FOLLOWUP_KE { KEr } */ assert_hook_rekey(ike_rekey, 1, 3); assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_no_payload(IN, PLV2_NONCE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_ike_sa_state(a, IKE_DELETING); @@ -440,6 +456,198 @@ START_TEST(test_regular_ke_invalid_multi_ke) } END_TEST +/** + * Optimized IKE_SA rekeying either initiated by the original initiator or + * responder of the IKE_SA. + */ +START_TEST(test_optimized) +{ + ike_sa_t *a, *b, *new_sa; + status_t s; + + assert_track_sas_start(); + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { N(OPT_REKEY), Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_notify(IN, OPTIMIZED_REKEY); + assert_no_notify(IN, REKEY_SA); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYED); + assert_child_sa_count(b, 0); + new_sa = assert_ike_sa_checkout(3, 4, FALSE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(OPT_REKEY), Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_notify(IN, OPTIMIZED_REKEY); + assert_no_notify(IN, REKEY_SA); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_DELETING); + assert_child_sa_count(a, 0); + new_sa = assert_ike_sa_checkout(3, 4, TRUE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + assert_track_sas(2, 2); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * Optimized IKE_SA rekeying with multiple key exchanges either initiated by the + * original initiator or responder of the IKE_SA. + */ +START_TEST(test_optimized_multi_ke) +{ + ike_sa_t *a, *b, *new_sa; + status_t s; + + assert_track_sas_start(); + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &multi_ke_conf); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + } + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { N(OPT_REKEY), Ni, KEi } --> */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, OPTIMIZED_REKEY); + assert_no_notify(IN, REKEY_SA); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(OPT_REKEY), Nr, KEr, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, OPTIMIZED_REKEY); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_payload(IN, PLV2_NONCE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_no_notify(IN, OPTIMIZED_REKEY); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_no_payload(IN, PLV2_NONCE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYED); + assert_child_sa_count(b, 0); + new_sa = assert_ike_sa_checkout(3, 4, FALSE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_no_notify(IN, OPTIMIZED_REKEY); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_payload(IN, PLV2_SECURITY_ASSOCIATION); + assert_no_payload(IN, PLV2_NONCE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_DELETING); + assert_child_sa_count(a, 0); + new_sa = assert_ike_sa_checkout(3, 4, TRUE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + assert_track_sas(2, 2); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + /** * Both peers initiate the IKE_SA rekeying concurrently and should handle the * collision properly depending on the nonces. @@ -2568,6 +2776,20 @@ START_TEST(test_collision_delete_drop_delete) } END_TEST +START_SETUP(disable_optimized_rekey) +{ + lib->settings->set_bool(lib->settings, "%s.optimized_rekeying", + FALSE, lib->ns); +} +END_SETUP + +START_TEARDOWN(enable_optimized_rekey) +{ + lib->settings->set_bool(lib->settings, "%s.optimized_rekeying", + TRUE, lib->ns); +} +END_TEARDOWN + Suite *ike_rekey_suite_create() { Suite *s; @@ -2576,13 +2798,20 @@ Suite *ike_rekey_suite_create() s = suite_create("ike rekey"); tc = tcase_create("regular"); + tcase_add_checked_fixture(tc, disable_optimized_rekey, enable_optimized_rekey); 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_invalid, 0, 2); tcase_add_loop_test(tc, test_regular_ke_invalid_multi_ke, 0, 2); suite_add_tcase(s, tc); + tc = tcase_create("optimized"); + tcase_add_loop_test(tc, test_optimized, 0, 2); + tcase_add_loop_test(tc, test_optimized_multi_ke, 0, 2); + suite_add_tcase(s, tc); + tc = tcase_create("collisions rekey"); + tcase_add_checked_fixture(tc, disable_optimized_rekey, enable_optimized_rekey); tcase_add_loop_test(tc, test_collision, 0, 4); tcase_add_loop_test(tc, test_collision_multi_ke, 0, 4); tcase_add_loop_test(tc, test_collision_mixed, 0, 4); @@ -2597,6 +2826,7 @@ Suite *ike_rekey_suite_create() suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); + tcase_add_checked_fixture(tc, disable_optimized_rekey, enable_optimized_rekey); tcase_add_loop_test(tc, test_collision_delete, 0, 2); tcase_add_loop_test(tc, test_collision_delete_multi_ke, 0, 2); tcase_add_loop_test(tc, test_collision_delete_drop_delete, 0, 2);