From 22550bd26203e38dae2498c4b42b8be70b2979c7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 25 Oct 2019 14:20:59 +0200 Subject: [PATCH 01/46] transform: Add additional key exchange transform types --- src/libstrongswan/crypto/transform.c | 20 +++++++++++++++++--- src/libstrongswan/crypto/transform.h | 8 ++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/libstrongswan/crypto/transform.c b/src/libstrongswan/crypto/transform.c index d7bc21540..32fd50df9 100644 --- a/src/libstrongswan/crypto/transform.c +++ b/src/libstrongswan/crypto/transform.c @@ -19,14 +19,21 @@ #include #include -ENUM_BEGIN(transform_type_names, ENCRYPTION_ALGORITHM, EXTENDED_SEQUENCE_NUMBERS, +ENUM_BEGIN(transform_type_names, ENCRYPTION_ALGORITHM, ADDITIONAL_KEY_EXCHANGE_7, "ENCRYPTION_ALGORITHM", "PSEUDO_RANDOM_FUNCTION", "INTEGRITY_ALGORITHM", "KEY_EXCHANGE_METHOD", - "EXTENDED_SEQUENCE_NUMBERS"); + "EXTENDED_SEQUENCE_NUMBERS", + "ADDITIONAL_KEY_EXCHANGE_1", + "ADDITIONAL_KEY_EXCHANGE_2", + "ADDITIONAL_KEY_EXCHANGE_3", + "ADDITIONAL_KEY_EXCHANGE_4", + "ADDITIONAL_KEY_EXCHANGE_5", + "ADDITIONAL_KEY_EXCHANGE_6", + "ADDITIONAL_KEY_EXCHANGE_7"); ENUM_NEXT(transform_type_names, HASH_ALGORITHM, KEY_DERIVATION_FUNCTION, - EXTENDED_SEQUENCE_NUMBERS, + ADDITIONAL_KEY_EXCHANGE_7, "HASH_ALGORITHM", "RANDOM_NUMBER_GENERATOR", "AEAD_ALGORITHM", @@ -60,6 +67,13 @@ enum_name_t* transform_get_enum_names(transform_type_t type) case INTEGRITY_ALGORITHM: return integrity_algorithm_names; case KEY_EXCHANGE_METHOD: + case ADDITIONAL_KEY_EXCHANGE_1: + case ADDITIONAL_KEY_EXCHANGE_2: + case ADDITIONAL_KEY_EXCHANGE_3: + case ADDITIONAL_KEY_EXCHANGE_4: + case ADDITIONAL_KEY_EXCHANGE_5: + case ADDITIONAL_KEY_EXCHANGE_6: + case ADDITIONAL_KEY_EXCHANGE_7: return key_exchange_method_names; case EXTENDED_SEQUENCE_NUMBERS: return extended_sequence_numbers_names; diff --git a/src/libstrongswan/crypto/transform.h b/src/libstrongswan/crypto/transform.h index e96ee8ca7..4ec4e9b15 100644 --- a/src/libstrongswan/crypto/transform.h +++ b/src/libstrongswan/crypto/transform.h @@ -1,4 +1,5 @@ /* + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2006-2009 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -35,6 +36,13 @@ enum transform_type_t { INTEGRITY_ALGORITHM = 3, KEY_EXCHANGE_METHOD = 4, EXTENDED_SEQUENCE_NUMBERS = 5, + ADDITIONAL_KEY_EXCHANGE_1 = 6, + ADDITIONAL_KEY_EXCHANGE_2 = 7, + ADDITIONAL_KEY_EXCHANGE_3 = 8, + ADDITIONAL_KEY_EXCHANGE_4 = 9, + ADDITIONAL_KEY_EXCHANGE_5 = 10, + ADDITIONAL_KEY_EXCHANGE_6 = 11, + ADDITIONAL_KEY_EXCHANGE_7 = 12, HASH_ALGORITHM = 256, RANDOM_NUMBER_GENERATOR = 257, AEAD_ALGORITHM = 258, From 2e059e0c2732d36df3e2c30fc3548e61fb9851ee Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 29 Oct 2019 11:46:22 +0100 Subject: [PATCH 02/46] transform: Add helper to check if transform type negotiates key exchange --- src/libstrongswan/crypto/transform.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libstrongswan/crypto/transform.h b/src/libstrongswan/crypto/transform.h index 4ec4e9b15..6e6594fc2 100644 --- a/src/libstrongswan/crypto/transform.h +++ b/src/libstrongswan/crypto/transform.h @@ -65,6 +65,18 @@ extern enum_name_t *transform_type_names; */ enum_name_t *transform_get_enum_names(transform_type_t type); +/** + * Check if the given transform type is used to negotiate a key exchange. + * + * @param type type of transform to check + * @return TRUE if the transform type negotiates a key exchange + */ +static inline bool is_ke_transform(transform_type_t type) +{ + return type == KEY_EXCHANGE_METHOD || (ADDITIONAL_KEY_EXCHANGE_1 <= type && + type <= ADDITIONAL_KEY_EXCHANGE_7); +} + /** * Extended sequence numbers, as in IKEv2 RFC 3.3.2. */ From fb6b8c833b21acac992b5808f5659ba6ed1e7a4c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 29 Oct 2019 11:50:00 +0100 Subject: [PATCH 03/46] proposal: Skip all KE transforms if PROPOSAL_SKIP_KE given --- src/libstrongswan/crypto/proposal/proposal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index 1ad04db4e..5c568a081 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -411,7 +411,7 @@ static bool select_algos(private_proposal_t *this, proposal_t *other, { continue; } - if (type == KEY_EXCHANGE_METHOD && (flags & PROPOSAL_SKIP_KE)) + if (is_ke_transform(type) && (flags & PROPOSAL_SKIP_KE)) { continue; } @@ -604,7 +604,7 @@ METHOD(proposal_t, clone_, proposal_t*, { continue; } - if (entry->type == KEY_EXCHANGE_METHOD && (flags & PROPOSAL_SKIP_KE)) + if (is_ke_transform(entry->type) && (flags & PROPOSAL_SKIP_KE)) { continue; } From 9cc5f4a511a7feac235f986a2c82316fa312d707 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 5 Nov 2019 10:22:36 +0100 Subject: [PATCH 04/46] proposal: Make all key exchange transforms optional in ESP/AH proposals --- src/libstrongswan/crypto/proposal/proposal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index 5c568a081..ba6b3f0f2 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -322,7 +322,7 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, uint16_t alg1, alg2, ks1, ks2; bool found = FALSE, optional = FALSE; - if (type == KEY_EXCHANGE_METHOD) + if (is_ke_transform(type)) { optional = this->protocol == PROTO_ESP || this->protocol == PROTO_AH; } From 3e0495745c3a11705134f81ce5ca5ae936c5c04f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 25 Oct 2019 14:55:05 +0200 Subject: [PATCH 05/46] proposal-substructure: Encode additional key exchange methods --- .../encoding/payloads/proposal_substructure.c | 75 +++++++------------ 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/src/libcharon/encoding/payloads/proposal_substructure.c b/src/libcharon/encoding/payloads/proposal_substructure.c index a1cd71093..f6090a6d7 100644 --- a/src/libcharon/encoding/payloads/proposal_substructure.c +++ b/src/libcharon/encoding/payloads/proposal_substructure.c @@ -1443,22 +1443,21 @@ static void set_from_proposal_v1(private_proposal_substructure_t *this, } /** - * Add an IKEv2 proposal to the substructure + * Encode all transforms of the given type */ -static void set_from_proposal_v2(private_proposal_substructure_t *this, - proposal_t *proposal) +static void encode_transforms_v2(private_proposal_substructure_t *this, + proposal_t *proposal, transform_type_t type) { transform_substructure_t *transform; - uint16_t alg, key_size; enumerator_t *enumerator; + uint16_t alg, key_size; - /* encryption algorithm is only available in ESP */ - enumerator = proposal->create_enumerator(proposal, ENCRYPTION_ALGORITHM); + enumerator = proposal->create_enumerator(proposal, type); while (enumerator->enumerate(enumerator, &alg, &key_size)) { - transform = transform_substructure_create_type(PLV2_TRANSFORM_SUBSTRUCTURE, - ENCRYPTION_ALGORITHM, alg); - if (key_size) + transform = transform_substructure_create_type( + PLV2_TRANSFORM_SUBSTRUCTURE, type, alg); + if (type == ENCRYPTION_ALGORITHM && key_size) { transform->add_transform_attribute(transform, transform_attribute_create_value(PLV2_TRANSFORM_ATTRIBUTE, @@ -1467,46 +1466,26 @@ static void set_from_proposal_v2(private_proposal_substructure_t *this, add_transform_substructure(this, transform); } enumerator->destroy(enumerator); +} - /* integrity algorithms */ - enumerator = proposal->create_enumerator(proposal, INTEGRITY_ALGORITHM); - while (enumerator->enumerate(enumerator, &alg, &key_size)) - { - transform = transform_substructure_create_type(PLV2_TRANSFORM_SUBSTRUCTURE, - INTEGRITY_ALGORITHM, alg); - add_transform_substructure(this, transform); - } - enumerator->destroy(enumerator); - - /* prf algorithms */ - enumerator = proposal->create_enumerator(proposal, PSEUDO_RANDOM_FUNCTION); - while (enumerator->enumerate(enumerator, &alg, &key_size)) - { - transform = transform_substructure_create_type(PLV2_TRANSFORM_SUBSTRUCTURE, - PSEUDO_RANDOM_FUNCTION, alg); - add_transform_substructure(this, transform); - } - enumerator->destroy(enumerator); - - /* dh groups */ - enumerator = proposal->create_enumerator(proposal, KEY_EXCHANGE_METHOD); - while (enumerator->enumerate(enumerator, &alg, NULL)) - { - transform = transform_substructure_create_type(PLV2_TRANSFORM_SUBSTRUCTURE, - KEY_EXCHANGE_METHOD, alg); - add_transform_substructure(this, transform); - } - enumerator->destroy(enumerator); - - /* extended sequence numbers */ - enumerator = proposal->create_enumerator(proposal, EXTENDED_SEQUENCE_NUMBERS); - while (enumerator->enumerate(enumerator, &alg, NULL)) - { - transform = transform_substructure_create_type(PLV2_TRANSFORM_SUBSTRUCTURE, - EXTENDED_SEQUENCE_NUMBERS, alg); - add_transform_substructure(this, transform); - } - enumerator->destroy(enumerator); +/** + * Add an IKEv2 proposal to the substructure + */ +static void set_from_proposal_v2(private_proposal_substructure_t *this, + proposal_t *proposal) +{ + encode_transforms_v2(this, proposal, ENCRYPTION_ALGORITHM); + encode_transforms_v2(this, proposal, INTEGRITY_ALGORITHM); + encode_transforms_v2(this, proposal, PSEUDO_RANDOM_FUNCTION); + encode_transforms_v2(this, proposal, KEY_EXCHANGE_METHOD); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_1); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_2); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_3); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_4); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_5); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_6); + encode_transforms_v2(this, proposal, ADDITIONAL_KEY_EXCHANGE_7); + encode_transforms_v2(this, proposal, EXTENDED_SEQUENCE_NUMBERS); } /** From cc9ab450d6f3c520677707faf0478be831f9f17a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 18 Dec 2019 18:42:59 +0100 Subject: [PATCH 06/46] notify-payload: Add notify type for IKE_INTERMEDIATE exchange --- src/libcharon/encoding/payloads/notify_payload.c | 14 ++++++++------ src/libcharon/encoding/payloads/notify_payload.h | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libcharon/encoding/payloads/notify_payload.c b/src/libcharon/encoding/payloads/notify_payload.c index edb3c489a..f7103005a 100644 --- a/src/libcharon/encoding/payloads/notify_payload.c +++ b/src/libcharon/encoding/payloads/notify_payload.c @@ -114,11 +114,12 @@ ENUM_NEXT(notify_type_names, INITIAL_CONTACT, SIGNATURE_HASH_ALGORITHMS, MS_NOTI "SENDER_REQUEST_ID", "FRAGMENTATION_SUPPORTED", "SIGNATURE_HASH_ALGORITHMS"); -ENUM_NEXT(notify_type_names, USE_PPK, NO_PPK_AUTH, SIGNATURE_HASH_ALGORITHMS, +ENUM_NEXT(notify_type_names, USE_PPK, INTERMEDIATE_EXCHANGE_SUPPORTED, SIGNATURE_HASH_ALGORITHMS, "USE_PPK", "PPK_IDENTITY", - "NO_PPK_AUTH"); -ENUM_NEXT(notify_type_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, NO_PPK_AUTH, + "NO_PPK_AUTH", + "INTERMEDIATE_EXCHANGE_SUPPORTED"); +ENUM_NEXT(notify_type_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, INTERMEDIATE_EXCHANGE_SUPPORTED, "INITIAL_CONTACT"); ENUM_NEXT(notify_type_names, DPD_R_U_THERE, DPD_R_U_THERE_ACK, INITIAL_CONTACT_IKEV1, "DPD_R_U_THERE", @@ -228,11 +229,12 @@ ENUM_NEXT(notify_type_short_names, INITIAL_CONTACT, SIGNATURE_HASH_ALGORITHMS, M "SENDER_REQ_ID", "FRAG_SUP", "HASH_ALG"); -ENUM_NEXT(notify_type_short_names, USE_PPK, NO_PPK_AUTH, SIGNATURE_HASH_ALGORITHMS, +ENUM_NEXT(notify_type_short_names, USE_PPK, INTERMEDIATE_EXCHANGE_SUPPORTED, SIGNATURE_HASH_ALGORITHMS, "USE_PPK", "PPK_ID", - "NO_PPK"); -ENUM_NEXT(notify_type_short_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, NO_PPK_AUTH, + "NO_PPK", + "IKE_INT_SUP"); +ENUM_NEXT(notify_type_short_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, INTERMEDIATE_EXCHANGE_SUPPORTED, "INITIAL_CONTACT"); ENUM_NEXT(notify_type_short_names, DPD_R_U_THERE, DPD_R_U_THERE_ACK, INITIAL_CONTACT_IKEV1, "DPD", diff --git a/src/libcharon/encoding/payloads/notify_payload.h b/src/libcharon/encoding/payloads/notify_payload.h index a1596bb05..55d49a86a 100644 --- a/src/libcharon/encoding/payloads/notify_payload.h +++ b/src/libcharon/encoding/payloads/notify_payload.h @@ -160,6 +160,8 @@ enum notify_type_t { PPK_IDENTITY = 16436, /* No Postquantum Preshared Key Auth, RFC 8784 */ NO_PPK_AUTH = 16437, + /* IKEv2 Intermediate Exchanges, RFC 9242 */ + INTERMEDIATE_EXCHANGE_SUPPORTED = 16438, /* IKEv1 initial contact */ INITIAL_CONTACT_IKEV1 = 24578, /* IKEv1 DPD */ From a45d454e94999fdba11a5b5f08174db2c01e3206 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 25 Oct 2019 14:39:54 +0200 Subject: [PATCH 07/46] ike-header: Add IKE_INTERMEDIATE exchange type --- src/libcharon/encoding/payloads/ike_header.c | 15 +++++++++------ src/libcharon/encoding/payloads/ike_header.h | 5 ++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/libcharon/encoding/payloads/ike_header.c b/src/libcharon/encoding/payloads/ike_header.c index 2aee4496d..8cf503ab2 100644 --- a/src/libcharon/encoding/payloads/ike_header.c +++ b/src/libcharon/encoding/payloads/ike_header.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 Tobias Brunner + * Copyright (C) 2007-2020 Tobias Brunner * Copyright (C) 2005-2006 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -129,15 +129,17 @@ ENUM_NEXT(exchange_type_names, QUICK_MODE, IKE_SESSION_RESUME, TRANSACTION, "CREATE_CHILD_SA", "INFORMATIONAL", "IKE_SESSION_RESUME"); +ENUM_NEXT(exchange_type_names, IKE_INTERMEDIATE, IKE_INTERMEDIATE, IKE_SESSION_RESUME, + "IKE_INTERMEDIATE"); #ifdef ME -ENUM_NEXT(exchange_type_names, ME_CONNECT, ME_CONNECT, IKE_SESSION_RESUME, +ENUM_NEXT(exchange_type_names, ME_CONNECT, ME_CONNECT, IKE_INTERMEDIATE, "ME_CONNECT"); -ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, - EXCHANGE_TYPE_UNDEFINED, ME_CONNECT, +ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, EXCHANGE_TYPE_UNDEFINED, + ME_CONNECT, "EXCHANGE_TYPE_UNDEFINED"); #else -ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, - EXCHANGE_TYPE_UNDEFINED, IKE_SESSION_RESUME, +ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, EXCHANGE_TYPE_UNDEFINED, + IKE_INTERMEDIATE, "EXCHANGE_TYPE_UNDEFINED"); #endif /* ME */ ENUM_END(exchange_type_names, EXCHANGE_TYPE_UNDEFINED); @@ -218,6 +220,7 @@ METHOD(payload_t, verify, status_t, } break; case IKE_SA_INIT: + case IKE_INTERMEDIATE: case IKE_AUTH: case CREATE_CHILD_SA: case INFORMATIONAL: diff --git a/src/libcharon/encoding/payloads/ike_header.h b/src/libcharon/encoding/payloads/ike_header.h index 5a38ad658..72f610377 100644 --- a/src/libcharon/encoding/payloads/ike_header.h +++ b/src/libcharon/encoding/payloads/ike_header.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 Tobias Brunner + * Copyright (C) 2007-2020 Tobias Brunner * Copyright (C) 2005-2011 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -122,6 +122,9 @@ enum exchange_type_t{ */ IKE_SESSION_RESUME = 38, + /* IKE_INTERMEDIATE (RFC 9242) */ + IKE_INTERMEDIATE = 43, + #ifdef ME /** * ME_CONNECT From 25f2cdfc567f8655c006183b59e368fd39a49618 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 25 Oct 2019 14:40:35 +0200 Subject: [PATCH 08/46] message: Add rules for IKE_INTERMEDIATE exchanges --- src/libcharon/encoding/message.c | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/libcharon/encoding/message.c b/src/libcharon/encoding/message.c index ba0c17433..1e750370a 100644 --- a/src/libcharon/encoding/message.c +++ b/src/libcharon/encoding/message.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006-2018 Tobias Brunner + * Copyright (C) 2006-2020 Tobias Brunner * Copyright (C) 2005-2010 Martin Willi * Copyright (C) 2006 Daniel Roethlisberger * Copyright (C) 2005 Jan Hutter @@ -400,6 +400,46 @@ static payload_order_t create_child_sa_r_order[] = { {PLV2_FRAGMENT, 0}, }; +/** + * Message rule for IKE_INTERMEDIATE from initiator. + */ +static payload_rule_t ike_intermediate_i_rules[] = { +/* payload type min max encr suff */ + {PLV2_FRAGMENT, 0, 1, TRUE, TRUE}, + {PLV2_NOTIFY, 0, MAX_NOTIFY_PAYLOADS, TRUE, FALSE}, + {PLV2_KEY_EXCHANGE, 0, 1, TRUE, FALSE}, +}; + +/** + * payload order for IKE_INTERMEDIATE initiator + */ +static payload_order_t ike_intermediate_i_order[] = { +/* payload type notify type */ + {PLV2_KEY_EXCHANGE, 0}, + {PLV2_NOTIFY, 0}, + {PLV2_FRAGMENT, 0}, +}; + +/** + * Message rule for IKE_INTERMEDIATE from responder. + */ +static payload_rule_t ike_intermediate_r_rules[] = { +/* payload type min max encr suff */ + {PLV2_FRAGMENT, 0, 1, TRUE, TRUE}, + {PLV2_NOTIFY, 0, MAX_NOTIFY_PAYLOADS, TRUE, TRUE}, + {PLV2_KEY_EXCHANGE, 0, 1, TRUE, FALSE}, +}; + +/** + * payload order for IKE_INTERMEDIATE responder + */ +static payload_order_t ike_intermediate_r_order[] = { +/* payload type notify type */ + {PLV2_KEY_EXCHANGE, 0}, + {PLV2_NOTIFY, 0}, + {PLV2_FRAGMENT, 0}, +}; + #ifdef ME /** * Message rule for ME_CONNECT from initiator. @@ -767,6 +807,14 @@ static message_rule_t message_rules[] = { countof(create_child_sa_r_rules), create_child_sa_r_rules, countof(create_child_sa_r_order), create_child_sa_r_order, }, + {IKE_INTERMEDIATE, TRUE, TRUE, + countof(ike_intermediate_i_rules), ike_intermediate_i_rules, + countof(ike_intermediate_i_order), ike_intermediate_i_order, + }, + {IKE_INTERMEDIATE, FALSE, TRUE, + countof(ike_intermediate_r_rules), ike_intermediate_r_rules, + countof(ike_intermediate_r_order), ike_intermediate_r_order, + }, #ifdef ME {ME_CONNECT, TRUE, TRUE, countof(me_connect_i_rules), me_connect_i_rules, From aedf73f7cf6e6ee1a99afe644b74011ab9011258 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 8 Feb 2022 14:23:37 +0100 Subject: [PATCH 09/46] ikev2: Reject IKE_INTERMEDIATE requests after IKE_AUTH We currently only support these exchanges for additional key exchanges, so once we have the final keys derived and the ike-init task is removed, we don't expect any more of them. --- src/libcharon/sa/ikev2/task_manager_v2.c | 92 +++++++++++++++--------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 8c07cf272..c5cc34f0e 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -244,6 +244,49 @@ METHOD(task_manager_t, flush, void, flush_queue(this, TASK_QUEUE_ACTIVE); } +/** + * Check if a given task has been queued already + */ +static bool has_queued(private_task_manager_t *this, task_queue_t queue, + task_type_t type) +{ + enumerator_t *enumerator; + array_t *array; + task_t *task; + bool found = FALSE; + + switch (queue) + { + case TASK_QUEUE_ACTIVE: + array = this->active_tasks; + break; + case TASK_QUEUE_PASSIVE: + array = this->passive_tasks; + break; + case TASK_QUEUE_QUEUED: + array = this->queued_tasks; + break; + default: + return FALSE; + } + + enumerator = array_create_enumerator(array); + while (enumerator->enumerate(enumerator, &task)) + { + if (queue == TASK_QUEUE_QUEUED) + { + task = ((queued_task_t*)task)->task; + } + if (task->get_type(task) == type) + { + found = TRUE; + break; + } + } + enumerator->destroy(enumerator); + return found; +} + /** * Move a task of a specific type from the queue to the active list, if it is * not delayed. @@ -1676,6 +1719,11 @@ static inline bool reject_request(private_task_manager_t *this, case IKE_SA_INIT: reject = state != IKE_CREATED; break; + case IKE_INTERMEDIATE: + /* only accept this if we have not yet completed the KEs */ + reject = state != IKE_CONNECTING || + !has_queued(this, TASK_QUEUE_PASSIVE, TASK_IKE_INIT); + break; case IKE_AUTH: reject = state != IKE_CONNECTING; break; @@ -2029,64 +2077,42 @@ METHOD(task_manager_t, queue_task, void, queue_task_delayed(this, task, 0); } -/** - * Check if a given task has been queued already - */ -static bool has_queued(private_task_manager_t *this, task_type_t type) -{ - enumerator_t *enumerator; - bool found = FALSE; - queued_task_t *queued; - - enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, &queued)) - { - if (queued->task->get_type(queued->task) == type) - { - found = TRUE; - break; - } - } - enumerator->destroy(enumerator); - return found; -} - METHOD(task_manager_t, queue_ike, void, private_task_manager_t *this) { - if (!has_queued(this, TASK_IKE_VENDOR)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_VENDOR)) { queue_task(this, (task_t*)ike_vendor_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_INIT)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_INIT)) { queue_task(this, (task_t*)ike_init_create(this->ike_sa, TRUE, NULL)); } - if (!has_queued(this, TASK_IKE_NATD)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_NATD)) { queue_task(this, (task_t*)ike_natd_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_CERT_PRE)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_CERT_PRE)) { queue_task(this, (task_t*)ike_cert_pre_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_AUTH)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_AUTH)) { queue_task(this, (task_t*)ike_auth_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_CERT_POST)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_CERT_POST)) { queue_task(this, (task_t*)ike_cert_post_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_CONFIG)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_CONFIG)) { queue_task(this, (task_t*)ike_config_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_AUTH_LIFETIME)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_AUTH_LIFETIME)) { queue_task(this, (task_t*)ike_auth_lifetime_create(this->ike_sa, TRUE)); } - if (!has_queued(this, TASK_IKE_MOBIKE)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_MOBIKE)) { peer_cfg_t *peer_cfg; @@ -2096,12 +2122,12 @@ METHOD(task_manager_t, queue_ike, void, queue_task(this, (task_t*)ike_mobike_create(this->ike_sa, TRUE)); } } - if (!has_queued(this, TASK_IKE_ESTABLISH)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_ESTABLISH)) { queue_task(this, (task_t*)ike_establish_create(this->ike_sa, TRUE)); } #ifdef ME - if (!has_queued(this, TASK_IKE_ME)) + if (!has_queued(this, TASK_QUEUE_QUEUED, TASK_IKE_ME)) { queue_task(this, (task_t*)ike_me_create(this->ike_sa, TRUE)); } From 041358976bbebf631b7cf39b4de43ac8123a3ff3 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 25 Oct 2019 14:27:47 +0200 Subject: [PATCH 10/46] notify-payload: Add notify types for multiple key exchanges --- src/libcharon/encoding/payloads/notify_payload.c | 16 ++++++++++++---- src/libcharon/encoding/payloads/notify_payload.h | 11 +++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/libcharon/encoding/payloads/notify_payload.c b/src/libcharon/encoding/payloads/notify_payload.c index f7103005a..2b2c6e930 100644 --- a/src/libcharon/encoding/payloads/notify_payload.c +++ b/src/libcharon/encoding/payloads/notify_payload.c @@ -61,7 +61,9 @@ ENUM_NEXT(notify_type_names, SINGLE_PAIR_REQUIRED, CHILD_SA_NOT_FOUND, AUTHENTIC "USE_ASSIGNED_HoA", "TEMPORARY_FAILURE", "CHILD_SA_NOT_FOUND"); -ENUM_NEXT(notify_type_names, ME_CONNECT_FAILED, ME_CONNECT_FAILED, CHILD_SA_NOT_FOUND, +ENUM_NEXT(notify_type_names, STATE_NOT_FOUND, STATE_NOT_FOUND, CHILD_SA_NOT_FOUND, + "STATE_NOT_FOUND"); +ENUM_NEXT(notify_type_names, ME_CONNECT_FAILED, ME_CONNECT_FAILED, STATE_NOT_FOUND, "ME_CONNECT_FAILED"); ENUM_NEXT(notify_type_names, MS_NOTIFY_STATUS, MS_NOTIFY_STATUS, ME_CONNECT_FAILED, "MS_NOTIFY_STATUS"); @@ -119,7 +121,9 @@ ENUM_NEXT(notify_type_names, USE_PPK, INTERMEDIATE_EXCHANGE_SUPPORTED, SIGNATURE "PPK_IDENTITY", "NO_PPK_AUTH", "INTERMEDIATE_EXCHANGE_SUPPORTED"); -ENUM_NEXT(notify_type_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, INTERMEDIATE_EXCHANGE_SUPPORTED, +ENUM_NEXT(notify_type_names, ADDITIONAL_KEY_EXCHANGE, ADDITIONAL_KEY_EXCHANGE, INTERMEDIATE_EXCHANGE_SUPPORTED, + "ADDITIONAL_KEY_EXCHANGE"); +ENUM_NEXT(notify_type_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, ADDITIONAL_KEY_EXCHANGE, "INITIAL_CONTACT"); ENUM_NEXT(notify_type_names, DPD_R_U_THERE, DPD_R_U_THERE_ACK, INITIAL_CONTACT_IKEV1, "DPD_R_U_THERE", @@ -176,7 +180,9 @@ ENUM_NEXT(notify_type_short_names, SINGLE_PAIR_REQUIRED, CHILD_SA_NOT_FOUND, AUT "ASSIGNED_HoA", "TEMP_FAIL", "NO_CHILD_SA"); -ENUM_NEXT(notify_type_short_names, ME_CONNECT_FAILED, ME_CONNECT_FAILED, CHILD_SA_NOT_FOUND, +ENUM_NEXT(notify_type_short_names, STATE_NOT_FOUND, STATE_NOT_FOUND, CHILD_SA_NOT_FOUND, + "NO_STATE"); +ENUM_NEXT(notify_type_short_names, ME_CONNECT_FAILED, ME_CONNECT_FAILED, STATE_NOT_FOUND, "ME_CONN_FAIL"); ENUM_NEXT(notify_type_short_names, MS_NOTIFY_STATUS, MS_NOTIFY_STATUS, ME_CONNECT_FAILED, "MS_STATUS"); @@ -234,7 +240,9 @@ ENUM_NEXT(notify_type_short_names, USE_PPK, INTERMEDIATE_EXCHANGE_SUPPORTED, SIG "PPK_ID", "NO_PPK", "IKE_INT_SUP"); -ENUM_NEXT(notify_type_short_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, INTERMEDIATE_EXCHANGE_SUPPORTED, +ENUM_NEXT(notify_type_short_names, ADDITIONAL_KEY_EXCHANGE, ADDITIONAL_KEY_EXCHANGE, INTERMEDIATE_EXCHANGE_SUPPORTED, + "ADD_KE"); +ENUM_NEXT(notify_type_short_names, INITIAL_CONTACT_IKEV1, INITIAL_CONTACT_IKEV1, ADDITIONAL_KEY_EXCHANGE, "INITIAL_CONTACT"); ENUM_NEXT(notify_type_short_names, DPD_R_U_THERE, DPD_R_U_THERE_ACK, INITIAL_CONTACT_IKEV1, "DPD", diff --git a/src/libcharon/encoding/payloads/notify_payload.h b/src/libcharon/encoding/payloads/notify_payload.h index 55d49a86a..eb0784b3e 100644 --- a/src/libcharon/encoding/payloads/notify_payload.h +++ b/src/libcharon/encoding/payloads/notify_payload.h @@ -80,6 +80,9 @@ enum notify_type_t { TEMPORARY_FAILURE = 43, CHILD_SA_NOT_FOUND = 44, + /* multiple key exchanges, RFC 9370 */ + STATE_NOT_FOUND = 47, + /* IKE-ME, private use */ ME_CONNECT_FAILED = 8192, @@ -154,6 +157,7 @@ enum notify_type_t { FRAGMENTATION_SUPPORTED = 16430, /* Signature Hash Algorithms, RFC 7427 */ SIGNATURE_HASH_ALGORITHMS = 16431, + /* Use Postquantum Preshared Key, RFC 8784 */ USE_PPK = 16435, /* Postquantum Preshared Key Identity, RFC 8784 */ @@ -162,13 +166,20 @@ enum notify_type_t { NO_PPK_AUTH = 16437, /* IKEv2 Intermediate Exchanges, RFC 9242 */ INTERMEDIATE_EXCHANGE_SUPPORTED = 16438, + + /* multiple key exchanges, RFC 9370 */ + ADDITIONAL_KEY_EXCHANGE = 16441, + /* IKEv1 initial contact */ INITIAL_CONTACT_IKEV1 = 24578, + /* IKEv1 DPD */ DPD_R_U_THERE = 36136, DPD_R_U_THERE_ACK = 36137, + /* IKEv1 Cisco High Availability */ UNITY_LOAD_BALANCE = 40501, + /* BEET mode, not even a draft yet. private use */ USE_BEET_MODE = 40961, /* IKE-ME, private use */ From 414db6cab1129d8185fe29e5cd01db8d62bd8315 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 3 Apr 2020 15:01:17 +0200 Subject: [PATCH 11/46] ike-header: Add IKE_FOLLOWUP_KE exchange type --- src/libcharon/encoding/payloads/ike_header.c | 11 +++++++---- src/libcharon/encoding/payloads/ike_header.h | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libcharon/encoding/payloads/ike_header.c b/src/libcharon/encoding/payloads/ike_header.c index 8cf503ab2..bdbb6463a 100644 --- a/src/libcharon/encoding/payloads/ike_header.c +++ b/src/libcharon/encoding/payloads/ike_header.c @@ -129,17 +129,19 @@ ENUM_NEXT(exchange_type_names, QUICK_MODE, IKE_SESSION_RESUME, TRANSACTION, "CREATE_CHILD_SA", "INFORMATIONAL", "IKE_SESSION_RESUME"); -ENUM_NEXT(exchange_type_names, IKE_INTERMEDIATE, IKE_INTERMEDIATE, IKE_SESSION_RESUME, - "IKE_INTERMEDIATE"); +ENUM_NEXT(exchange_type_names, IKE_INTERMEDIATE, IKE_FOLLOWUP_KE, + IKE_SESSION_RESUME, + "IKE_INTERMEDIATE", + "IKE_FOLLOWUP_KE"); #ifdef ME -ENUM_NEXT(exchange_type_names, ME_CONNECT, ME_CONNECT, IKE_INTERMEDIATE, +ENUM_NEXT(exchange_type_names, ME_CONNECT, ME_CONNECT, IKE_FOLLOWUP_KE, "ME_CONNECT"); ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, EXCHANGE_TYPE_UNDEFINED, ME_CONNECT, "EXCHANGE_TYPE_UNDEFINED"); #else ENUM_NEXT(exchange_type_names, EXCHANGE_TYPE_UNDEFINED, EXCHANGE_TYPE_UNDEFINED, - IKE_INTERMEDIATE, + IKE_FOLLOWUP_KE, "EXCHANGE_TYPE_UNDEFINED"); #endif /* ME */ ENUM_END(exchange_type_names, EXCHANGE_TYPE_UNDEFINED); @@ -225,6 +227,7 @@ METHOD(payload_t, verify, status_t, case CREATE_CHILD_SA: case INFORMATIONAL: case IKE_SESSION_RESUME: + case IKE_FOLLOWUP_KE: #ifdef ME case ME_CONNECT: #endif /* ME */ diff --git a/src/libcharon/encoding/payloads/ike_header.h b/src/libcharon/encoding/payloads/ike_header.h index 72f610377..61625c4ae 100644 --- a/src/libcharon/encoding/payloads/ike_header.h +++ b/src/libcharon/encoding/payloads/ike_header.h @@ -125,6 +125,9 @@ enum exchange_type_t{ /* IKE_INTERMEDIATE (RFC 9242) */ IKE_INTERMEDIATE = 43, + /* IKE_FOLLOWUP_KE (RFC 9370) */ + IKE_FOLLOWUP_KE = 44, + #ifdef ME /** * ME_CONNECT From b8358936aa7f0bbd662879209b7a1c05ca01cdc6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 3 Apr 2020 15:10:40 +0200 Subject: [PATCH 12/46] message: Add rules for IKE_FOLLOWUP_KE exchanges --- src/libcharon/encoding/message.c | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/libcharon/encoding/message.c b/src/libcharon/encoding/message.c index 1e750370a..1876d992e 100644 --- a/src/libcharon/encoding/message.c +++ b/src/libcharon/encoding/message.c @@ -440,6 +440,46 @@ static payload_order_t ike_intermediate_r_order[] = { {PLV2_FRAGMENT, 0}, }; +/** + * Message rule for IKE_FOLLOWUP_KE from initiator. + */ +static payload_rule_t ike_followup_ke_i_rules[] = { +/* payload type min max encr suff */ + {PLV2_FRAGMENT, 0, 1, TRUE, TRUE}, + {PLV2_NOTIFY, 0, MAX_NOTIFY_PAYLOADS, TRUE, FALSE}, + {PLV2_KEY_EXCHANGE, 1, 1, TRUE, FALSE}, +}; + +/** + * payload order for IKE_FOLLOWUP_KE initiator + */ +static payload_order_t ike_followup_ke_i_order[] = { +/* payload type notify type */ + {PLV2_KEY_EXCHANGE, 0}, + {PLV2_NOTIFY, 0}, + {PLV2_FRAGMENT, 0}, +}; + +/** + * Message rule for IKE_FOLLOWUP_KE from responder. + */ +static payload_rule_t ike_followup_ke_r_rules[] = { +/* payload type min max encr suff */ + {PLV2_FRAGMENT, 0, 1, TRUE, TRUE}, + {PLV2_NOTIFY, 0, MAX_NOTIFY_PAYLOADS, TRUE, TRUE}, + {PLV2_KEY_EXCHANGE, 1, 1, TRUE, FALSE}, +}; + +/** + * payload order for IKE_FOLLOWUP_KE responder + */ +static payload_order_t ike_followup_ke_r_order[] = { +/* payload type notify type */ + {PLV2_KEY_EXCHANGE, 0}, + {PLV2_NOTIFY, 0}, + {PLV2_FRAGMENT, 0}, +}; + #ifdef ME /** * Message rule for ME_CONNECT from initiator. @@ -815,6 +855,14 @@ static message_rule_t message_rules[] = { countof(ike_intermediate_r_rules), ike_intermediate_r_rules, countof(ike_intermediate_r_order), ike_intermediate_r_order, }, + {IKE_FOLLOWUP_KE, TRUE, TRUE, + countof(ike_followup_ke_i_rules), ike_followup_ke_i_rules, + countof(ike_followup_ke_i_order), ike_followup_ke_i_order, + }, + {IKE_FOLLOWUP_KE, FALSE, TRUE, + countof(ike_followup_ke_r_rules), ike_followup_ke_r_rules, + countof(ike_followup_ke_r_order), ike_followup_ke_r_order, + }, #ifdef ME {ME_CONNECT, TRUE, TRUE, countof(me_connect_i_rules), me_connect_i_rules, From a24993213ef30efd7fa37e59499d2451e594701e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 2 Jul 2019 15:01:26 +0200 Subject: [PATCH 13/46] keymat_v2: Add method to calculate IntAuth for IKE_INTERMEDIATE exchanges --- src/charon-tkm/src/tkm/tkm_keymat.c | 11 ++++++++++- src/libcharon/sa/ikev2/keymat_v2.c | 26 ++++++++++++++++++++++++-- src/libcharon/sa/ikev2/keymat_v2.h | 19 ++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/charon-tkm/src/tkm/tkm_keymat.c b/src/charon-tkm/src/tkm/tkm_keymat.c index 6e79c639e..2d3c1030e 100644 --- a/src/charon-tkm/src/tkm/tkm_keymat.c +++ b/src/charon-tkm/src/tkm/tkm_keymat.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Tobias Brunner + * Copyright (C) 2015-2019 Tobias Brunner * Copyright (C) 2012 Reto Buerki * Copyright (C) 2012 Adrian-Ken Rueegsegger * @@ -246,6 +246,14 @@ METHOD(keymat_t, get_aead, aead_t*, return this->aead; } +METHOD(keymat_v2_t, get_int_auth, bool, + private_tkm_keymat_t *this, bool verify, chunk_t data, chunk_t prev, + chunk_t *auth) +{ + DBG1(DBG_IKE, "TKM doesn't support IntAuth calculation"); + return FALSE; +} + METHOD(keymat_v2_t, get_auth_octets, bool, private_tkm_keymat_t *this, bool verify, chunk_t ike_sa_init, chunk_t nonce, chunk_t ppk, identification_t *id, char reserved[3], @@ -388,6 +396,7 @@ tkm_keymat_t *tkm_keymat_create(bool initiator) .derive_ike_keys_ppk = (void*)return_false, .derive_child_keys = _derive_child_keys, .get_skd = _get_skd, + .get_int_auth = _get_int_auth, .get_auth_octets = _get_auth_octets, .get_psk_sig = _get_psk_sig, .add_hash_algorithm = _add_hash_algorithm, diff --git a/src/libcharon/sa/ikev2/keymat_v2.c b/src/libcharon/sa/ikev2/keymat_v2.c index c8a9c7117..1a6cda609 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.c +++ b/src/libcharon/sa/ikev2/keymat_v2.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Tobias Brunner + * Copyright (C) 2015-2019 Tobias Brunner * Copyright (C) 2008 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -261,6 +261,7 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, return FALSE; } this->prf_alg = prf_alg; + DESTROY_IF(this->prf); this->prf = lib->crypto->create_prf(lib->crypto, this->prf_alg); if (!this->prf) { @@ -656,6 +657,27 @@ METHOD(keymat_t, get_aead, aead_t*, return in ? this->aead_in : this->aead_out; } +METHOD(keymat_v2_t, get_int_auth, bool, + private_keymat_v2_t *this, bool verify, chunk_t data, chunk_t prev, + chunk_t *auth) +{ + chunk_t skp; + + skp = verify ? this->skp_verify : this->skp_build; + + DBG3(DBG_IKE, "IntAuth_N-1 %B", &prev); + DBG3(DBG_IKE, "IntAuth_A|P %B", &data); + DBG4(DBG_IKE, "SK_p %B", &skp); + if (!this->prf->set_key(this->prf, skp) || + !this->prf->allocate_bytes(this->prf, prev, NULL) || + !this->prf->allocate_bytes(this->prf, data, auth)) + { + return FALSE; + } + DBG3(DBG_IKE, "IntAuth_N = prf(Sk_px, data) %B", auth); + return TRUE; +} + METHOD(keymat_v2_t, get_auth_octets, bool, private_keymat_v2_t *this, bool verify, chunk_t ike_sa_init, chunk_t nonce, chunk_t ppk, identification_t *id, char reserved[3], @@ -749,7 +771,6 @@ failure: chunk_free(&octets); chunk_free(&key); return success; - } METHOD(keymat_v2_t, hash_algorithm_supported, bool, @@ -805,6 +826,7 @@ keymat_v2_t *keymat_v2_create(bool initiator) .derive_ike_keys_ppk = _derive_ike_keys_ppk, .derive_child_keys = _derive_child_keys, .get_skd = _get_skd, + .get_int_auth = _get_int_auth, .get_auth_octets = _get_auth_octets, .get_psk_sig = _get_psk_sig, .add_hash_algorithm = _add_hash_algorithm, diff --git a/src/libcharon/sa/ikev2/keymat_v2.h b/src/libcharon/sa/ikev2/keymat_v2.h index 60bac653b..f2f0bec1c 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.h +++ b/src/libcharon/sa/ikev2/keymat_v2.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2015 Tobias Brunner + * Copyright (C) 2011-2019 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -91,6 +91,7 @@ struct keymat_v2_t { chunk_t nonce_i, chunk_t nonce_r, chunk_t *encr_i, chunk_t *integ_i, chunk_t *encr_r, chunk_t *integ_r); + /** * Get SKd to pass to derive_ikey_keys() during rekeying. * @@ -99,6 +100,22 @@ struct keymat_v2_t { */ pseudo_random_function_t (*get_skd)(keymat_v2_t *this, chunk_t *skd); + /** + * Generate data for signed octets when using IKE_INTEMEDIATE exchanges. + * + * The supplied chunk must contain the IKE header until the end of the + * Encrypted Payload header followed by the plaintext contents of the + * latter. + * + * @param verify TRUE as recipient, FALSE as sender + * @param data IKE_INTERMEDIATE packet data + * @param prev previous IntAuth value + * @param[out] auth IntAuth data to be used later with get_auth_octets() + * @return TRUE if octets created successfully + */ + bool (*get_int_auth)(keymat_v2_t *this, bool verify, chunk_t data, + chunk_t prev, chunk_t *auth); + /** * Generate octets to use for authentication procedure (RFC4306 2.15). * From 5c439bb8a3e4fa403056d855af7ea0492424e541 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 14:53:16 +0200 Subject: [PATCH 14/46] generator: Make pointer to length field optional Only useful if we generate an IKE header. --- src/libcharon/encoding/generator.c | 5 ++++- src/libcharon/encoding/message.c | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libcharon/encoding/generator.c b/src/libcharon/encoding/generator.c index 3075abcf4..aaa7f723f 100644 --- a/src/libcharon/encoding/generator.c +++ b/src/libcharon/encoding/generator.c @@ -430,7 +430,10 @@ METHOD(generator_t, get_chunk, chunk_t, { chunk_t data; - *lenpos = (uint32_t*)(this->buffer + this->header_length_offset); + if (lenpos) + { + *lenpos = (uint32_t*)(this->buffer + this->header_length_offset); + } data = chunk_create(this->buffer, get_length(this)); if (this->debug) { diff --git a/src/libcharon/encoding/message.c b/src/libcharon/encoding/message.c index 1876d992e..8ed0b1a68 100644 --- a/src/libcharon/encoding/message.c +++ b/src/libcharon/encoding/message.c @@ -2090,7 +2090,6 @@ METHOD(message_t, fragment, status_t, host_t *src, *dst; chunk_t data; status_t status; - uint32_t *lenpos; size_t len; src = this->packet->get_source(this->packet); @@ -2123,7 +2122,7 @@ METHOD(message_t, fragment, status_t, DESTROY_IF(generator); return status; } - data = generator->get_chunk(generator, &lenpos); + data = generator->get_chunk(generator, NULL); len = data.len + (encrypted ? encrypted->get_length(encrypted) : 0); } @@ -2179,7 +2178,7 @@ METHOD(message_t, fragment, status_t, } next = encrypted->payload_interface.get_next_type((payload_t*)encrypted); encrypted->generate_payloads(encrypted, generator); - data = generator->get_chunk(generator, &lenpos); + data = generator->get_chunk(generator, NULL); if (!is_encoded(this)) { encrypted->destroy(encrypted); From b9c69f90808a74ede77691ef7a648075069489cd Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 14:57:30 +0200 Subject: [PATCH 15/46] message: Add method to generate data to authenticate IKE_INTERMEDIATE exchanges --- src/libcharon/encoding/message.c | 77 ++++++++++++++++++++++++++++++++ src/libcharon/encoding/message.h | 15 +++++++ 2 files changed, 92 insertions(+) diff --git a/src/libcharon/encoding/message.c b/src/libcharon/encoding/message.c index 8ed0b1a68..fcc36024b 100644 --- a/src/libcharon/encoding/message.c +++ b/src/libcharon/encoding/message.c @@ -1984,6 +1984,82 @@ METHOD(message_t, generate, status_t, return SUCCESS; } +METHOD(message_t, get_plain, bool, + private_message_t *this, chunk_t *plain) +{ + generator_t *generator, *enc_generator; + enumerator_t *enumerator; + ike_header_t *ike_header; + payload_t *payload; + encrypted_payload_t *encrypted; + chunk_t int_auth_a, enc_header, int_auth_p; + struct { + uint8_t next_payload; + uint8_t flags; + uint16_t length; + } __attribute__((packed)) header = {}; + uint32_t *lenpos; + + if (this->major_version == IKEV1_MAJOR_VERSION || + this->exchange_type != IKE_INTERMEDIATE) + { + return FALSE; + } + + /* we expect to be called after the message has either been parsed + * or already generated once, so we don't modify payload order */ + generator = generator_create_no_dbg(); + ike_header = create_header(this); + payload = (payload_t*)ike_header; + /* for parsed messages the payloads were already extracted from the + * encrypted payload, if there were any unprotected paylaods we wouldn't + * know. lets assume there aren't any (also for sent messages) */ + payload->set_next_type(payload, PLV2_ENCRYPTED); + + generator->generate_payload(generator, payload); + int_auth_a = generator->get_chunk(generator, &lenpos); + + enc_generator = generator_create_no_dbg(); + this->payloads->get_first(this->payloads, (void**)&payload); + if (payload && payload->get_type(payload) == PLV2_ENCRYPTED) + { + /* we have to generate only the contents of this payload, + * not the payload itself, the header is added manually */ + encrypted = (encrypted_payload_t*)payload; + + encrypted->generate_payloads(encrypted, enc_generator); + + header.next_payload = payload->get_next_type(payload); + } + else + { + /* as mentioned above, assume all received payloads were contained in an + * encrypted payload */ + enumerator = create_payload_enumerator(this); + while (enumerator->enumerate(enumerator, &payload)) + { + enc_generator->generate_payload(enc_generator, payload); + } + enumerator->destroy(enumerator); + + header.next_payload = this->first_payload; + } + int_auth_p = enc_generator->get_chunk(enc_generator, NULL); + + /* flags are currently no copied, but the critical bit and the reserved + * bits MUST be zero for encrypted payloads, so that's what we assume */ + enc_header = chunk_from_thing(header); + header.length = htons(enc_header.len + int_auth_p.len); + + htoun32(lenpos, int_auth_a.len + enc_header.len + int_auth_p.len); + *plain = chunk_cat("ccc", int_auth_a, enc_header, int_auth_p); + + enc_generator->destroy(enc_generator); + generator->destroy(generator); + ike_header->destroy(ike_header); + return TRUE; +} + /** * Creates a (basic) clone of the given message */ @@ -3131,6 +3207,7 @@ message_t *message_create_from_packet(packet_t *packet) .get_fragments = _get_fragments, .get_metadata = _get_metadata, .set_metadata = _set_metadata, + .get_plain = _get_plain, .destroy = _destroy, }, .exchange_type = EXCHANGE_TYPE_UNDEFINED, diff --git a/src/libcharon/encoding/message.h b/src/libcharon/encoding/message.h index a31fce60b..229ee1225 100644 --- a/src/libcharon/encoding/message.h +++ b/src/libcharon/encoding/message.h @@ -258,6 +258,21 @@ struct message_t { */ status_t (*generate) (message_t *this, keymat_t *keymat, packet_t **packet); + /** + * Generate the plaintext encoding of this message as needed to authenticate + * IKE_INTERMEDIATE exchanges. + * + * The data returned is the concatenation of the IKE header and plaintext + * payloads (if any) up until the end of the header of the Encrypted + * Payload followed by the plaintext data of the Encrypted Payload (if any). + * Lenght fields are adjusted to only contain that of returned data (e.g. + * IV or padding is ignored). + * + * @param[out] plain plaintext encoding (allocated) + * @return TRUE if generated successfully + */ + bool (*get_plain)(message_t *this, chunk_t *plain); + /** * Check if the message has already been encoded using generate(). * From 515b9303dec61159b9e601be2c5e98781a6ee486 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 19 Jul 2021 17:12:09 +0200 Subject: [PATCH 16/46] message: Store original encrypted payload when generating fragments If we don't do this, get_plain() will fail after generating the message fragmented unless it was generated non-fragmented previously. --- src/libcharon/encoding/message.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libcharon/encoding/message.c b/src/libcharon/encoding/message.c index fcc36024b..5d05e2091 100644 --- a/src/libcharon/encoding/message.c +++ b/src/libcharon/encoding/message.c @@ -2257,7 +2257,7 @@ METHOD(message_t, fragment, status_t, data = generator->get_chunk(generator, NULL); if (!is_encoded(this)) { - encrypted->destroy(encrypted); + this->payloads->insert_last(this->payloads, encrypted); } aead = keymat->get_aead(keymat, FALSE); /* overhead for the encrypted fragment payload */ From 91f09b8d25df28b484f134fd8516293046a69b4a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 16:13:11 +0200 Subject: [PATCH 17/46] authenticator: Add optional method to set IntAuth data --- src/libcharon/sa/authenticator.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libcharon/sa/authenticator.h b/src/libcharon/sa/authenticator.h index cafea503c..0e05f7435 100644 --- a/src/libcharon/sa/authenticator.h +++ b/src/libcharon/sa/authenticator.h @@ -168,6 +168,16 @@ struct authenticator_t { */ void (*use_ppk)(authenticator_t *this, chunk_t ppk, bool no_ppk_auth); + /** + * Optional method to set authentication data for IKE_INTERMEDIATE + * exchanges. + * + * Has to be called before the final call to process()/build(). + * + * @param int_auth concatenated IntAuth_I|R data + */ + void (*set_int_auth)(authenticator_t *this, chunk_t int_auth); + /** * Check if the authenticator is capable of mutual authentication. * From e5828d26ea3fff45d027de5d6d1940f4249a20e5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 16:18:05 +0200 Subject: [PATCH 18/46] keymat_v2: Include optional IntAuth in signed octets --- src/charon-tkm/src/tkm/tkm_keymat.c | 8 ++++---- src/conftest/hooks/pretend_auth.c | 4 ++-- src/conftest/hooks/rebuild_auth.c | 3 ++- .../ikev2/authenticators/eap_authenticator.c | 13 ++++++------ .../ikev2/authenticators/psk_authenticator.c | 12 +++++------ .../authenticators/pubkey_authenticator.c | 10 ++++++---- src/libcharon/sa/ikev2/keymat_v2.c | 19 +++++++++--------- src/libcharon/sa/ikev2/keymat_v2.h | 20 +++++++++++-------- 8 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/charon-tkm/src/tkm/tkm_keymat.c b/src/charon-tkm/src/tkm/tkm_keymat.c index 2d3c1030e..3d181ca6d 100644 --- a/src/charon-tkm/src/tkm/tkm_keymat.c +++ b/src/charon-tkm/src/tkm/tkm_keymat.c @@ -256,8 +256,8 @@ METHOD(keymat_v2_t, get_int_auth, bool, METHOD(keymat_v2_t, get_auth_octets, bool, private_tkm_keymat_t *this, bool verify, chunk_t ike_sa_init, - chunk_t nonce, chunk_t ppk, identification_t *id, char reserved[3], - chunk_t *octets, array_t *schemes) + chunk_t nonce, chunk_t int_auth, chunk_t ppk, identification_t *id, + char reserved[3], chunk_t *octets, array_t *schemes) { sign_info_t *sign; @@ -299,8 +299,8 @@ METHOD(keymat_v2_t, get_skd, pseudo_random_function_t, METHOD(keymat_v2_t, get_psk_sig, bool, private_tkm_keymat_t *this, bool verify, chunk_t ike_sa_init, chunk_t nonce, - chunk_t secret, chunk_t ppk, identification_t *id, char reserved[3], - chunk_t *sig) + chunk_t int_auth, chunk_t secret, chunk_t ppk, identification_t *id, + char reserved[3], chunk_t *sig) { return FALSE; } diff --git a/src/conftest/hooks/pretend_auth.c b/src/conftest/hooks/pretend_auth.c index 5d16fb9a5..7c7d24a63 100644 --- a/src/conftest/hooks/pretend_auth.c +++ b/src/conftest/hooks/pretend_auth.c @@ -239,8 +239,8 @@ static bool build_auth(private_pretend_auth_t *this, } keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); if (!keymat->get_auth_octets(keymat, TRUE, this->ike_init, this->nonce, - chunk_empty, this->id, this->reserved, - &octets, NULL)) + chunk_empty, chunk_empty, this->id, + this->reserved, &octets, NULL)) { private->destroy(private); return FALSE; diff --git a/src/conftest/hooks/rebuild_auth.c b/src/conftest/hooks/rebuild_auth.c index 917e2212e..11c3cd4ac 100644 --- a/src/conftest/hooks/rebuild_auth.c +++ b/src/conftest/hooks/rebuild_auth.c @@ -138,7 +138,8 @@ static bool rebuild_auth(private_rebuild_auth_t *this, ike_sa_t *ike_sa, } keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); if (!keymat->get_auth_octets(keymat, FALSE, this->ike_init, this->nonce, - chunk_empty, id, reserved, &octets, NULL)) + chunk_empty, chunk_empty, id, reserved, + &octets, NULL)) { private->destroy(private); id->destroy(id); diff --git a/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c b/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c index 5f60af7ca..2b109aef2 100644 --- a/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c @@ -495,8 +495,8 @@ static bool verify_auth(private_eap_authenticator_t *this, message_t *message, other_id = this->ike_sa->get_other_id(this->ike_sa); keymat = (keymat_v2_t*)this->ike_sa->get_keymat(this->ike_sa); - if (!keymat->get_psk_sig(keymat, TRUE, init, nonce, this->msk, this->ppk, - other_id, this->reserved, &auth_data)) + if (!keymat->get_psk_sig(keymat, TRUE, init, nonce, chunk_empty, this->msk, + this->ppk, other_id, this->reserved, &auth_data)) { return FALSE; } @@ -541,8 +541,8 @@ static bool build_auth(private_eap_authenticator_t *this, message_t *message, DBG1(DBG_IKE, "authentication of '%Y' (myself) with %N", my_id, auth_class_names, AUTH_CLASS_EAP); - if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, this->msk, this->ppk, - my_id, this->reserved, &auth_data)) + if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, chunk_empty, this->msk, + this->ppk, my_id, this->reserved, &auth_data)) { return FALSE; } @@ -554,8 +554,9 @@ static bool build_auth(private_eap_authenticator_t *this, message_t *message, if (this->no_ppk_auth) { - if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, this->msk, - chunk_empty, my_id, this->reserved, &auth_data)) + if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, chunk_empty, + this->msk, chunk_empty, my_id, this->reserved, + &auth_data)) { DBG1(DBG_IKE, "failed adding NO_PPK_AUTH notify"); return FALSE; diff --git a/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c b/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c index 99c7fdb5e..5e1cbb712 100644 --- a/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c @@ -86,8 +86,8 @@ METHOD(authenticator_t, build, status_t, return NOT_FOUND; } if (!keymat->get_psk_sig(keymat, FALSE, this->ike_sa_init, this->nonce, - key->get_key(key), this->ppk, my_id, - this->reserved, &auth_data)) + chunk_empty, key->get_key(key), this->ppk, + my_id, this->reserved, &auth_data)) { key->destroy(key); return FAILED; @@ -103,8 +103,8 @@ METHOD(authenticator_t, build, status_t, if (this->no_ppk_auth) { if (!keymat->get_psk_sig(keymat, FALSE, this->ike_sa_init, this->nonce, - key->get_key(key), chunk_empty, my_id, - this->reserved, &auth_data)) + chunk_empty, key->get_key(key), chunk_empty, + my_id, this->reserved, &auth_data)) { DBG1(DBG_IKE, "failed adding NO_PPK_AUTH notify"); key->destroy(key); @@ -160,8 +160,8 @@ METHOD(authenticator_t, process, status_t, keys_found++; if (!keymat->get_psk_sig(keymat, TRUE, this->ike_sa_init, this->nonce, - key->get_key(key), this->ppk, other_id, - this->reserved, &auth_data)) + chunk_empty, key->get_key(key), this->ppk, + other_id, this->reserved, &auth_data)) { continue; } diff --git a/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c b/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c index 4f83dad10..7783d9274 100644 --- a/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c @@ -325,7 +325,8 @@ static status_t sign_signature_auth(private_pubkey_authenticator_t *this, } if (keymat->get_auth_octets(keymat, FALSE, this->ike_sa_init, this->nonce, - this->ppk, id, this->reserved, &octets, schemes)) + chunk_empty, this->ppk, id, this->reserved, + &octets, schemes)) { enumerator = array_create_enumerator(schemes); while (enumerator->enumerate(enumerator, ¶ms)) @@ -347,8 +348,9 @@ static status_t sign_signature_auth(private_pubkey_authenticator_t *this, chunk_free(&octets); if (keymat->get_auth_octets(keymat, FALSE, this->ike_sa_init, - this->nonce, chunk_empty, id, - this->reserved, &octets, schemes) && + this->nonce, chunk_empty, + chunk_empty, id, this->reserved, + &octets, schemes) && private->sign(private, params->scheme, params->params, octets, &auth_data) && build_signature_auth_data(&auth_data, params)) @@ -412,7 +414,7 @@ static bool get_auth_octets_scheme(private_pubkey_authenticator_t *this, keymat = (keymat_v2_t*)this->ike_sa->get_keymat(this->ike_sa); if (keymat->get_auth_octets(keymat, verify, this->ike_sa_init, this->nonce, - ppk, id, this->reserved, octets, + chunk_empty, ppk, id, this->reserved, octets, schemes) && array_remove(schemes, 0, scheme)) { diff --git a/src/libcharon/sa/ikev2/keymat_v2.c b/src/libcharon/sa/ikev2/keymat_v2.c index 1a6cda609..69a70fc6f 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.c +++ b/src/libcharon/sa/ikev2/keymat_v2.c @@ -680,8 +680,8 @@ METHOD(keymat_v2_t, get_int_auth, bool, METHOD(keymat_v2_t, get_auth_octets, bool, private_keymat_v2_t *this, bool verify, chunk_t ike_sa_init, - chunk_t nonce, chunk_t ppk, identification_t *id, char reserved[3], - chunk_t *octets, array_t *schemes) + chunk_t nonce, chunk_t int_auth, chunk_t ppk, identification_t *id, + char reserved[3], chunk_t *octets, array_t *schemes) { chunk_t chunk, idx; chunk_t skp_ppk = chunk_empty; @@ -712,8 +712,9 @@ METHOD(keymat_v2_t, get_auth_octets, bool, return FALSE; } chunk_clear(&skp_ppk); - *octets = chunk_cat("ccm", ike_sa_init, nonce, chunk); - DBG3(DBG_IKE, "octets = message + nonce + prf(Sk_px, IDx') %B", octets); + *octets = chunk_cat("ccmc", ike_sa_init, nonce, chunk, int_auth); + DBG3(DBG_IKE, "octets = message + nonce + prf(Sk_px, IDx') + IntAuth %B", + octets); return TRUE; } @@ -724,9 +725,9 @@ METHOD(keymat_v2_t, get_auth_octets, bool, #define IKEV2_KEY_PAD_LENGTH 17 METHOD(keymat_v2_t, get_psk_sig, bool, - private_keymat_v2_t *this, bool verify, chunk_t ike_sa_init, chunk_t nonce, - chunk_t secret, chunk_t ppk, identification_t *id, char reserved[3], - chunk_t *sig) + private_keymat_v2_t *this, bool verify, chunk_t ike_sa_init, + chunk_t nonce, chunk_t int_auth, chunk_t secret, chunk_t ppk, + identification_t *id, char reserved[3], chunk_t *sig) { chunk_t skp_ppk = chunk_empty, key = chunk_empty, octets = chunk_empty; chunk_t key_pad; @@ -744,8 +745,8 @@ METHOD(keymat_v2_t, get_psk_sig, bool, secret = skp_ppk; } } - if (!get_auth_octets(this, verify, ike_sa_init, nonce, ppk, id, reserved, - &octets, NULL)) + if (!get_auth_octets(this, verify, ike_sa_init, nonce, int_auth, ppk, id, + reserved, &octets, NULL)) { goto failure; } diff --git a/src/libcharon/sa/ikev2/keymat_v2.h b/src/libcharon/sa/ikev2/keymat_v2.h index f2f0bec1c..353afb86c 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.h +++ b/src/libcharon/sa/ikev2/keymat_v2.h @@ -124,21 +124,23 @@ struct keymat_v2_t { * the get_psk_sig() method instead. * * @param verify TRUE to create for verification, FALSE to sign - * @param ike_sa_init encoded ike_sa_init message + * @param ike_sa_init encoded IKE_SA_INIT message * @param nonce nonce value + * @param int_auth concatenated data of IKE_INTERMEDIATE exchanges * @param ppk optional postquantum preshared key * @param id identity * @param reserved reserved bytes of id_payload - * @param octests chunk receiving allocated auth octets + * @param octets chunk receiving allocated auth octets * @param schemes array containing signature schemes * (signature_params_t*) in case they need to be * modified by the keymat implementation * @return TRUE if octets created successfully */ bool (*get_auth_octets)(keymat_v2_t *this, bool verify, chunk_t ike_sa_init, - chunk_t nonce, chunk_t ppk, identification_t *id, - char reserved[3], chunk_t *octets, - array_t *schemes); + chunk_t nonce, chunk_t int_auth, chunk_t ppk, + identification_t *id, char reserved[3], + chunk_t *octets, array_t *schemes); + /** * Build the shared secret signature used for PSK and EAP authentication. * @@ -147,8 +149,9 @@ struct keymat_v2_t { * used as secret (used for EAP methods without MSK). * * @param verify TRUE to create for verification, FALSE to sign - * @param ike_sa_init encoded ike_sa_init message + * @param ike_sa_init encoded IKE_SA_INIT message * @param nonce nonce value + * @param int_auth concatenated data of IKE_INTERMEDIATE exchanges * @param secret optional secret to include into signature * @param ppk optional postquantum preshared key * @param id identity @@ -157,8 +160,9 @@ struct keymat_v2_t { * @return TRUE if signature created successfully */ bool (*get_psk_sig)(keymat_v2_t *this, bool verify, chunk_t ike_sa_init, - chunk_t nonce, chunk_t secret, chunk_t ppk, - identification_t *id, char reserved[3], chunk_t *sig); + chunk_t nonce, chunk_t int_auth, chunk_t secret, + chunk_t ppk, identification_t *id, char reserved[3], + chunk_t *sig); /** * Add a hash algorithm supported by the peer for signature authentication. From 5c69262ce62ee7df5e6db4eaab9544557f9bedd7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 16:32:17 +0200 Subject: [PATCH 19/46] eap-authenticator: Handle IntAuth data --- .../ikev2/authenticators/eap_authenticator.c | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c b/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c index 2b109aef2..69e2ae0d7 100644 --- a/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c @@ -60,6 +60,11 @@ struct private_eap_authenticator_t { */ chunk_t sent_init; + /** + * IntAuth data to include in AUTH calculation + */ + chunk_t int_auth; + /** * Reserved bytes of ID payload */ @@ -495,8 +500,9 @@ static bool verify_auth(private_eap_authenticator_t *this, message_t *message, other_id = this->ike_sa->get_other_id(this->ike_sa); keymat = (keymat_v2_t*)this->ike_sa->get_keymat(this->ike_sa); - if (!keymat->get_psk_sig(keymat, TRUE, init, nonce, chunk_empty, this->msk, - this->ppk, other_id, this->reserved, &auth_data)) + if (!keymat->get_psk_sig(keymat, TRUE, init, nonce, this->int_auth, + this->msk, this->ppk, other_id, this->reserved, + &auth_data)) { return FALSE; } @@ -541,8 +547,9 @@ static bool build_auth(private_eap_authenticator_t *this, message_t *message, DBG1(DBG_IKE, "authentication of '%Y' (myself) with %N", my_id, auth_class_names, AUTH_CLASS_EAP); - if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, chunk_empty, this->msk, - this->ppk, my_id, this->reserved, &auth_data)) + if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, this->int_auth, + this->msk, this->ppk, my_id, this->reserved, + &auth_data)) { return FALSE; } @@ -554,7 +561,7 @@ static bool build_auth(private_eap_authenticator_t *this, message_t *message, if (this->no_ppk_auth) { - if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, chunk_empty, + if (!keymat->get_psk_sig(keymat, FALSE, init, nonce, this->int_auth, this->msk, chunk_empty, my_id, this->reserved, &auth_data)) { @@ -768,6 +775,12 @@ METHOD(authenticator_t, use_ppk, void, this->no_ppk_auth = no_ppk_auth; } +METHOD(authenticator_t, set_int_auth, void, + private_eap_authenticator_t *this, chunk_t int_auth) +{ + this->int_auth = int_auth; +} + METHOD(authenticator_t, destroy, void, private_eap_authenticator_t *this) { @@ -794,6 +807,7 @@ eap_authenticator_t *eap_authenticator_create_builder(ike_sa_t *ike_sa, .build = _build_client, .process = _process_client, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = _is_mutual, .destroy = _destroy, }, @@ -825,6 +839,7 @@ eap_authenticator_t *eap_authenticator_create_verifier(ike_sa_t *ike_sa, .build = _build_server, .process = _process_server, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = _is_mutual, .destroy = _destroy, }, From c4dac17d8cc942e2af935a64ccf2438184f48a34 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 16:36:13 +0200 Subject: [PATCH 20/46] psk-authenticator: Handle IntAuth data --- .../ikev2/authenticators/psk_authenticator.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c b/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c index 5e1cbb712..19acc0f95 100644 --- a/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c @@ -49,6 +49,11 @@ struct private_psk_authenticator_t { */ chunk_t ike_sa_init; + /** + * IntAuth data to include in AUTH calculation + */ + chunk_t int_auth; + /** * Reserved bytes of ID payload */ @@ -86,7 +91,7 @@ METHOD(authenticator_t, build, status_t, return NOT_FOUND; } if (!keymat->get_psk_sig(keymat, FALSE, this->ike_sa_init, this->nonce, - chunk_empty, key->get_key(key), this->ppk, + this->int_auth, key->get_key(key), this->ppk, my_id, this->reserved, &auth_data)) { key->destroy(key); @@ -103,7 +108,7 @@ METHOD(authenticator_t, build, status_t, if (this->no_ppk_auth) { if (!keymat->get_psk_sig(keymat, FALSE, this->ike_sa_init, this->nonce, - chunk_empty, key->get_key(key), chunk_empty, + this->int_auth, key->get_key(key), chunk_empty, my_id, this->reserved, &auth_data)) { DBG1(DBG_IKE, "failed adding NO_PPK_AUTH notify"); @@ -160,7 +165,7 @@ METHOD(authenticator_t, process, status_t, keys_found++; if (!keymat->get_psk_sig(keymat, TRUE, this->ike_sa_init, this->nonce, - chunk_empty, key->get_key(key), this->ppk, + this->int_auth, key->get_key(key), this->ppk, other_id, this->reserved, &auth_data)) { continue; @@ -199,6 +204,12 @@ METHOD(authenticator_t, use_ppk, void, this->no_ppk_auth = no_ppk_auth; } +METHOD(authenticator_t, set_int_auth, void, + private_psk_authenticator_t *this, chunk_t int_auth) +{ + this->int_auth = int_auth; +} + METHOD(authenticator_t, destroy, void, private_psk_authenticator_t *this) { @@ -220,6 +231,7 @@ psk_authenticator_t *psk_authenticator_create_builder(ike_sa_t *ike_sa, .build = _build, .process = (void*)return_failed, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = (void*)return_false, .destroy = _destroy, }, @@ -248,6 +260,7 @@ psk_authenticator_t *psk_authenticator_create_verifier(ike_sa_t *ike_sa, .build = (void*)return_failed, .process = _process, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = (void*)return_false, .destroy = _destroy, }, From 1212780b323392b3267bd250dd9d530eabce2816 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 16:38:01 +0200 Subject: [PATCH 21/46] pubkey-authenticator: Handle IntAuth data --- .../authenticators/pubkey_authenticator.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c b/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c index 7783d9274..3f53bc1b2 100644 --- a/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c +++ b/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c @@ -53,6 +53,11 @@ struct private_pubkey_authenticator_t { */ chunk_t ike_sa_init; + /** + * IntAuth data to include in AUTH calculation + */ + chunk_t int_auth; + /** * Reserved bytes of ID payload */ @@ -325,7 +330,7 @@ static status_t sign_signature_auth(private_pubkey_authenticator_t *this, } if (keymat->get_auth_octets(keymat, FALSE, this->ike_sa_init, this->nonce, - chunk_empty, this->ppk, id, this->reserved, + this->int_auth, this->ppk, id, this->reserved, &octets, schemes)) { enumerator = array_create_enumerator(schemes); @@ -348,7 +353,7 @@ static status_t sign_signature_auth(private_pubkey_authenticator_t *this, chunk_free(&octets); if (keymat->get_auth_octets(keymat, FALSE, this->ike_sa_init, - this->nonce, chunk_empty, + this->nonce, this->int_auth, chunk_empty, id, this->reserved, &octets, schemes) && private->sign(private, params->scheme, params->params, @@ -414,7 +419,7 @@ static bool get_auth_octets_scheme(private_pubkey_authenticator_t *this, keymat = (keymat_v2_t*)this->ike_sa->get_keymat(this->ike_sa); if (keymat->get_auth_octets(keymat, verify, this->ike_sa_init, this->nonce, - chunk_empty, ppk, id, this->reserved, octets, + this->int_auth, ppk, id, this->reserved, octets, schemes) && array_remove(schemes, 0, scheme)) { @@ -698,6 +703,12 @@ METHOD(authenticator_t, use_ppk, void, this->no_ppk_auth = no_ppk_auth; } +METHOD(authenticator_t, set_int_auth, void, + private_pubkey_authenticator_t *this, chunk_t int_auth) +{ + this->int_auth = int_auth; +} + METHOD(authenticator_t, destroy, void, private_pubkey_authenticator_t *this) { @@ -719,6 +730,7 @@ pubkey_authenticator_t *pubkey_authenticator_create_builder(ike_sa_t *ike_sa, .build = _build, .process = (void*)return_failed, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = (void*)return_false, .destroy = _destroy, }, @@ -747,6 +759,7 @@ pubkey_authenticator_t *pubkey_authenticator_create_verifier(ike_sa_t *ike_sa, .build = (void*)return_failed, .process = _process, .use_ppk = _use_ppk, + .set_int_auth = _set_int_auth, .is_mutual = (void*)return_false, .destroy = _destroy, }, From f6b2e6a21f96149db24b6dd8f388eea64c5c629a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 20 Aug 2019 17:07:55 +0200 Subject: [PATCH 22/46] ike-auth: Calculate and collect IntAuth for IKE_INTERMEDIATE exchanges The message ID of the first IKE_AUTH exchange is a safe-guard against potential truncation attacks if IKE_INTERMEDIATE exchanges are not used for multiple key exchanges but some other future use where the number of exchanges might not depend on the selected proposal. --- src/libcharon/sa/ikev2/tasks/ike_auth.c | 103 ++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/libcharon/sa/ikev2/tasks/ike_auth.c b/src/libcharon/sa/ikev2/tasks/ike_auth.c index 82ccd4fca..493cd5b83 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_auth.c +++ b/src/libcharon/sa/ikev2/tasks/ike_auth.c @@ -81,6 +81,21 @@ struct private_ike_auth_t { */ packet_t *other_packet; + /** + * IntAuth data from IKE_INTERMEDIATE exchanges: IntAuth_i | IntAuth_r | MID + */ + chunk_t int_auth; + + /** + * Pointer for IntAuth_i into int_auth + */ + chunk_t int_auth_i; + + /** + * Pointer for IntAuth_r into int_auth + */ + chunk_t int_auth_r; + /** * Reserved bytes of ID payload */ @@ -193,6 +208,61 @@ static status_t collect_other_init_data(private_ike_auth_t *this, return NEED_MORE; } +/** + * Collect IntAuth data for IKE_INTERMEDIATE exchanges. + */ +static status_t collect_int_auth_data(private_ike_auth_t *this, bool verify, + message_t *message) +{ + keymat_v2_t *keymat; + chunk_t int_auth_ap, prev = chunk_empty, int_auth; + + if (!message->get_plain(message, &int_auth_ap)) + { + return FAILED; + } + if (this->int_auth.len) + { + prev = this->initiator != verify ? this->int_auth_i : this->int_auth_r; + } + keymat = (keymat_v2_t*)this->ike_sa->get_keymat(this->ike_sa); + if (!keymat->get_int_auth(keymat, verify, int_auth_ap, prev, &int_auth)) + { + chunk_free(&int_auth_ap); + return FAILED; + } + chunk_free(&int_auth_ap); + + if (!this->int_auth.len) + { /* IntAuth consists of IntAuth_i | IntAuth_r | MID */ + this->int_auth = chunk_alloc(int_auth.len * 2 + sizeof(uint32_t)); + this->int_auth_i = chunk_create(this->int_auth.ptr, int_auth.len); + memset(this->int_auth.ptr, 0, this->int_auth.len); + prev = this->int_auth_i; + } + else if (!this->int_auth_r.len) + { + this->int_auth_r = chunk_create(this->int_auth.ptr + int_auth.len, + int_auth.len); + prev = this->int_auth_r; + } + memcpy(prev.ptr, int_auth.ptr, int_auth.len); + chunk_free(&int_auth); + return NEED_MORE; +} + +/** + * Set the MID in the IntAuth data to that of the first IKE_AUTH message. + */ +static void set_ike_auth_mid(private_ike_auth_t *this, message_t *message) +{ + if (this->int_auth.len) + { + htoun32(this->int_auth.ptr + this->int_auth.len - sizeof(uint32_t), + message->get_message_id(message)); + } +} + /** * Get and store reserved bytes of id_payload, required for AUTH payload */ @@ -662,6 +732,8 @@ METHOD(task_t, build_i, status_t, charon->bus->alert(charon->bus, ALERT_LOCAL_AUTH_FAILED); return FAILED; } + /* set MID in IntAuth data if used */ + set_ike_auth_mid(this, message); } if (!this->do_another_auth && !this->my_auth) @@ -733,6 +805,10 @@ METHOD(task_t, build_i, status_t, charon->bus->alert(charon->bus, ALERT_LOCAL_AUTH_FAILED); return FAILED; } + if (this->int_auth.ptr && this->my_auth->set_int_auth) + { + this->my_auth->set_int_auth(this->my_auth, this->int_auth); + } } /* for authentication methods that return NEED_MORE, the PPK will be reset * in process_i() for messages without PPK_ID notify, so we always set it @@ -784,6 +860,8 @@ METHOD(task_t, post_build_i, status_t, { case IKE_SA_INIT: return collect_my_init_data(this, message); + case IKE_INTERMEDIATE: + return collect_int_auth_data(this, FALSE, message); default: return NEED_MORE; } @@ -800,6 +878,8 @@ METHOD(task_t, process_r, status_t, { case IKE_SA_INIT: return collect_other_init_data(this, message); + case IKE_INTERMEDIATE: + return collect_int_auth_data(this, TRUE, message); case IKE_AUTH: break; default: @@ -841,6 +921,8 @@ METHOD(task_t, process_r, status_t, { this->initial_contact = TRUE; } + /* set MID in IntAuth data if used */ + set_ike_auth_mid(this, message); this->first_auth = TRUE; } @@ -912,6 +994,10 @@ METHOD(task_t, process_r, status_t, this->authentication_failed = TRUE; return NEED_MORE; } + if (this->int_auth.ptr && this->other_auth->set_int_auth) + { + this->other_auth->set_int_auth(this->other_auth, this->int_auth); + } } if (message->get_payload(message, PLV2_AUTH) && is_first_round(this, FALSE)) @@ -1097,6 +1183,10 @@ METHOD(task_t, build_r, status_t, { goto local_auth_failed; } + if (this->int_auth.ptr && this->my_auth->set_int_auth) + { + this->my_auth->set_int_auth(this->my_auth, this->int_auth); + } } } @@ -1219,6 +1309,8 @@ METHOD(task_t, post_build_r, status_t, { case IKE_SA_INIT: return collect_my_init_data(this, message); + case IKE_INTERMEDIATE: + return collect_int_auth_data(this, FALSE, message); default: return NEED_MORE; } @@ -1299,6 +1391,8 @@ METHOD(task_t, process_i, status_t, this->ike_sa->enable_extension(this->ike_sa, EXT_MULTIPLE_AUTH); } return collect_other_init_data(this, message); + case IKE_INTERMEDIATE: + return collect_int_auth_data(this, TRUE, message); case IKE_AUTH: break; default: @@ -1404,6 +1498,11 @@ METHOD(task_t, process_i, status_t, { goto peer_auth_failed; } + if (this->int_auth.ptr && this->other_auth->set_int_auth) + { + this->other_auth->set_int_auth(this->other_auth, + this->int_auth); + } } else { @@ -1558,6 +1657,9 @@ METHOD(task_t, migrate, void, clear_ppk(this); chunk_free(&this->my_nonce); chunk_free(&this->other_nonce); + chunk_free(&this->int_auth); + this->int_auth_i = chunk_empty; + this->int_auth_r = chunk_empty; DESTROY_IF(this->my_packet); DESTROY_IF(this->other_packet); DESTROY_IF(this->peer_cfg); @@ -1586,6 +1688,7 @@ METHOD(task_t, destroy, void, clear_ppk(this); chunk_free(&this->my_nonce); chunk_free(&this->other_nonce); + chunk_free(&this->int_auth); DESTROY_IF(this->my_packet); DESTROY_IF(this->other_packet); DESTROY_IF(this->my_auth); From ec0ec550708d143f02ef3e4740618823397cf471 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 28 Jun 2018 15:33:35 +0200 Subject: [PATCH 23/46] keymat_v2: Proper cleanup if derive_ike_keys() is called multiple times --- src/libcharon/sa/ikev2/keymat_v2.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libcharon/sa/ikev2/keymat_v2.c b/src/libcharon/sa/ikev2/keymat_v2.c index 69a70fc6f..b6807e911 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.c +++ b/src/libcharon/sa/ikev2/keymat_v2.c @@ -280,6 +280,8 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, ENCRYPTION_ALGORITHM); return FALSE; } + DESTROY_IF(this->aead_in); + DESTROY_IF(this->aead_out); if (!encryption_algorithm_is_aead(enc_alg)) { if (!proposal->get_algorithm(proposal, INTEGRITY_ALGORITHM, &int_alg, @@ -412,6 +414,7 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, { goto failure; } + chunk_clear(&this->skd); chunk_split(keymat, "ammmmaa", key_size, &this->skd, sk_ai.len, &sk_ai, sk_ar.len, &sk_ar, sk_ei.len, &sk_ei, sk_er.len, &sk_er, key_size, &sk_pi, key_size, &sk_pr); @@ -433,6 +436,8 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, /* SK_pi/SK_pr used for authentication => stored for later */ DBG4(DBG_IKE, "Sk_pi secret %B", &sk_pi); DBG4(DBG_IKE, "Sk_pr secret %B", &sk_pr); + chunk_clear(&this->skp_build); + chunk_clear(&this->skp_verify); if (this->initiator) { this->skp_build = sk_pi; From c36eaf42da92e83e9e190aa960b0431c6c3632f5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 9 Apr 2020 11:36:30 +0200 Subject: [PATCH 24/46] key-exchange: Add helper to concatenate shared secrets of several key exchanges --- src/libstrongswan/crypto/key_exchange.c | 42 ++++++++++++++++++++++++- src/libstrongswan/crypto/key_exchange.h | 15 ++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/libstrongswan/crypto/key_exchange.c b/src/libstrongswan/crypto/key_exchange.c index d672ec7e8..5b0113989 100644 --- a/src/libstrongswan/crypto/key_exchange.c +++ b/src/libstrongswan/crypto/key_exchange.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2019 Tobias Brunner + * Copyright (C) 2010-2020 Tobias Brunner * Copyright (C) 2005-2010 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -619,3 +619,43 @@ bool key_exchange_verify_pubkey(key_exchange_method_t ke, chunk_t value) } return valid; } + +/* + * Described in header + */ +bool key_exchange_concat_secrets(array_t *kes, chunk_t *first, + chunk_t *others) +{ + key_exchange_t *ke; + chunk_t secret; + int i; + + if (!array_count(kes)) + { + return FALSE; + } + *first = chunk_empty; + *others = chunk_empty; + for (i = 0; i < array_count(kes); i++) + { + if (array_get(kes, i, &ke) && + ke->get_shared_secret(ke, &secret)) + { + if (i == 0) + { + *first = secret; + } + else + { + *others = chunk_cat("ss", *others, secret); + } + } + else + { + chunk_clear(first); + chunk_clear(others); + return FALSE; + } + } + return TRUE; +} diff --git a/src/libstrongswan/crypto/key_exchange.h b/src/libstrongswan/crypto/key_exchange.h index 70d6d4993..73bf61f06 100644 --- a/src/libstrongswan/crypto/key_exchange.h +++ b/src/libstrongswan/crypto/key_exchange.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2019 Tobias Brunner + * Copyright (C) 2010-2020 Tobias Brunner * Copyright (C) 2005-2007 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -29,6 +29,7 @@ typedef struct key_exchange_t key_exchange_t; typedef struct diffie_hellman_params_t diffie_hellman_params_t; #include +#include /** * Key exchange method. @@ -209,4 +210,16 @@ bool key_exchange_is_ecdh(key_exchange_method_t ke); */ bool key_exchange_verify_pubkey(key_exchange_method_t ke, chunk_t value); +/** + * Return the first shared secret plus the concatenated additional shared + * secrets of all the key exchange methods in the given array. + * + * @param kes array of key_exchange_t* + * @param secret first shared secret (allocated) + * @param add_secret concatenated additional shared secrets (allocated) + * @return TRUE on success + */ +bool key_exchange_concat_secrets(array_t *kes, chunk_t *secret, + chunk_t *add_secret); + #endif /** KEY_EXCHANGE_H_ @}*/ From c14e4ab2a8b4b114ded9de9b4c62e02d045b00e0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 9 Apr 2020 11:37:52 +0200 Subject: [PATCH 25/46] keymat_v2: Support key derivation with multiple key exchanges --- src/charon-tkm/src/tkm/tkm_keymat.c | 17 +++++++--- src/charon-tkm/tests/keymat_tests.c | 11 +++++-- src/libcharon/plugins/ha/ha_dispatcher.c | 10 ++++-- src/libcharon/sa/ikev2/keymat_v2.c | 35 ++++++++++++--------- src/libcharon/sa/ikev2/keymat_v2.h | 10 +++--- src/libcharon/sa/ikev2/tasks/child_create.c | 8 ++++- src/libcharon/sa/ikev2/tasks/ike_init.c | 6 +++- 7 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/charon-tkm/src/tkm/tkm_keymat.c b/src/charon-tkm/src/tkm/tkm_keymat.c index 3d181ca6d..d8b800f05 100644 --- a/src/charon-tkm/src/tkm/tkm_keymat.c +++ b/src/charon-tkm/src/tkm/tkm_keymat.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2019 Tobias Brunner + * Copyright (C) 2015-2020 Tobias Brunner * Copyright (C) 2012 Reto Buerki * Copyright (C) 2012 Adrian-Ken Rueegsegger * @@ -95,13 +95,14 @@ METHOD(keymat_t, create_nonce_gen, nonce_gen_t*, } METHOD(keymat_v2_t, derive_ike_keys, bool, - private_tkm_keymat_t *this, proposal_t *proposal, key_exchange_t *ke, + private_tkm_keymat_t *this, proposal_t *proposal, array_t *kes, chunk_t nonce_i, chunk_t nonce_r, ike_sa_id_t *id, pseudo_random_function_t rekey_function, chunk_t rekey_skd) { uint64_t nc_id, spi_loc, spi_rem; chunk_t *nonce; tkm_diffie_hellman_t *tkm_dh; + key_exchange_t *ke; dh_id_type dh_id; nonce_type nonce_rem; result_type res; @@ -109,6 +110,12 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, icv_len_type icv_len; iv_len_type iv_len; + if (array_count(kes) != 1) + { + DBG1(DBG_IKE, "the TKM currently only supports a single key exchange"); + return FALSE; + } + /* Acquire nonce context id */ nonce = this->initiator ? &nonce_i : &nonce_r; nc_id = tkm->chunk_map->get_id(tkm->chunk_map, nonce); @@ -119,6 +126,7 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, } /* Get DH context id */ + array_get(kes, ARRAY_HEAD, &ke); tkm_dh = (tkm_diffie_hellman_t *)ke; dh_id = tkm_dh->get_id(tkm_dh); @@ -198,14 +206,15 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, } METHOD(keymat_v2_t, derive_child_keys, bool, - private_tkm_keymat_t *this, proposal_t *proposal, key_exchange_t *ke, + private_tkm_keymat_t *this, proposal_t *proposal, array_t *kes, chunk_t nonce_i, chunk_t nonce_r, chunk_t *encr_i, chunk_t *integ_i, chunk_t *encr_r, chunk_t *integ_r) { esa_info_t *esa_info_i, *esa_info_r; dh_id_type dh_id = 0; + key_exchange_t *ke; - if (ke) + if (kes && array_get(kes, ARRAY_HEAD, &ke)) { dh_id = ((tkm_diffie_hellman_t *)ke)->get_id((tkm_diffie_hellman_t *)ke); } diff --git a/src/charon-tkm/tests/keymat_tests.c b/src/charon-tkm/tests/keymat_tests.c index e5d6280e7..56c8b6809 100644 --- a/src/charon-tkm/tests/keymat_tests.c +++ b/src/charon-tkm/tests/keymat_tests.c @@ -55,9 +55,12 @@ START_TEST(test_derive_ike_keys) ck_assert(dh->ke.get_public_key(&dh->ke, &pubvalue)); ck_assert(dh->ke.set_public_key(&dh->ke, pubvalue)); + array_t *kes = NULL; + array_insert_create(&kes, ARRAY_TAIL, dh); fail_unless(keymat->keymat_v2.derive_ike_keys(&keymat->keymat_v2, proposal, - &dh->ke, nonce, nonce, ike_sa_id, PRF_UNDEFINED, chunk_empty), + kes, nonce, nonce, ike_sa_id, PRF_UNDEFINED, chunk_empty), "Key derivation failed"); + array_destroy(kes); chunk_free(&nonce); aead_t * const aead = keymat->keymat_v2.keymat.get_aead(&keymat->keymat_v2.keymat, TRUE); @@ -92,11 +95,13 @@ START_TEST(test_derive_child_keys) chunk_t encr_i, encr_r, integ_i, integ_r; chunk_t nonce = chunk_from_chars("test chunk"); + array_t *kes = NULL; + array_insert_create(&kes, ARRAY_TAIL, dh); fail_unless(keymat->keymat_v2.derive_child_keys(&keymat->keymat_v2, proposal, - &dh->ke, - nonce, nonce, &encr_i, + kes, nonce, nonce, &encr_i, &integ_i, &encr_r, &integ_r), "Child key derivation failed"); + array_destroy(kes); esa_info_t *info = (esa_info_t *)encr_i.ptr; fail_if(!info, "encr_i does not contain esa information"); diff --git a/src/libcharon/plugins/ha/ha_dispatcher.c b/src/libcharon/plugins/ha/ha_dispatcher.c index 08a348b61..b0ec3d9cf 100644 --- a/src/libcharon/plugins/ha/ha_dispatcher.c +++ b/src/libcharon/plugins/ha/ha_dispatcher.c @@ -234,9 +234,12 @@ static void process_ike_add(private_ha_dispatcher_t *this, ha_message_t *message if (ike_sa->get_version(ike_sa) == IKEV2) { keymat_v2_t *keymat_v2 = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); + array_t *kes = NULL; - ok = keymat_v2->derive_ike_keys(keymat_v2, proposal, dh, nonce_i, + array_insert_create(&kes, ARRAY_HEAD, dh); + ok = keymat_v2->derive_ike_keys(keymat_v2, proposal, kes, nonce_i, nonce_r, ike_sa->get_id(ike_sa), old_prf, old_skd); + array_destroy(kes); } if (ike_sa->get_version(ike_sa) == IKEV1) { @@ -662,6 +665,7 @@ static void process_child_add(private_ha_dispatcher_t *this, chunk_t encr_i, integ_i, encr_r, integ_r; linked_list_t *local_ts, *remote_ts; key_exchange_t *dh = NULL; + array_t *kes = NULL; enumerator = message->create_attribute_enumerator(message); while (enumerator->enumerate(enumerator, &attribute, &value)) @@ -767,12 +771,13 @@ static void process_child_add(private_ha_dispatcher_t *this, if (secret.len) { dh = ha_diffie_hellman_create(secret, chunk_empty); + array_insert_create(&kes, ARRAY_HEAD, dh); } if (ike_sa->get_version(ike_sa) == IKEV2) { keymat_v2_t *keymat_v2 = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); - ok = keymat_v2->derive_child_keys(keymat_v2, proposal, dh, + ok = keymat_v2->derive_child_keys(keymat_v2, proposal, kes, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r); } if (ike_sa->get_version(ike_sa) == IKEV1) @@ -786,6 +791,7 @@ static void process_child_add(private_ha_dispatcher_t *this, ok = keymat_v1->derive_child_keys(keymat_v1, proposal, dh, spi_i, spi_r, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r); } + array_destroy(kes); DESTROY_IF(dh); if (!ok) { diff --git a/src/libcharon/sa/ikev2/keymat_v2.c b/src/libcharon/sa/ikev2/keymat_v2.c index b6807e911..08fcdedc7 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.c +++ b/src/libcharon/sa/ikev2/keymat_v2.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2019 Tobias Brunner + * Copyright (C) 2015-2020 Tobias Brunner * Copyright (C) 2008 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -237,13 +237,13 @@ static bool set_aead_keys(private_keymat_v2_t *this, uint16_t enc_alg, } METHOD(keymat_v2_t, derive_ike_keys, bool, - private_keymat_v2_t *this, proposal_t *proposal, key_exchange_t *dh, + private_keymat_v2_t *this, proposal_t *proposal, array_t *kes, chunk_t nonce_i, chunk_t nonce_r, ike_sa_id_t *id, pseudo_random_function_t rekey_function, chunk_t rekey_skd) { - chunk_t skeyseed = chunk_empty, secret, full_nonce, fixed_nonce; - chunk_t prf_plus_seed, spi_i, spi_r, keymat = chunk_empty; - chunk_t sk_ei = chunk_empty, sk_er = chunk_empty; + chunk_t skeyseed = chunk_empty, secret, add_secret = chunk_empty; + chunk_t full_nonce, fixed_nonce, prf_plus_seed, spi_i, spi_r; + chunk_t keymat = chunk_empty, sk_ei = chunk_empty, sk_er = chunk_empty; chunk_t sk_ai = chunk_empty, sk_ar = chunk_empty, sk_pi, sk_pr; kdf_t *prf = NULL, *prf_plus = NULL; uint16_t prf_alg, key_size, enc_alg, enc_size, int_alg; @@ -302,13 +302,15 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, return FALSE; } - if (!dh->get_shared_secret(dh, &secret)) + if (!key_exchange_concat_secrets(kes, &secret, &add_secret)) { return FALSE; } - DBG4(DBG_IKE, "shared Diffie Hellman secret %B", &secret); + DBG4(DBG_IKE, "key exchange secret %B", &secret); + DBG4(DBG_IKE, "additional key exchange secret %B", &add_secret); /* full nonce is used as seed for PRF+ ... */ full_nonce = chunk_cat("cc", nonce_i, nonce_r); + DBG4(DBG_IKE, "nonces %B", &full_nonce); /* but the PRF may need a fixed key which only uses the first bytes of * the nonces. */ switch (prf_alg) @@ -342,6 +344,7 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, key_derivation_function_names, KDF_PRF, pseudo_random_function_names, this->prf_alg); chunk_clear(&secret); + chunk_clear(&add_secret); chunk_free(&full_nonce); chunk_free(&fixed_nonce); return FALSE; @@ -365,11 +368,12 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, key_derivation_function_names, KDF_PRF, pseudo_random_function_names, rekey_function); chunk_clear(&secret); + chunk_clear(&add_secret); chunk_free(&full_nonce); chunk_free(&fixed_nonce); return FALSE; } - secret = chunk_cat("sc", secret, full_nonce); + secret = chunk_cat("scc", secret, full_nonce, add_secret); if (prf->set_param(prf, KDF_PARAM_KEY, secret) && prf->set_param(prf, KDF_PARAM_SALT, rekey_skd) && prf->allocate_bytes(prf, 0, &skeyseed)) @@ -380,6 +384,7 @@ METHOD(keymat_v2_t, derive_ike_keys, bool, } DBG4(DBG_IKE, "SKEYSEED %B", &skeyseed); chunk_clear(&secret); + chunk_clear(&add_secret); chunk_free(&fixed_nonce); DESTROY_IF(prf); @@ -529,12 +534,13 @@ METHOD(keymat_v2_t, derive_ike_keys_ppk, bool, } METHOD(keymat_v2_t, derive_child_keys, bool, - private_keymat_v2_t *this, proposal_t *proposal, key_exchange_t *dh, + private_keymat_v2_t *this, proposal_t *proposal, array_t *kes, chunk_t nonce_i, chunk_t nonce_r, chunk_t *encr_i, chunk_t *integ_i, chunk_t *encr_r, chunk_t *integ_r) { uint16_t enc_alg, int_alg, enc_size = 0, int_size = 0; - chunk_t seed, secret = chunk_empty, keymat = chunk_empty; + chunk_t seed, secret = chunk_empty, add_secret = chunk_empty; + chunk_t keymat = chunk_empty; kdf_t *prf_plus; if (proposal->get_algorithm(proposal, ENCRYPTION_ALGORITHM, @@ -601,15 +607,16 @@ METHOD(keymat_v2_t, derive_child_keys, bool, int_size /= 8; } - if (dh) + if (kes) { - if (!dh->get_shared_secret(dh, &secret)) + if (!key_exchange_concat_secrets(kes, &secret, &add_secret)) { return FALSE; } - DBG4(DBG_CHD, "DH secret %B", &secret); + DBG4(DBG_CHD, "key exchange secret %B", &secret); + DBG4(DBG_CHD, "additional key exchange secret %B", &add_secret); } - seed = chunk_cata("scc", secret, nonce_i, nonce_r); + seed = chunk_cata("sccs", secret, nonce_i, nonce_r, add_secret); DBG4(DBG_CHD, "seed %B", &seed); prf_plus = lib->crypto->create_kdf(lib->crypto, KDF_PRF_PLUS, this->prf_alg); diff --git a/src/libcharon/sa/ikev2/keymat_v2.h b/src/libcharon/sa/ikev2/keymat_v2.h index 353afb86c..4fcc20d58 100644 --- a/src/libcharon/sa/ikev2/keymat_v2.h +++ b/src/libcharon/sa/ikev2/keymat_v2.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2019 Tobias Brunner + * Copyright (C) 2011-2020 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -44,7 +44,7 @@ struct keymat_v2_t { * crypters and authentication functions. * * @param proposal selected algorithms - * @param dh diffie hellman key allocated by create_ke() + * @param kes array of key_exchange_t* created by create_ke() * @param nonce_i initiators nonce value * @param nonce_r responders nonce value * @param id IKE_SA identifier @@ -53,7 +53,7 @@ struct keymat_v2_t { * @return TRUE on success */ bool (*derive_ike_keys)(keymat_v2_t *this, proposal_t *proposal, - key_exchange_t *dh, chunk_t nonce_i, + array_t *kes, chunk_t nonce_i, chunk_t nonce_r, ike_sa_id_t *id, pseudo_random_function_t rekey_function, chunk_t rekey_skd); @@ -77,7 +77,7 @@ struct keymat_v2_t { * If no PFS is used for the CHILD_SA, dh can be NULL. * * @param proposal selected algorithms - * @param dh diffie hellman key allocated by create_ke(), or NULL + * @param kes array of key_exchange_t* created by create_ke(), or NULL * @param nonce_i initiators nonce value * @param nonce_r responders nonce value * @param encr_i chunk to write initiators encryption key to @@ -87,7 +87,7 @@ struct keymat_v2_t { * @return TRUE on success */ bool (*derive_child_keys)(keymat_v2_t *this, - proposal_t *proposal, key_exchange_t *dh, + proposal_t *proposal, array_t *kes, chunk_t nonce_i, chunk_t nonce_r, chunk_t *encr_i, chunk_t *integ_i, chunk_t *encr_r, chunk_t *integ_r); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 3e80a7d2d..39632e786 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -500,6 +500,7 @@ static status_t select_and_install(private_child_create_t *this, chunk_t integ_i = chunk_empty, integ_r = chunk_empty; linked_list_t *my_ts, *other_ts; host_t *me, *other; + array_t *kes = NULL; proposal_selection_flag_t flags = 0; if (this->proposals == NULL) @@ -690,8 +691,12 @@ static status_t select_and_install(private_child_create_t *this, this->ipcomp = IPCOMP_NONE; } status_i = status_o = FAILED; + if (this->dh) + { + array_insert_create(&kes, ARRAY_HEAD, this->dh); + } if (this->keymat->derive_child_keys(this->keymat, this->proposal, - this->dh, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r)) + kes, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r)) { if (this->initiator) { @@ -769,6 +774,7 @@ static status_t select_and_install(private_child_create_t *this, chunk_clear(&integ_r); chunk_clear(&encr_i); chunk_clear(&encr_r); + array_destroy(kes); if (status != SUCCESS) { diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index b84c2832a..2ef721ddc 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -802,6 +802,7 @@ static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i, pseudo_random_function_t prf_alg = PRF_UNDEFINED; chunk_t skd = chunk_empty; ike_sa_id_t *id; + array_t *kes; id = this->ike_sa->get_id(this->ike_sa); if (this->old_sa) @@ -810,11 +811,14 @@ static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i, old_keymat = (keymat_v2_t*)this->old_sa->get_keymat(this->old_sa); prf_alg = old_keymat->get_skd(old_keymat, &skd); } - if (!this->keymat->derive_ike_keys(this->keymat, this->proposal, this->dh, + array_insert_create(&kes, ARRAY_HEAD, this->dh); + if (!this->keymat->derive_ike_keys(this->keymat, this->proposal, kes, nonce_i, nonce_r, id, prf_alg, skd)) { + array_destroy(kes); return FALSE; } + array_destroy(kes); charon->bus->ike_keys(charon->bus, this->ike_sa, this->dh, chunk_empty, nonce_i, nonce_r, this->old_sa, NULL, AUTH_NONE); return TRUE; From eff0c43a17dcd6cc04ac3f9ba36a4bc42e96a7ef Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 9 Apr 2020 11:53:45 +0200 Subject: [PATCH 26/46] bus: Support multiple key exchanges in ike/child_keys() events --- src/libcharon/bus/bus.c | 8 ++++---- src/libcharon/bus/bus.h | 9 +++++---- src/libcharon/bus/listeners/listener.h | 8 ++++---- src/libcharon/plugins/ha/ha_child.c | 7 ++++--- src/libcharon/plugins/ha/ha_ike.c | 13 +++++++++---- src/libcharon/sa/ikev1/phase1.c | 5 ++++- src/libcharon/sa/ikev1/tasks/quick_mode.c | 8 +++++++- src/libcharon/sa/ikev2/tasks/child_create.c | 5 ++--- src/libcharon/sa/ikev2/tasks/ike_init.c | 6 +++--- 9 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/libcharon/bus/bus.c b/src/libcharon/bus/bus.c index 57eaaac89..99387d5e0 100644 --- a/src/libcharon/bus/bus.c +++ b/src/libcharon/bus/bus.c @@ -574,7 +574,7 @@ METHOD(bus_t, message, void, } METHOD(bus_t, ike_keys, void, - private_bus_t *this, ike_sa_t *ike_sa, key_exchange_t *dh, + private_bus_t *this, ike_sa_t *ike_sa, array_t *kes, chunk_t dh_other, chunk_t nonce_i, chunk_t nonce_r, ike_sa_t *rekey, shared_key_t *shared, auth_method_t method) { @@ -591,7 +591,7 @@ METHOD(bus_t, ike_keys, void, continue; } entry->calling++; - keep = entry->listener->ike_keys(entry->listener, ike_sa, dh, dh_other, + keep = entry->listener->ike_keys(entry->listener, ike_sa, kes, dh_other, nonce_i, nonce_r, rekey, shared, method); entry->calling--; @@ -639,7 +639,7 @@ METHOD(bus_t, ike_derived_keys, void, METHOD(bus_t, child_keys, void, private_bus_t *this, child_sa_t *child_sa, bool initiator, - key_exchange_t *dh, chunk_t nonce_i, chunk_t nonce_r) + array_t *kes, chunk_t nonce_i, chunk_t nonce_r) { enumerator_t *enumerator; ike_sa_t *ike_sa; @@ -658,7 +658,7 @@ METHOD(bus_t, child_keys, void, } entry->calling++; keep = entry->listener->child_keys(entry->listener, ike_sa, - child_sa, initiator, dh, nonce_i, nonce_r); + child_sa, initiator, kes, nonce_i, nonce_r); entry->calling--; if (!keep) { diff --git a/src/libcharon/bus/bus.h b/src/libcharon/bus/bus.h index 5e809f04f..d814d09e3 100644 --- a/src/libcharon/bus/bus.h +++ b/src/libcharon/bus/bus.h @@ -30,6 +30,7 @@ typedef struct bus_t bus_t; #include #include +#include #include #include #include @@ -348,7 +349,7 @@ struct bus_t { * IKE_SA keymat hook. * * @param ike_sa IKE_SA this keymat belongs to - * @param dh diffie hellman shared secret + * @param kes array of key_exchange_t* * @param dh_other others DH public value (IKEv1 only) * @param nonce_i initiator's nonce * @param nonce_r responder's nonce @@ -356,7 +357,7 @@ struct bus_t { * @param shared shared key used for key derivation (IKEv1-PSK only) * @param method auth method for key derivation (IKEv1-non-PSK only) */ - void (*ike_keys)(bus_t *this, ike_sa_t *ike_sa, key_exchange_t *dh, + void (*ike_keys)(bus_t *this, ike_sa_t *ike_sa, array_t *kes, chunk_t dh_other, chunk_t nonce_i, chunk_t nonce_r, ike_sa_t *rekey, shared_key_t *shared, auth_method_t method); @@ -381,12 +382,12 @@ struct bus_t { * * @param child_sa CHILD_SA this keymat is used for * @param initiator initiator of the CREATE_CHILD_SA exchange - * @param dh diffie hellman shared secret + * @param kes array of key_exchange_t*, or NULL * @param nonce_i initiator's nonce * @param nonce_r responder's nonce */ void (*child_keys)(bus_t *this, child_sa_t *child_sa, bool initiator, - key_exchange_t *dh, chunk_t nonce_i, chunk_t nonce_r); + array_t *kes, chunk_t nonce_i, chunk_t nonce_r); /** * CHILD_SA derived keys hook. diff --git a/src/libcharon/bus/listeners/listener.h b/src/libcharon/bus/listeners/listener.h index bec48d187..42297e2f4 100644 --- a/src/libcharon/bus/listeners/listener.h +++ b/src/libcharon/bus/listeners/listener.h @@ -83,7 +83,7 @@ struct listener_t { * Hook called with IKE_SA key material. * * @param ike_sa IKE_SA this keymat belongs to - * @param dh diffie hellman shared secret + * @param kes array of key_exchange_t* * @param dh_other others DH public value (IKEv1 only) * @param nonce_i initiator's nonce * @param nonce_r responder's nonce @@ -92,7 +92,7 @@ struct listener_t { * @param method auth method for key derivation (IKEv1-non-PSK only) * @return TRUE to stay registered, FALSE to unregister */ - bool (*ike_keys)(listener_t *this, ike_sa_t *ike_sa, key_exchange_t *dh, + bool (*ike_keys)(listener_t *this, ike_sa_t *ike_sa, array_t *kes, chunk_t dh_other, chunk_t nonce_i, chunk_t nonce_r, ike_sa_t *rekey, shared_key_t *shared, auth_method_t method); @@ -119,13 +119,13 @@ struct listener_t { * @param ike_sa IKE_SA the child sa belongs to * @param child_sa CHILD_SA this keymat is used for * @param initiator initiator of the CREATE_CHILD_SA exchange - * @param dh diffie hellman shared secret + * @param kes array of key_exchange_t*, or NULL * @param nonce_i initiator's nonce * @param nonce_r responder's nonce * @return TRUE to stay registered, FALSE to unregister */ bool (*child_keys)(listener_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa, - bool initiator, key_exchange_t *dh, + bool initiator, array_t *kes, chunk_t nonce_i, chunk_t nonce_r); /** diff --git a/src/libcharon/plugins/ha/ha_child.c b/src/libcharon/plugins/ha/ha_child.c index 1081986dd..364fe1d5f 100644 --- a/src/libcharon/plugins/ha/ha_child.c +++ b/src/libcharon/plugins/ha/ha_child.c @@ -51,10 +51,10 @@ struct private_ha_child_t { METHOD(listener_t, child_keys, bool, private_ha_child_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa, - bool initiator, key_exchange_t *dh, chunk_t nonce_i, chunk_t nonce_r) + bool initiator, array_t *kes, chunk_t nonce_i, chunk_t nonce_r) { ha_message_t *m; - chunk_t secret; + chunk_t secret, add_secret = chunk_empty; proposal_t *proposal; uint16_t alg, len; linked_list_t *local_ts, *remote_ts; @@ -101,10 +101,11 @@ METHOD(listener_t, child_keys, bool, } m->add_attribute(m, HA_NONCE_I, nonce_i); m->add_attribute(m, HA_NONCE_R, nonce_r); - if (dh && dh->get_shared_secret(dh, &secret)) + if (kes && key_exchange_concat_secrets(kes, &secret, &add_secret)) { m->add_attribute(m, HA_SECRET, secret); chunk_clear(&secret); + chunk_clear(&add_secret); } local_ts = linked_list_create(); diff --git a/src/libcharon/plugins/ha/ha_ike.c b/src/libcharon/plugins/ha/ha_ike.c index 6535e4a1e..e6dab8457 100644 --- a/src/libcharon/plugins/ha/ha_ike.c +++ b/src/libcharon/plugins/ha/ha_ike.c @@ -82,12 +82,13 @@ static void copy_extensions(ha_message_t *m, ike_sa_t *ike_sa) } METHOD(listener_t, ike_keys, bool, - private_ha_ike_t *this, ike_sa_t *ike_sa, key_exchange_t *dh, + private_ha_ike_t *this, ike_sa_t *ike_sa, array_t *kes, chunk_t dh_other, chunk_t nonce_i, chunk_t nonce_r, ike_sa_t *rekey, shared_key_t *shared, auth_method_t method) { ha_message_t *m; - chunk_t secret; + key_exchange_t *ke; + chunk_t secret = chunk_empty, add_secret = chunk_empty; proposal_t *proposal; uint16_t alg, len; @@ -95,8 +96,12 @@ METHOD(listener_t, ike_keys, bool, { /* do not sync SA between nodes */ return TRUE; } - if (!dh->get_shared_secret(dh, &secret)) + if (!key_exchange_concat_secrets(kes, &secret, &add_secret) || + !array_get(kes, ARRAY_HEAD, &ke) || + add_secret.len > 0) { + chunk_clear(&secret); + chunk_clear(&add_secret); return TRUE; } @@ -142,7 +147,7 @@ METHOD(listener_t, ike_keys, bool, chunk_clear(&secret); if (ike_sa->get_version(ike_sa) == IKEV1) { - if (dh->get_public_key(dh, &secret)) + if (ke->get_public_key(ke, &secret)) { m->add_attribute(m, HA_LOCAL_DH, secret); chunk_free(&secret); diff --git a/src/libcharon/sa/ikev1/phase1.c b/src/libcharon/sa/ikev1/phase1.c index 2520b6a93..61c400580 100644 --- a/src/libcharon/sa/ikev1/phase1.c +++ b/src/libcharon/sa/ikev1/phase1.c @@ -220,6 +220,7 @@ METHOD(phase1_t, derive_keys, bool, private_phase1_t *this, peer_cfg_t *peer_cfg, auth_method_t method) { shared_key_t *shared_key = NULL; + array_t *kes = NULL; switch (method) { @@ -245,9 +246,11 @@ METHOD(phase1_t, derive_keys, bool, DBG1(DBG_IKE, "key derivation for %N failed", auth_method_names, method); return FALSE; } - charon->bus->ike_keys(charon->bus, this->ike_sa, this->dh, this->dh_value, + array_insert_create(&kes, ARRAY_HEAD, this->dh); + charon->bus->ike_keys(charon->bus, this->ike_sa, kes, this->dh_value, this->nonce_i, this->nonce_r, NULL, shared_key, method); + array_destroy(kes); DESTROY_IF(shared_key); return TRUE; } diff --git a/src/libcharon/sa/ikev1/tasks/quick_mode.c b/src/libcharon/sa/ikev1/tasks/quick_mode.c index 3612f0d6a..8436c66dc 100644 --- a/src/libcharon/sa/ikev1/tasks/quick_mode.c +++ b/src/libcharon/sa/ikev1/tasks/quick_mode.c @@ -270,6 +270,7 @@ static bool install(private_quick_mode_t *this) chunk_t encr_i, encr_r, integ_i, integ_r; linked_list_t *tsi, *tsr, *my_ts, *other_ts; child_sa_t *old = NULL; + array_t *kes = NULL; this->child_sa->set_proposal(this->child_sa, this->proposal); this->child_sa->set_state(this->child_sa, CHILD_INSTALLING); @@ -377,8 +378,13 @@ static bool install(private_quick_mode_t *this) return FALSE; } + if (this->dh) + { + array_insert_create(&kes, ARRAY_HEAD, this->dh); + } charon->bus->child_keys(charon->bus, this->child_sa, this->initiator, - this->dh, this->nonce_i, this->nonce_r); + kes, this->nonce_i, this->nonce_r); + array_destroy(kes); my_ts = linked_list_create_from_enumerator( this->child_sa->create_ts_enumerator(this->child_sa, TRUE)); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 39632e786..acc1a928f 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -768,6 +768,8 @@ static status_t select_and_install(private_child_create_t *this, charon->bus->child_derived_keys(charon->bus, this->child_sa, this->initiator, encr_i, encr_r, integ_i, integ_r); + charon->bus->child_keys(charon->bus, this->child_sa, + this->initiator, kes, nonce_i, nonce_r); } } chunk_clear(&integ_i); @@ -781,9 +783,6 @@ static status_t select_and_install(private_child_create_t *this, return status; } - charon->bus->child_keys(charon->bus, this->child_sa, this->initiator, - this->dh, nonce_i, nonce_r); - #if DEBUG_LEVEL >= 0 child_sa_outbound_state_t out_state; diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index 2ef721ddc..8ace41609 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -802,7 +802,7 @@ static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i, pseudo_random_function_t prf_alg = PRF_UNDEFINED; chunk_t skd = chunk_empty; ike_sa_id_t *id; - array_t *kes; + array_t *kes = NULL; id = this->ike_sa->get_id(this->ike_sa); if (this->old_sa) @@ -818,9 +818,9 @@ static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i, array_destroy(kes); return FALSE; } - array_destroy(kes); - charon->bus->ike_keys(charon->bus, this->ike_sa, this->dh, chunk_empty, + charon->bus->ike_keys(charon->bus, this->ike_sa, kes, chunk_empty, nonce_i, nonce_r, this->old_sa, NULL, AUTH_NONE); + array_destroy(kes); return TRUE; } From 0d49ddec2ef5ef8170348e9ee906ed2016f5a156 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 31 Oct 2019 17:16:44 +0100 Subject: [PATCH 27/46] ike-init: Add support for multiple key exchanges Initially, this is handled with a key derivation for each IKE_INTERMEDIATE exchange. When rekeying, the keys are derived only once all IKE_FOLLOWUP_KE exchanges are done. --- src/libcharon/sa/ikev2/tasks/ike_init.c | 523 ++++++++++++++++++------ 1 file changed, 397 insertions(+), 126 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index 8ace41609..4eda3a062 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-2019 Tobias Brunner + * Copyright (C) 2008-2020 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -30,9 +30,13 @@ #include #include -/** maximum retries to do with cookies/other dh groups */ +/** maximum retries to do with cookies/other ke methods */ #define MAX_RETRIES 5 +/** maximum number of key exchanges (including the initial one) */ +#define MAX_KEY_EXCHANGES (ADDITIONAL_KEY_EXCHANGE_7 - \ + ADDITIONAL_KEY_EXCHANGE_1 + 2) + typedef struct private_ike_init_t private_ike_init_t; /** @@ -56,29 +60,39 @@ struct private_ike_init_t { bool initiator; /** - * Whether the key exchange is done + * Key exchanges to perform */ - bool ke_done; + struct { + transform_type_t type; + key_exchange_method_t method; + bool done; + bool derived; + } key_exchanges[MAX_KEY_EXCHANGES]; /** - * Whether keys have already been derived + * Current key exchange */ - bool ke_derived; + int ke_index; /** - * diffie hellman group to use + * Key exchange method from the parsed or sent KE payload */ - key_exchange_method_t dh_group; + key_exchange_method_t ke_method; /** - * diffie hellman key exchange + * Current key exchange object */ - key_exchange_t *dh; + key_exchange_t *ke; /** - * Applying DH public value failed? + * All key exchanges performed during rekeying (key_exchange_t) */ - bool dh_failed; + array_t *kes; + + /** + * Applying KE public key failed? + */ + bool ke_failed; /** * Keymat derivation (from IKE_SA) @@ -86,17 +100,17 @@ struct private_ike_init_t { keymat_v2_t *keymat; /** - * nonce chosen by us + * Nonce chosen by us */ chunk_t my_nonce; /** - * nonce chosen by peer + * Nonce chosen by peer */ chunk_t other_nonce; /** - * nonce generator + * Nonce generator */ nonce_gen_t *nonceg; @@ -106,17 +120,17 @@ struct private_ike_init_t { proposal_t *proposal; /** - * Old IKE_SA which gets rekeyed + * Old IKE_SA that gets rekeyed */ ike_sa_t *old_sa; /** - * cookie received from responder + * Cookie received from responder */ chunk_t cookie; /** - * retries done so far after failure (cookie or bad dh group) + * Retries done so far after failure (cookie or bad KE method) */ u_int retry; @@ -131,6 +145,15 @@ struct private_ike_init_t { bool follow_redirects; }; +/** + * Returns the exchange type for additional exchanges when using multiple key + * exchanges, depending on whether this happens initially or during a rekeying + */ +static exchange_type_t exchange_type_multi_ke(private_ike_init_t *this) +{ + return this->old_sa ? IKE_FOLLOWUP_KE : IKE_INTERMEDIATE; +} + /** * Allocate our own nonce value */ @@ -320,7 +343,7 @@ static bool build_payloads(private_ike_init_t *this, 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; + linked_list_t *proposal_list, *other_ke_methods; ike_sa_id_t *id; proposal_t *proposal; enumerator_t *enumerator; @@ -333,7 +356,7 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) if (this->initiator) { proposal_list = ike_cfg->get_proposals(ike_cfg); - other_dh_groups = linked_list_create(); + other_ke_methods = linked_list_create(); enumerator = proposal_list->create_enumerator(proposal_list); while (enumerator->enumerate(enumerator, (void**)&proposal)) { @@ -342,23 +365,23 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) { proposal->set_spi(proposal, id->get_initiator_spi(id)); } - /* move the selected DH group to the front of the proposal */ + /* move the selected KE method to the front of the proposal */ if (!proposal->promote_transform(proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) + this->ke_method)) { /* the proposal does not include the group, move to the back */ proposal_list->remove_at(proposal_list, enumerator); - other_dh_groups->insert_last(other_dh_groups, proposal); + other_ke_methods->insert_last(other_ke_methods, proposal); } } enumerator->destroy(enumerator); /* add proposals that don't contain the selected group */ - enumerator = other_dh_groups->create_enumerator(other_dh_groups); + enumerator = other_ke_methods->create_enumerator(other_ke_methods); while (enumerator->enumerate(enumerator, (void**)&proposal)) { /* no need to remove from the list as we destroy it anyway*/ proposal_list->insert_last(proposal_list, proposal); } enumerator->destroy(enumerator); - other_dh_groups->destroy(other_dh_groups); + other_ke_methods->destroy(other_ke_methods); sa_payload = sa_payload_create_from_proposals_v2(proposal_list); proposal_list->destroy_offset(proposal_list, offsetof(proposal_t, destroy)); @@ -375,25 +398,17 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) message->add_payload(message, (payload_t*)sa_payload); ke_payload = ke_payload_create_from_key_exchange(PLV2_KEY_EXCHANGE, - this->dh); + this->ke); if (!ke_payload) { DBG1(DBG_IKE, "creating KE payload failed"); return FALSE; } + message->add_payload(message, (payload_t*)ke_payload); + nonce_payload = nonce_payload_create(PLV2_NONCE); nonce_payload->set_nonce(nonce_payload, this->my_nonce); - - if (this->old_sa) - { /* payload order differs if we are rekeying */ - message->add_payload(message, (payload_t*)nonce_payload); - message->add_payload(message, (payload_t*)ke_payload); - } - else - { - message->add_payload(message, (payload_t*)ke_payload); - message->add_payload(message, (payload_t*)nonce_payload); - } + message->add_payload(message, (payload_t*)nonce_payload); /* negotiate fragmentation if we are not rekeying */ if (!this->old_sa && @@ -523,6 +538,106 @@ static void process_sa_payload(private_ike_init_t *this, message_t *message, offsetof(proposal_t, destroy)); } +/** + * Collect all key exchanges from the proposal + */ +static void determine_key_exchanges(private_ike_init_t *this) +{ + transform_type_t t = KEY_EXCHANGE_METHOD; + uint16_t alg; + int i = 1; + + this->proposal->get_algorithm(this->proposal, t, &alg, NULL); + this->key_exchanges[0].type = t; + this->key_exchanges[0].method = alg; + + for (t = ADDITIONAL_KEY_EXCHANGE_1; t <= ADDITIONAL_KEY_EXCHANGE_7; t++) + { + if (this->proposal->get_algorithm(this->proposal, t, &alg, NULL)) + { + this->key_exchanges[i].type = t; + this->key_exchanges[i].method = alg; + i++; + } + } +} + +/** + * Check if additional key exchanges are required + */ +static bool additional_key_exchange_required(private_ike_init_t *this) +{ + int i; + + for (i = this->ke_index; i < MAX_KEY_EXCHANGES; i++) + { + if (this->key_exchanges[i].type && !this->key_exchanges[i].done) + { + return TRUE; + } + } + return FALSE; +} + +/** + * Clear data on key exchanges + */ +static void clear_key_exchanges(private_ike_init_t *this) +{ + int i; + + for (i = 0; i < MAX_KEY_EXCHANGES; i++) + { + this->key_exchanges[i].type = 0; + this->key_exchanges[i].method = 0; + this->key_exchanges[i].done = FALSE; + } + this->ke_index = 0; + + array_destroy_offset(this->kes, offsetof(key_exchange_t, destroy)); + this->kes = NULL; +} + +/** + * Process a KE payload + */ +static void process_ke_payload(private_ike_init_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 (method != received) + { + DBG1(DBG_IKE, "key exchange method in received payload %N doesn't " + "match negotiated %N", key_exchange_method_names, received, + key_exchange_method_names, method); + this->ke_failed = TRUE; + return; + } + + if (!this->initiator) + { + DESTROY_IF(this->ke); + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + method); + if (!this->ke) + { + DBG1(DBG_IKE, "negotiated key exchange method %N not supported", + key_exchange_method_names, method); + } + } + else if (this->ke) + { + this->ke_failed = this->ke->get_method(this->ke) != received; + } + + if (this->ke && !this->ke_failed) + { + this->ke_failed = !this->ke->set_public_key(this->ke, + ke->get_key_exchange_data(ke)); + } +} + /** * Read payloads from message */ @@ -531,7 +646,7 @@ static void process_payloads(private_ike_init_t *this, message_t *message) enumerator_t *enumerator; payload_t *payload; ike_sa_id_t *id; - ke_payload_t *ke_payload = NULL; + ke_payload_t *ke_pld = NULL; enumerator = message->create_payload_enumerator(message); while (enumerator->enumerate(enumerator, &payload)) @@ -545,9 +660,9 @@ static void process_payloads(private_ike_init_t *this, message_t *message) } case PLV2_KEY_EXCHANGE: { - ke_payload = (ke_payload_t*)payload; + ke_pld = (ke_payload_t*)payload; - this->dh_group = ke_payload->get_key_exchange_method(ke_payload); + this->ke_method = ke_pld->get_key_exchange_method(ke_pld); break; } case PLV2_NONCE: @@ -641,29 +756,57 @@ static void process_payloads(private_ike_init_t *this, message_t *message) this->proposal->get_spi(this->proposal)); } } - } - if (ke_payload && this->proposal && - this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) - { - if (!this->initiator) + determine_key_exchanges(this); + if (ke_pld) { - this->dh = this->keymat->keymat.create_ke( - &this->keymat->keymat, this->dh_group); - } - else if (this->dh) - { - this->dh_failed = this->dh->get_method(this->dh) != this->dh_group; - } - if (this->dh && !this->dh_failed) - { - this->dh_failed = !this->dh->set_public_key(this->dh, - ke_payload->get_key_exchange_data(ke_payload)); + process_ke_payload(this, ke_pld); } } } +/** + * Build payloads in additional exchanges when using multiple key exchanges + */ +static bool build_payloads_multi_ke(private_ike_init_t *this, + message_t *message) +{ + ke_payload_t *ke; + + ke = ke_payload_create_from_key_exchange(PLV2_KEY_EXCHANGE, this->ke); + if (!ke) + { + DBG1(DBG_IKE, "creating KE payload failed"); + return FALSE; + } + message->add_payload(message, (payload_t*)ke); + return TRUE; +} + +METHOD(task_t, build_i_multi_ke, status_t, + private_ike_init_t *this, message_t *message) +{ + key_exchange_method_t method; + + message->set_exchange_type(message, exchange_type_multi_ke(this)); + + DESTROY_IF(this->ke); + method = this->key_exchanges[this->ke_index].method; + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + method); + if (!this->ke) + { + DBG1(DBG_IKE, "negotiated key exchange method %N not supported", + key_exchange_method_names, method); + return FAILED; + } + if (!build_payloads_multi_ke(this, message)) + { + return FAILED; + } + return NEED_MORE; +} + METHOD(task_t, build_i, status_t, private_ike_init_t *this, message_t *message) { @@ -684,49 +827,50 @@ METHOD(task_t, build_i, status_t, } /* if we are retrying after an INVALID_KE_PAYLOAD we already have one */ - if (!this->dh) + if (!this->ke) { - if (this->old_sa && lib->settings->get_bool(lib->settings, + if (this->old_sa && + 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 */ + { /* reuse the KE method we used for the old IKE_SA when rekeying */ proposal_t *proposal; - uint16_t dh_group; + uint16_t ke_method; proposal = this->old_sa->get_proposal(this->old_sa); if (proposal->get_algorithm(proposal, KEY_EXCHANGE_METHOD, - &dh_group, NULL)) + &ke_method, NULL)) { - this->dh_group = dh_group; + this->ke_method = ke_method; } else { /* this shouldn't happen, but let's be safe */ - this->dh_group = ike_cfg->get_algorithm(ike_cfg, - KEY_EXCHANGE_METHOD); + this->ke_method = ike_cfg->get_algorithm(ike_cfg, + KEY_EXCHANGE_METHOD); } } else { - this->dh_group = ike_cfg->get_algorithm(ike_cfg, - KEY_EXCHANGE_METHOD); + this->ke_method = ike_cfg->get_algorithm(ike_cfg, + KEY_EXCHANGE_METHOD); } - this->dh = this->keymat->keymat.create_ke(&this->keymat->keymat, - this->dh_group); - if (!this->dh) + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + this->ke_method); + if (!this->ke) { - DBG1(DBG_IKE, "configured DH group %N not supported", - key_exchange_method_names, this->dh_group); + DBG1(DBG_IKE, "configured key exchange method %N not supported", + key_exchange_method_names, this->ke_method); return FAILED; } } - else if (this->dh->get_method(this->dh) != this->dh_group) - { /* reset DH instance if group changed (INVALID_KE_PAYLOAD) */ - this->dh->destroy(this->dh); - this->dh = this->keymat->keymat.create_ke(&this->keymat->keymat, - this->dh_group); - if (!this->dh) + else if (this->ke->get_method(this->ke) != this->ke_method) + { /* reset KE instance if method changed (INVALID_KE_PAYLOAD) */ + this->ke->destroy(this->ke); + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + this->ke_method); + if (!this->ke) { - DBG1(DBG_IKE, "requested DH group %N not supported", - key_exchange_method_names, this->dh_group); + DBG1(DBG_IKE, "requested key exchange method %N not supported", + key_exchange_method_names, this->ke_method); return FAILED; } } @@ -763,6 +907,35 @@ METHOD(task_t, build_i, status_t, return NEED_MORE; } +/** + * Process payloads in additional exchanges when using multiple key exchanges + */ +static void process_payloads_multi_ke(private_ike_init_t *this, + message_t *message) +{ + ke_payload_t *ke; + + ke = (ke_payload_t*)message->get_payload(message, PLV2_KEY_EXCHANGE); + if (ke) + { + process_ke_payload(this, ke); + } + else + { + DBG1(DBG_IKE, "KE payload missing in message"); + } +} + +METHOD(task_t, process_r_multi_ke, status_t, + private_ike_init_t *this, message_t *message) +{ + if (message->get_exchange_type(message) == exchange_type_multi_ke(this)) + { + process_payloads_multi_ke(this, message); + } + return NEED_MORE; +} + METHOD(task_t, process_r, status_t, private_ike_init_t *this, message_t *message) { @@ -798,30 +971,46 @@ METHOD(task_t, process_r, status_t, static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i, chunk_t nonce_r) { + ike_sa_t *old_sa; keymat_v2_t *old_keymat; pseudo_random_function_t prf_alg = PRF_UNDEFINED; chunk_t skd = chunk_empty; ike_sa_id_t *id; array_t *kes = NULL; + bool success; - id = this->ike_sa->get_id(this->ike_sa); if (this->old_sa) { - /* rekeying: Include old SKd, use old PRF, apply SPI */ - old_keymat = (keymat_v2_t*)this->old_sa->get_keymat(this->old_sa); - prf_alg = old_keymat->get_skd(old_keymat, &skd); + if (additional_key_exchange_required(this)) + { /* when rekeying, we only derive keys once all exchanges are done */ + return FALSE; + } + old_sa = this->old_sa; + kes = this->kes; } - array_insert_create(&kes, ARRAY_HEAD, this->dh); - if (!this->keymat->derive_ike_keys(this->keymat, this->proposal, kes, - nonce_i, nonce_r, id, prf_alg, skd)) + else + { /* key derivation for additional key exchanges is like rekeying, so pass + * our own SA as old SA to get SK_d */ + old_sa = this->ike_sa; + array_insert_create(&kes, ARRAY_HEAD, this->ke); + } + + id = this->ike_sa->get_id(this->ike_sa); + old_keymat = (keymat_v2_t*)old_sa->get_keymat(old_sa); + prf_alg = old_keymat->get_skd(old_keymat, &skd); + success = this->keymat->derive_ike_keys(this->keymat, this->proposal, kes, + nonce_i, nonce_r, id, prf_alg, skd); + if (success) + { + charon->bus->ike_keys(charon->bus, this->ike_sa, kes, chunk_empty, + nonce_i, nonce_r, skd.len ? old_sa : NULL, NULL, + AUTH_NONE); + } + if (kes != this->kes) { array_destroy(kes); - return FALSE; } - charon->bus->ike_keys(charon->bus, this->ike_sa, kes, chunk_empty, - nonce_i, nonce_r, this->old_sa, NULL, AUTH_NONE); - array_destroy(kes); - return TRUE; + return success; } METHOD(ike_init_t, derive_keys, status_t, @@ -829,7 +1018,7 @@ METHOD(ike_init_t, derive_keys, status_t, { bool success; - if (!this->ke_done || this->ke_derived) + if (!this->ke_index || this->key_exchanges[this->ke_index-1].derived) { return NEED_MORE; } @@ -843,14 +1032,64 @@ METHOD(ike_init_t, derive_keys, status_t, success = derive_keys_internal(this, this->other_nonce, this->my_nonce); } - this->ke_derived = TRUE; + this->key_exchanges[this->ke_index-1].derived = TRUE; if (!success) { DBG1(DBG_IKE, "key derivation failed"); return FAILED; } - return SUCCESS; + return additional_key_exchange_required(this) ? NEED_MORE : SUCCESS; +} + +/** + * Called when a key exchange is done + */ +static status_t key_exchange_done(private_ike_init_t *this) +{ + if (this->old_sa) + { + /* during rekeying, we store all the key exchanges performed */ + array_insert_create(&this->kes, ARRAY_TAIL, this->ke); + this->ke = NULL; + } + + this->key_exchanges[this->ke_index++].done = TRUE; + + return additional_key_exchange_required(this) ? NEED_MORE : SUCCESS; +} + +METHOD(task_t, build_r_multi_ke, status_t, + private_ike_init_t *this, message_t *message) +{ + if (!this->ke) + { + message->add_notify(message, FALSE, INVALID_SYNTAX, chunk_empty); + return FAILED; + } + if (this->ke_failed) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + return FAILED; + } + if (!build_payloads_multi_ke(this, message)) + { + return FAILED; + } + + if (key_exchange_done(this) != NEED_MORE && this->old_sa) + { + /* during rekeying, we derive keys once all exchanges are done */ + if (derive_keys(this) != SUCCESS) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + return FAILED; + } + return SUCCESS; + } + /* when not rekeying, we derive keys after each IKE_INTERMEDIATE but only + * once we receive the next message, so IntAuth is based on the right keys */ + return NEED_MORE; } METHOD(task_t, build_r, status_t, @@ -883,19 +1122,20 @@ METHOD(task_t, build_r, status_t, return FAILED; } - if (this->dh == NULL || + if (!this->ke || !this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) + this->ke_method)) { uint16_t group; if (this->proposal->get_algorithm(this->proposal, KEY_EXCHANGE_METHOD, - &group, NULL)) + &group, NULL) && + this->ke_method != group) { - DBG1(DBG_IKE, "DH group %N unacceptable, requesting %N", - key_exchange_method_names, this->dh_group, + DBG1(DBG_IKE, "key exchange method %N unacceptable, requesting %N", + key_exchange_method_names, this->ke_method, key_exchange_method_names, group); - this->dh_group = group; + this->ke_method = group; group = htons(group); message->add_notify(message, FALSE, INVALID_KE_PAYLOAD, chunk_from_thing(group)); @@ -908,9 +1148,9 @@ METHOD(task_t, build_r, status_t, return FAILED; } - if (this->dh_failed) + if (this->ke_failed) { - DBG1(DBG_IKE, "applying DH public value failed"); + DBG1(DBG_IKE, "applying KE public value failed"); message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); return FAILED; } @@ -920,9 +1160,14 @@ METHOD(task_t, build_r, status_t, message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); return FAILED; } - this->ke_done = TRUE; - if (this->old_sa) + if (key_exchange_done(this) == NEED_MORE) + { + /* use other exchange type for additional key exchanges */ + this->public.task.build = _build_r_multi_ke; + this->public.task.process = _process_r_multi_ke; + } + else if (this->old_sa) { /* during rekeying, we derive keys here directly */ if (derive_keys(this) != SUCCESS) @@ -1019,6 +1264,26 @@ METHOD(task_t, pre_process_i, status_t, return SUCCESS; } +METHOD(task_t, process_i_multi_ke, status_t, + private_ike_init_t *this, message_t *message) +{ + process_payloads_multi_ke(this, message); + + if (this->ke_failed) + { + return FAILED; + } + + if (key_exchange_done(this) != NEED_MORE && this->old_sa) + { + /* during rekeying, we derive keys once all exchanges are done */ + return derive_keys(this); + } + /* when not rekeying, we derive keys after each IKE_INTERMEDIATE but only + * once we send the next message, so IntAuth is based on the right keys */ + return NEED_MORE; +} + METHOD(task_t, process_i, status_t, private_ike_init_t *this, message_t *message) { @@ -1039,16 +1304,16 @@ METHOD(task_t, process_i, status_t, case INVALID_KE_PAYLOAD: { chunk_t data; - key_exchange_method_t bad_group DBG_UNUSED; + key_exchange_method_t bad_method DBG_UNUSED; - bad_group = this->dh_group; + bad_method = this->ke_method; data = notify->get_notification_data(notify); - this->dh_group = ntohs(*((uint16_t*)data.ptr)); - DBG1(DBG_IKE, "peer didn't accept DH group %N, " + this->ke_method = ntohs(*((uint16_t*)data.ptr)); + DBG1(DBG_IKE, "peer didn't accept key exchange method %N, " "it requested %N", key_exchange_method_names, - bad_group, key_exchange_method_names, this->dh_group); + bad_method, key_exchange_method_names, this->ke_method); - if (this->old_sa == NULL) + if (!this->old_sa) { /* reset the IKE_SA if we are not rekeying */ this->ike_sa->reset(this->ike_sa, FALSE); } @@ -1125,29 +1390,33 @@ METHOD(task_t, process_i, status_t, process_payloads(this, message); /* check if we have everything */ - if (this->proposal == NULL || + if (!this->proposal || this->other_nonce.len == 0 || this->my_nonce.len == 0) { - DBG1(DBG_IKE, "peers proposal selection invalid"); + DBG1(DBG_IKE, "peer's proposal selection invalid"); return FAILED; } - if (this->dh == NULL || - !this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) + if (!this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, + this->ke_method)) { - DBG1(DBG_IKE, "peer DH group selection invalid"); + DBG1(DBG_IKE, "peer's key exchange method selection invalid"); return FAILED; } - if (this->dh_failed) + if (this->ke_failed) { - DBG1(DBG_IKE, "applying DH public value failed"); + DBG1(DBG_IKE, "applying key exchange public value failed"); return FAILED; } - this->ke_done = TRUE; - if (this->old_sa) + if (key_exchange_done(this) == NEED_MORE) + { + /* use other exchange type for additional key exchanges */ + this->public.task.build = _build_i_multi_ke; + this->public.task.process = _process_i_multi_ke; + } + else if (this->old_sa) { /* during rekeying, we derive keys here directly */ return derive_keys(this); @@ -1167,24 +1436,26 @@ METHOD(task_t, migrate, void, { DESTROY_IF(this->proposal); chunk_free(&this->other_nonce); - this->ke_done = FALSE; - this->ke_derived = FALSE; + clear_key_exchanges(this); this->ike_sa = ike_sa; this->keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); this->proposal = NULL; - this->dh_failed = FALSE; + this->ke_failed = FALSE; + this->public.task.build = _build_i; + this->public.task.process = _process_i; } METHOD(task_t, destroy, void, private_ike_init_t *this) { - DESTROY_IF(this->dh); + DESTROY_IF(this->ke); DESTROY_IF(this->proposal); DESTROY_IF(this->nonceg); chunk_free(&this->my_nonce); chunk_free(&this->other_nonce); chunk_free(&this->cookie); + clear_key_exchanges(this); free(this); } @@ -1221,7 +1492,7 @@ ike_init_t *ike_init_create(ike_sa_t *ike_sa, bool initiator, ike_sa_t *old_sa) }, .ike_sa = ike_sa, .initiator = initiator, - .dh_group = KE_NONE, + .ke_method = KE_NONE, .keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa), .old_sa = old_sa, .signature_authentication = lib->settings->get_bool(lib->settings, From ca3e6d2d144ed32dc52ccde86b5e2347657108ed Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 6 Apr 2020 17:41:15 +0200 Subject: [PATCH 28/46] ike-rekey: Support IKE_SA rekeying with multiple key exchanges --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 500 +++++++++++++++++++---- 1 file changed, 414 insertions(+), 86 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index fac008715..f624464ec 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2018 Tobias Brunner + * Copyright (C) 2015-2020 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -26,7 +26,6 @@ #include #include - typedef struct private_ike_rekey_t private_ike_rekey_t; /** @@ -65,14 +64,49 @@ struct private_ike_rekey_t { ike_delete_t *ike_delete; /** - * colliding task detected by the task manager + * Colliding passive task if any */ private_ike_rekey_t *collision; /** - * TRUE if rekeying can't be handled temporarily + * Link value for the current key exchange */ - bool failed_temporarily; + chunk_t link; + + /** + * State/error flags + */ + enum { + + /** + * Set if rekeying can't be handled temporarily. + */ + IKE_REKEY_FAILED_TEMPORARILY = (1<<0), + + /** + * Set if the parsed link value was invalid. + */ + IKE_REKEY_LINK_INVALID = (1<<1), + + /** + * Set if we use multiple key exchanges and already processed the + * CREATE_CHILD_SA response and started sending IKE_FOLLOWUP_KEs. + */ + IKE_REKEY_FOLLOWUP_KE = (1<<2), + + /** + * Set if a passive rekeying has completed successfully and we don't + * expect any further messages. + */ + IKE_REKEY_DONE = (1<<3), + + /** + * Set if we adopted a completed passive task, otherwise we just + * reference it. + */ + IKE_REKEY_ADOPTED_PASSIVE = (1<<4), + + } flags; }; /** @@ -161,10 +195,24 @@ METHOD(task_t, process_i_delete, status_t, return this->ike_delete->task.process(&this->ike_delete->task, message); } +METHOD(task_t, build_i_multi_ke, status_t, + private_ike_rekey_t *this, message_t *message) +{ + status_t status; + + charon->bus->set_sa(charon->bus, this->new_sa); + message->add_notify(message, FALSE, ADDITIONAL_KEY_EXCHANGE, this->link); + status = this->ike_init->task.build(&this->ike_init->task, message); + charon->bus->set_sa(charon->bus, this->ike_sa); + this->flags |= IKE_REKEY_FOLLOWUP_KE; + return status; +} + METHOD(task_t, build_i, status_t, private_ike_rekey_t *this, message_t *message) { ike_version_t version; + status_t status; /* create new SA only on first try */ if (!this->new_sa) @@ -188,9 +236,9 @@ METHOD(task_t, build_i, status_t, this->ike_init = ike_init_create(this->new_sa, TRUE, this->ike_sa); this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); } - this->ike_init->task.build(&this->ike_init->task, message); - - return NEED_MORE; + status = this->ike_init->task.build(&this->ike_init->task, message); + charon->bus->set_sa(charon->bus, this->ike_sa); + return status; } /** @@ -231,25 +279,116 @@ static bool have_half_open_children(private_ike_rekey_t *this) return FALSE; } +/** + * Check if we are actively rekeying and optionally, if we already sent an + * IKE_FOLLOWUP_KE message. + */ +static bool actively_rekeying(private_ike_rekey_t *this, bool *follow_up_sent) +{ + enumerator_t *enumerator; + task_t *task; + bool found = FALSE; + + enumerator = this->ike_sa->create_task_enumerator(this->ike_sa, + TASK_QUEUE_ACTIVE); + while (enumerator->enumerate(enumerator, (void**)&task)) + { + if (task->get_type(task) == TASK_IKE_REKEY) + { + if (follow_up_sent) + { + private_ike_rekey_t *rekey = (private_ike_rekey_t*)task; + *follow_up_sent = rekey->flags & IKE_REKEY_FOLLOWUP_KE; + } + found = TRUE; + break; + } + } + enumerator->destroy(enumerator); + return found; +} + +/** + * Process payloads in a IKE_FOLLOWUP_KE message or a CREATE_CHILD_SA response + */ +static void process_link(private_ike_rekey_t *this, message_t *message) +{ + notify_payload_t *notify; + chunk_t link; + + notify = message->get_notify(message, ADDITIONAL_KEY_EXCHANGE); + if (!notify) + { + DBG1(DBG_IKE, "%N notify missing", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + this->flags |= IKE_REKEY_LINK_INVALID; + } + else + { + link = notify->get_notification_data(notify); + if (this->initiator) + { + chunk_free(&this->link); + this->link = chunk_clone(link); + } + else if (!chunk_equals_const(this->link, link)) + { + DBG1(DBG_IKE, "data in %N notify doesn't match", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + this->flags |= IKE_REKEY_LINK_INVALID; + } + } +} + +METHOD(task_t, process_r_multi_ke, status_t, + private_ike_rekey_t *this, message_t *message) +{ + if (message->get_exchange_type(message) != IKE_FOLLOWUP_KE) + { + return FAILED; + } + if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) + { + DBG1(DBG_IKE, "peer continued rekeying, but we are deleting"); + this->flags |= IKE_REKEY_FAILED_TEMPORARILY; + return NEED_MORE; + } + + charon->bus->set_sa(charon->bus, this->new_sa); + process_link(this, message); + this->ike_init->task.process(&this->ike_init->task, message); + charon->bus->set_sa(charon->bus, this->ike_sa); + return NEED_MORE; +} + METHOD(task_t, process_r, status_t, private_ike_rekey_t *this, message_t *message) { + bool follow_up_sent; + if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) { DBG1(DBG_IKE, "peer initiated rekeying, but we are deleting"); - this->failed_temporarily = TRUE; + this->flags |= IKE_REKEY_FAILED_TEMPORARILY; return NEED_MORE; } if (this->ike_sa->has_condition(this->ike_sa, COND_REAUTHENTICATING)) { DBG1(DBG_IKE, "peer initiated rekeying, but we are reauthenticating"); - this->failed_temporarily = TRUE; + this->flags |= IKE_REKEY_FAILED_TEMPORARILY; return NEED_MORE; } if (have_half_open_children(this)) { DBG1(DBG_IKE, "peer initiated rekeying, but a child is half-open"); - this->failed_temporarily = TRUE; + this->flags |= IKE_REKEY_FAILED_TEMPORARILY; + return NEED_MORE; + } + if (actively_rekeying(this, &follow_up_sent) && follow_up_sent) + { + DBG1(DBG_IKE, "peer initiated rekeying, but we did too and already " + "sent IKE_FOLLOWUP_KE"); + this->flags |= IKE_REKEY_FAILED_TEMPORARILY; return NEED_MORE; } @@ -269,11 +408,16 @@ METHOD(task_t, process_r, status_t, METHOD(task_t, build_r, status_t, private_ike_rekey_t *this, message_t *message) { - if (this->failed_temporarily) + if (this->flags & IKE_REKEY_FAILED_TEMPORARILY) { message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } + if (this->flags & IKE_REKEY_LINK_INVALID) + { + message->add_notify(message, TRUE, STATE_NOT_FOUND, chunk_empty); + return SUCCESS; + } if (!this->new_sa) { /* IKE_SA/a CHILD_SA is in an unacceptable state, deny rekeying */ @@ -282,17 +426,43 @@ METHOD(task_t, build_r, status_t, } charon->bus->set_sa(charon->bus, this->new_sa); - if (this->ike_init->task.build(&this->ike_init->task, message) == FAILED) + switch (this->ike_init->task.build(&this->ike_init->task, message)) { - this->ike_init->task.destroy(&this->ike_init->task); - this->ike_init = NULL; - charon->bus->set_sa(charon->bus, this->ike_sa); - return SUCCESS; + case FAILED: + this->ike_init->task.destroy(&this->ike_init->task); + this->ike_init = NULL; + charon->bus->set_sa(charon->bus, this->ike_sa); + return SUCCESS; + case NEED_MORE: + /* additional key exchanges, the value in the notify doesn't really + * matter to us as we have a window size of 1 */ + charon->bus->set_sa(charon->bus, this->ike_sa); + if (!this->link.ptr) + { + this->link = chunk_clone(chunk_from_chars(0x42)); + } + message->add_notify(message, FALSE, ADDITIONAL_KEY_EXCHANGE, + this->link); + if (this->ike_sa->get_state(this->ike_sa) != IKE_REKEYING) + { + this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); + } + this->public.task.process = _process_r_multi_ke; + return NEED_MORE; + default: + charon->bus->set_sa(charon->bus, this->ike_sa); + if (this->ike_sa->get_state(this->ike_sa) != IKE_REKEYING) + { + this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); + } + break; } - charon->bus->set_sa(charon->bus, this->ike_sa); - if (this->ike_sa->get_state(this->ike_sa) != IKE_REKEYING) - { /* in case of a collision we let the initiating task handle this */ + this->flags |= IKE_REKEY_DONE; + + /* if we are actively rekeying, we let the initiating task handle this */ + if (!actively_rekeying(this, NULL)) + { establish_new(this); /* make sure the IKE_SA is gone in case the peer fails to delete it */ lib->scheduler->schedule_job(lib->scheduler, (job_t*) @@ -303,7 +473,7 @@ METHOD(task_t, build_r, status_t, } /** - * Conclude any undetected rekey collision. + * Conclude any (undetected) rekey collision. * * If the peer does not detect the collision it will delete this IKE_SA. * Depending on when our request reaches the peer and we receive the delete @@ -311,18 +481,174 @@ METHOD(task_t, build_r, status_t, * * Returns TRUE if there was a collision, FALSE otherwise. */ -static bool conclude_undetected_collision(private_ike_rekey_t *this) +static bool conclude_collision(private_ike_rekey_t *this, bool maybe_undetected) { - if (this->collision) + if (this->collision && + this->flags & IKE_REKEY_ADOPTED_PASSIVE) { - DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision, abort " - "active rekeying"); + if (maybe_undetected) + { + DBG1(DBG_IKE, "peer may not have noticed IKE_SA rekey collision, " + "abort active rekeying"); + } establish_new(this->collision); return TRUE; } return FALSE; } +/** + * Delete the redundant IKE_SA we created. + */ +static void delete_redundant(private_ike_rekey_t *this) +{ + host_t *host; + + /* apply host for a proper delete */ + host = this->ike_sa->get_my_host(this->ike_sa); + this->new_sa->set_my_host(this->new_sa, host->clone(host)); + host = this->ike_sa->get_other_host(this->ike_sa); + this->new_sa->set_other_host(this->new_sa, host->clone(host)); + this->new_sa->set_state(this->new_sa, IKE_REKEYED); + if (this->new_sa->delete(this->new_sa, FALSE) == DESTROY_ME) + { + this->new_sa->destroy(this->new_sa); + } + else + { + charon->ike_sa_manager->checkin(charon->ike_sa_manager, this->new_sa); + } + charon->bus->set_sa(charon->bus, this->ike_sa); + this->new_sa = NULL; +} + +/** + * Check in the redundant IKE_SA created by the peer and wait for its deletion. + */ +static void wait_for_redundant_delete(private_ike_rekey_t *this) +{ + private_ike_rekey_t *other = this->collision; + job_t *job; + + /* peer should delete the SA it created, add a timeout just in case */ + job = (job_t*)delete_ike_sa_job_create( + other->new_sa->get_id(other->new_sa), TRUE); + lib->scheduler->schedule_job(lib->scheduler, job, HALF_OPEN_IKE_SA_TIMEOUT); + other->new_sa->set_state(other->new_sa, IKE_REKEYED); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, other->new_sa); + other->new_sa = NULL; + charon->bus->set_sa(charon->bus, this->ike_sa); +} + +/** + * Remove the passive rekey task that's waiting for IKE_FOLLOWUP_KE requests + * that will never come. + */ +static void remove_passive_rekey_task(private_ike_rekey_t *this) +{ + enumerator_t *enumerator; + task_t *task; + + enumerator = this->ike_sa->create_task_enumerator(this->ike_sa, + TASK_QUEUE_PASSIVE); + while (enumerator->enumerate(enumerator, &task)) + { + if (task->get_type(task) == TASK_IKE_REKEY) + { + this->ike_sa->remove_task(this->ike_sa, enumerator); + task->destroy(task); + break; + } + } + enumerator->destroy(enumerator); +} + +/** + * Handle any collision as necessary and report back if we lost the + * collision and should abort the active task. + */ +static bool collision_lost(private_ike_rekey_t *this, bool multi_ke) +{ + private_ike_rekey_t *other = this->collision; + chunk_t this_nonce, other_nonce; + + if (!this->collision) + { + return FALSE; + } + + this_nonce = this->ike_init->get_lower_nonce(this->ike_init); + other_nonce = other->ike_init->get_lower_nonce(other->ike_init); + + /* the SA with the lowest nonce should be deleted (if already complete), + * check if we or the peer created that */ + if (memcmp(this_nonce.ptr, other_nonce.ptr, + min(this_nonce.len, other_nonce.len)) < 0) + { + if (multi_ke) + { + DBG1(DBG_IKE, "IKE_SA rekey collision lost, abort incomplete " + "multi-KE rekeying"); + } + else + { + DBG1(DBG_IKE, "IKE_SA rekey collision lost, deleting redundant " + "IKE_SA %s[%d]", this->new_sa->get_name(this->new_sa), + this->new_sa->get_unique_id(this->new_sa)); + delete_redundant(this); + } + /* establish the other SA if the passive task is done (i.e. was + * single-KE or our response was delayed and the winner continued), + * otherwise, we just let it continue independently */ + conclude_collision(this, FALSE); + return TRUE; + } + + /* the passive rekeying is complete only if it was single-KE. otherwise, + * the peer would either have stopped before sending IKE_FOLLOWUP_KE when it + * noticed it lost, or it responded with TEMPORARY_FAILURE to our + * CREATE_CHILD_SA request if it already started sending them. + * since the task is not completed immediately, we clean up the collision */ + if (this->flags & IKE_REKEY_ADOPTED_PASSIVE) + { + if (multi_ke) + { + DBG1(DBG_IKE, "IKE_SA rekey collision won, continue with multi-KE " + "rekeying and wait for delete for redundant IKE_SA %s[%d]", + other->new_sa->get_name(other->new_sa), + other->new_sa->get_unique_id(other->new_sa)); + } + else + { + DBG1(DBG_IKE, "IKE_SA rekey collision won, waiting for delete for " + "redundant IKE_SA %s[%d]", + other->new_sa->get_name(other->new_sa), + other->new_sa->get_unique_id(other->new_sa)); + } + wait_for_redundant_delete(this); + other->public.task.destroy(&other->public.task); + } + else + { + /* the peer will not continue with its multi-KE rekeying, so we must + * remove the passive task that's waiting for IKE_FOLLOWUP_KEs */ + if (multi_ke) + { + DBG1(DBG_IKE, "IKE_SA rekey collision won, continue with " + "multi-KE rekeying and remove passive %N task", + task_type_names, TASK_IKE_REKEY); + } + else + { + DBG1(DBG_IKE, "IKE_SA rekey collision won, remove passive %N task", + task_type_names, TASK_IKE_REKEY); + } + remove_passive_rekey_task(this); + } + this->collision = NULL; + return FALSE; +} + METHOD(task_t, process_i, status_t, private_ike_rekey_t *this, message_t *message) { @@ -336,78 +662,67 @@ METHOD(task_t, process_i, status_t, this->ike_sa->get_id(this->ike_sa), TRUE)); return SUCCESS; } + if (message->get_notify(message, STATE_NOT_FOUND)) + { + DBG1(DBG_IKE, "peer didn't like our %N notify data", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + if (!conclude_collision(this, TRUE)) + { + schedule_delayed_rekey(this); + } + return SUCCESS; + } + charon->bus->set_sa(charon->bus, this->new_sa); switch (this->ike_init->task.process(&this->ike_init->task, message)) { case FAILED: + charon->bus->set_sa(charon->bus, this->ike_sa); /* rekeying failed, fallback to old SA */ - if (!conclude_undetected_collision(this)) + if (!conclude_collision(this, TRUE)) { schedule_delayed_rekey(this); } return SUCCESS; case NEED_MORE: - /* bad KE method, try again */ - this->ike_init->task.migrate(&this->ike_init->task, this->new_sa); - return NEED_MORE; + if (message->get_notify(message, INVALID_KE_PAYLOAD)) + { /* bad key exchange mechanism, try again */ + this->ike_init->task.migrate(&this->ike_init->task, + this->new_sa); + charon->bus->set_sa(charon->bus, this->ike_sa); + return NEED_MORE; + } + /* multiple key exchanges, continue with IKE_FOLLOWUP_KE */ + process_link(this, message); + charon->bus->set_sa(charon->bus, this->ike_sa); + if (this->flags & IKE_REKEY_LINK_INVALID) + { /* we can't continue without notify, maybe the peer returns + * one later */ + if (!conclude_collision(this, TRUE)) + { + schedule_delayed_rekey(this); + } + return SUCCESS; + } + this->public.task.build = _build_i_multi_ke; + /* there will only be a collision if we process a CREATE_CHILD_SA + * response, if we already sent an IKE_FOLOWUP_KE, the passive task + * would just respond with TEMPORARY_FAILURE and get ignored */ + return collision_lost(this, TRUE) ? SUCCESS : NEED_MORE; default: + charon->bus->set_sa(charon->bus, this->ike_sa); break; } - if (this->collision) + /* there will not be a collision here if this task is for a multi-KE + * rekeying, as that would already have been handled above when processing + * the CREATE_CHILD_SA response */ + if (collision_lost(this, FALSE)) { - private_ike_rekey_t *other = this->collision; - host_t *host; - chunk_t this_nonce, other_nonce; - - this_nonce = this->ike_init->get_lower_nonce(this->ike_init); - other_nonce = other->ike_init->get_lower_nonce(other->ike_init); - - /* the SA with the lowest nonce should be deleted, check if we or - * the peer created that */ - if (memcmp(this_nonce.ptr, other_nonce.ptr, - min(this_nonce.len, other_nonce.len)) < 0) - { - DBG1(DBG_IKE, "IKE_SA rekey collision lost, deleting redundant " - "IKE_SA %s[%d]", this->new_sa->get_name(this->new_sa), - this->new_sa->get_unique_id(this->new_sa)); - /* apply host for a proper delete */ - host = this->ike_sa->get_my_host(this->ike_sa); - this->new_sa->set_my_host(this->new_sa, host->clone(host)); - host = this->ike_sa->get_other_host(this->ike_sa); - this->new_sa->set_other_host(this->new_sa, host->clone(host)); - this->new_sa->set_state(this->new_sa, IKE_REKEYED); - if (this->new_sa->delete(this->new_sa, FALSE) == DESTROY_ME) - { - this->new_sa->destroy(this->new_sa); - } - else - { - charon->ike_sa_manager->checkin(charon->ike_sa_manager, - this->new_sa); - } - charon->bus->set_sa(charon->bus, this->ike_sa); - this->new_sa = NULL; - establish_new(other); - return SUCCESS; - } - - /* peer should delete the SA it created, add a timeout just in case */ - job_t *job = (job_t*)delete_ike_sa_job_create( - other->new_sa->get_id(other->new_sa), TRUE); - lib->scheduler->schedule_job(lib->scheduler, job, - HALF_OPEN_IKE_SA_TIMEOUT); - DBG1(DBG_IKE, "IKE_SA rekey collision won, waiting for delete for " - "redundant IKE_SA %s[%d]", other->new_sa->get_name(other->new_sa), - other->new_sa->get_unique_id(other->new_sa)); - other->new_sa->set_state(other->new_sa, IKE_REKEYED); - charon->ike_sa_manager->checkin(charon->ike_sa_manager, other->new_sa); - other->new_sa = NULL; - charon->bus->set_sa(charon->bus, this->ike_sa); + return SUCCESS; } establish_new(this); - /* rekeying successful, delete this IKE_SA using a subtask */ this->ike_delete = ike_delete_create(this->ike_sa, TRUE); this->public.task.build = _build_i_delete; @@ -437,7 +752,7 @@ METHOD(ike_rekey_t, collide, bool, switch (other->get_type(other)) { case TASK_IKE_DELETE: - conclude_undetected_collision(this); + conclude_collision(this, TRUE); break; case TASK_IKE_REKEY: { @@ -447,14 +762,22 @@ METHOD(ike_rekey_t, collide, bool, { DBG1(DBG_IKE, "colliding exchange did not result in an IKE_SA, " "ignore"); + if (this->collision == rekey) + { + this->collision = NULL; + } break; } - if (this->collision) - { - this->collision->public.task.destroy(&this->collision->public.task); - } + /* we keep track of the passive exchange in any case, if not + * complete yet, this method might be called again later */ this->collision = rekey; - return TRUE; + if (rekey->flags & IKE_REKEY_DONE) + { + this->flags |= IKE_REKEY_ADOPTED_PASSIVE; + return TRUE; + } + DBG1(DBG_IKE, "colliding passive exchange is not yet complete"); + break; } default: /* shouldn't happen */ @@ -481,10 +804,14 @@ static void cleanup(private_ike_rekey_t *this) cur_sa = charon->bus->get_sa(charon->bus); DESTROY_IF(this->new_sa); charon->bus->set_sa(charon->bus, cur_sa); - if (this->collision) + /* only destroy if the passive task was adopted, otherwise it is still + * queued and might get destroyed by the task manager */ + if (this->collision && + this->flags & IKE_REKEY_ADOPTED_PASSIVE) { this->collision->public.task.destroy(&this->collision->public.task); } + chunk_free(&this->link); } METHOD(task_t, migrate, void, @@ -496,6 +823,7 @@ METHOD(task_t, migrate, void, this->new_sa = NULL; this->ike_init = NULL; this->ike_delete = NULL; + this->flags = 0; } METHOD(task_t, destroy, void, From d7760416d62d0c37235caa6a9fe3905c12e67672 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 25 Jun 2020 10:26:38 +0200 Subject: [PATCH 29/46] child-create: Add support for multiple key exchanges It also changes that payloads are built before installing the CHILD_SA on the responder, that is, the KE payload is generated before keys are derived, so that key_exchange_t::get_public_key() is called before get_shared_secret(), or its internal equivalent, which could be relevant for KE implementations that want to ensure that the key can't be accessed again after the key derivation. --- src/charon-tkm/src/tkm/tkm_kernel_ipsec.c | 18 +- src/charon-tkm/src/tkm/tkm_keymat.c | 14 +- src/charon-tkm/src/tkm/tkm_types.h | 4 +- src/charon-tkm/tests/keymat_tests.c | 4 +- src/libcharon/sa/ikev2/tasks/child_create.c | 959 +++++++++++++++----- src/libcharon/sa/ikev2/tasks/child_create.h | 8 +- src/libcharon/sa/ikev2/tasks/child_rekey.c | 8 +- 7 files changed, 765 insertions(+), 250 deletions(-) diff --git a/src/charon-tkm/src/tkm/tkm_kernel_ipsec.c b/src/charon-tkm/src/tkm/tkm_kernel_ipsec.c index 1e680258d..504bec2f4 100644 --- a/src/charon-tkm/src/tkm/tkm_kernel_ipsec.c +++ b/src/charon-tkm/src/tkm/tkm_kernel_ipsec.c @@ -107,29 +107,27 @@ METHOD(kernel_ipsec_t, add_sa, status_t, } esa = *(esa_info_t *)(data->enc_key.ptr); - /* only handle the case where we have both distinct ESP spi's available */ - if (esa.spi_r == id->spi) + /* only handle the case where we have both distinct ESP SPIs available, + * which is always the outbound SA */ + if (esa.spi_l == id->spi) { chunk_free(&esa.nonce_i); chunk_free(&esa.nonce_r); return SUCCESS; } + spi_loc = esa.spi_l; + spi_rem = id->spi; + local = id->src; + peer = id->dst; + if (data->initiator) { - spi_loc = id->spi; - spi_rem = esa.spi_r; - local = id->dst; - peer = id->src; nonce_loc = &esa.nonce_i; nonce_rem = &esa.nonce_r; } else { - spi_loc = esa.spi_r; - spi_rem = id->spi; - local = id->src; - peer = id->dst; nonce_loc = &esa.nonce_r; nonce_rem = &esa.nonce_i; } diff --git a/src/charon-tkm/src/tkm/tkm_keymat.c b/src/charon-tkm/src/tkm/tkm_keymat.c index d8b800f05..a435e29de 100644 --- a/src/charon-tkm/src/tkm/tkm_keymat.c +++ b/src/charon-tkm/src/tkm/tkm_keymat.c @@ -221,7 +221,7 @@ METHOD(keymat_v2_t, derive_child_keys, bool, INIT(esa_info_i, .isa_id = this->isa_ctx_id, - .spi_r = proposal->get_spi(proposal), + .spi_l = proposal->get_spi(proposal), .nonce_i = chunk_clone(nonce_i), .nonce_r = chunk_clone(nonce_r), .is_encr_r = FALSE, @@ -230,15 +230,15 @@ METHOD(keymat_v2_t, derive_child_keys, bool, INIT(esa_info_r, .isa_id = this->isa_ctx_id, - .spi_r = proposal->get_spi(proposal), + .spi_l = proposal->get_spi(proposal), .nonce_i = chunk_clone(nonce_i), .nonce_r = chunk_clone(nonce_r), .is_encr_r = TRUE, .dh_id = dh_id, ); - DBG1(DBG_CHD, "passing on esa info (isa: %llu, spi_r: %x, dh_id: %llu)", - esa_info_i->isa_id, ntohl(esa_info_i->spi_r), esa_info_i->dh_id); + DBG1(DBG_CHD, "passing on esa info (isa: %llu, spi_l: %x, dh_id: %llu)", + esa_info_i->isa_id, ntohl(esa_info_i->spi_l), esa_info_i->dh_id); /* store ESA info in encr_i/r, which is passed to add_sa */ *encr_i = chunk_create((u_char *)esa_info_i, sizeof(esa_info_t)); @@ -296,6 +296,12 @@ METHOD(keymat_v2_t, get_skd, pseudo_random_function_t, { isa_info_t *isa_info; + if (!this->ae_ctx_id) + { + *skd = chunk_empty; + return PRF_UNDEFINED; + } + INIT(isa_info, .parent_isa_id = this->isa_ctx_id, .ae_id = this->ae_ctx_id, diff --git a/src/charon-tkm/src/tkm/tkm_types.h b/src/charon-tkm/src/tkm/tkm_types.h index 19124f25c..365f36678 100644 --- a/src/charon-tkm/src/tkm/tkm_types.h +++ b/src/charon-tkm/src/tkm/tkm_types.h @@ -49,9 +49,9 @@ struct esa_info_t { isa_id_type isa_id; /** - * Responder SPI of child SA. + * Local SPI of child SA. */ - esp_spi_type spi_r; + esp_spi_type spi_l; /** * Initiator nonce. diff --git a/src/charon-tkm/tests/keymat_tests.c b/src/charon-tkm/tests/keymat_tests.c index 56c8b6809..9cd6d5e02 100644 --- a/src/charon-tkm/tests/keymat_tests.c +++ b/src/charon-tkm/tests/keymat_tests.c @@ -107,7 +107,7 @@ START_TEST(test_derive_child_keys) fail_if(!info, "encr_i does not contain esa information"); fail_if(info->isa_id != keymat->get_isa_id(keymat), "Isa context id mismatch (encr_i)"); - fail_if(info->spi_r != 42, + fail_if(info->spi_l != 42, "SPI mismatch (encr_i)"); fail_unless(chunk_equals(info->nonce_i, nonce), "nonce_i mismatch (encr_i)"); @@ -124,7 +124,7 @@ START_TEST(test_derive_child_keys) fail_if(!info, "encr_r does not contain esa information"); fail_if(info->isa_id != keymat->get_isa_id(keymat), "Isa context id mismatch (encr_r)"); - fail_if(info->spi_r != 42, + fail_if(info->spi_l != 42, "SPI mismatch (encr_r)"); fail_unless(chunk_equals(info->nonce_i, nonce), "nonce_i mismatch (encr_r)"); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index acc1a928f..a04b14d55 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2019 Tobias Brunner + * Copyright (C) 2008-2020 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -32,6 +32,10 @@ #include #include +/** Maximum number of key exchanges (including the initial one, if any) */ +#define MAX_KEY_EXCHANGES (ADDITIONAL_KEY_EXCHANGE_7 - \ + ADDITIONAL_KEY_EXCHANGE_1 + 2) + typedef struct private_child_create_t private_child_create_t; /** @@ -115,19 +119,43 @@ struct private_child_create_t { traffic_selector_t *packet_tsr; /** - * optional diffie hellman exchange + * Key exchanges to perform */ - key_exchange_t *dh; + struct { + transform_type_t type; + key_exchange_method_t method; + bool done; + } key_exchanges[MAX_KEY_EXCHANGES]; /** - * Applying DH public value failed? + * Current key exchange */ - bool dh_failed; + int ke_index; /** - * group used for DH exchange + * Kex exchange method from the parsed or sent KE payload */ - key_exchange_method_t dh_group; + key_exchange_method_t ke_method; + + /** + * Current key exchange object (if any) + */ + key_exchange_t *ke; + + /** + * All key exchanges performed (key_exchange_t) + */ + array_t *kes; + + /** + * Applying KE public key failed? + */ + bool ke_failed; + + /** + * Link value for current key exchange + */ + chunk_t link; /** * IKE_SAs keymat @@ -200,7 +228,7 @@ struct private_child_create_t { bool rekey; /** - * whether we are retrying with another DH group + * whether we are retrying with another KE method */ bool retry; }; @@ -304,35 +332,39 @@ static bool allocate_spi(private_child_create_t *this) this->proto = this->proposal->get_protocol(this->proposal); } this->my_spi = this->child_sa->alloc_spi(this->child_sa, this->proto); + if (!this->my_spi) + { + DBG1(DBG_IKE, "unable to allocate SPI from kernel"); + } return this->my_spi != 0; } /** - * Update the proposals with the allocated SPIs as initiator and check the DH - * group and promote it if necessary + * Update the proposals with the allocated SPIs as initiator and check the KE + * method and promote it if necessary */ static bool update_and_check_proposals(private_child_create_t *this) { enumerator_t *enumerator; proposal_t *proposal; - linked_list_t *other_dh_groups; + linked_list_t *other_ke_methods; bool found = FALSE; - other_dh_groups = linked_list_create(); + other_ke_methods = linked_list_create(); enumerator = this->proposals->create_enumerator(this->proposals); while (enumerator->enumerate(enumerator, &proposal)) { proposal->set_spi(proposal, this->my_spi); - /* move the selected DH group to the front, if any */ - if (this->dh_group != KE_NONE) + /* move the selected KE method to the front, if any */ + if (this->ke_method != KE_NONE) { /* proposals that don't contain the selected group are * moved to the back */ if (!proposal->promote_transform(proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) + this->ke_method)) { this->proposals->remove_at(this->proposals, enumerator); - other_dh_groups->insert_last(other_dh_groups, proposal); + other_ke_methods->insert_last(other_ke_methods, proposal); } else { @@ -341,15 +373,15 @@ static bool update_and_check_proposals(private_child_create_t *this) } } enumerator->destroy(enumerator); - enumerator = other_dh_groups->create_enumerator(other_dh_groups); + enumerator = other_ke_methods->create_enumerator(other_ke_methods); while (enumerator->enumerate(enumerator, (void**)&proposal)) { /* no need to remove from the list as we destroy it anyway*/ this->proposals->insert_last(this->proposals, proposal); } enumerator->destroy(enumerator); - other_dh_groups->destroy(other_dh_groups); + other_ke_methods->destroy(other_ke_methods); - return this->dh_group == KE_NONE || found; + return this->ke_method == KE_NONE || found; } /** @@ -486,107 +518,27 @@ static bool check_mode(private_child_create_t *this, host_t *i, host_t *r) } /** - * Install a CHILD_SA for usage, return value: - * - FAILED: no acceptable proposal - * - INVALID_ARG: diffie hellman group unacceptable + * Do traffic selector narrowing and check mode: + * - FAILED: mode mismatch * - NOT_FOUND: TS unacceptable */ -static status_t select_and_install(private_child_create_t *this, - bool no_dh, bool ike_auth) +static status_t narrow_and_check_ts(private_child_create_t *this, bool ike_auth) { - status_t status, status_i, status_o; - chunk_t nonce_i, nonce_r; - chunk_t encr_i = chunk_empty, encr_r = chunk_empty; - chunk_t integ_i = chunk_empty, integ_r = chunk_empty; linked_list_t *my_ts, *other_ts; host_t *me, *other; - array_t *kes = NULL; - proposal_selection_flag_t flags = 0; - - if (this->proposals == NULL) - { - DBG1(DBG_IKE, "SA payload missing in message"); - return FAILED; - } - if (this->tsi == NULL || this->tsr == NULL) - { - DBG1(DBG_IKE, "TS payloads missing in message"); - return NOT_FOUND; - } me = this->ike_sa->get_my_host(this->ike_sa); other = this->ike_sa->get_other_host(this->ike_sa); - if (no_dh) - { - flags |= PROPOSAL_SKIP_KE; - } - if (!this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN) && - !lib->settings->get_bool(lib->settings, "%s.accept_private_algs", - FALSE, lib->ns)) - { - flags |= PROPOSAL_SKIP_PRIVATE; - } - if (!lib->settings->get_bool(lib->settings, - "%s.prefer_configured_proposals", TRUE, lib->ns)) - { - flags |= PROPOSAL_PREFER_SUPPLIED; - } - this->proposal = this->config->select_proposal(this->config, - this->proposals, flags); - if (this->proposal == NULL) - { - DBG1(DBG_IKE, "no acceptable proposal found"); - charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_CHILD, - this->proposals); - return FAILED; - } - this->other_spi = this->proposal->get_spi(this->proposal); - - if (!this->initiator) - { - if (!allocate_spi(this)) - { - /* responder has no SPI allocated yet */ - DBG1(DBG_IKE, "allocating SPI failed"); - return FAILED; - } - this->proposal->set_spi(this->proposal, this->my_spi); - } this->child_sa->set_proposal(this->child_sa, this->proposal); - if (!this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, - this->dh_group)) - { - uint16_t group; - - if (this->proposal->get_algorithm(this->proposal, KEY_EXCHANGE_METHOD, - &group, NULL)) - { - DBG1(DBG_IKE, "DH group %N unacceptable, requesting %N", - key_exchange_method_names, this->dh_group, - key_exchange_method_names, group); - this->dh_group = group; - return INVALID_ARG; - } - /* the selected proposal does not use a DH group */ - DBG1(DBG_IKE, "ignoring KE exchange, agreed on a non-PFS proposal"); - DESTROY_IF(this->dh); - this->dh = NULL; - this->dh_group = KE_NONE; - } - if (this->initiator) { - nonce_i = this->my_nonce; - nonce_r = this->other_nonce; my_ts = narrow_ts(this, TRUE, this->tsi); other_ts = narrow_ts(this, FALSE, this->tsr); } else { - nonce_r = this->my_nonce; - nonce_i = this->other_nonce; my_ts = narrow_ts(this, TRUE, this->tsr); other_ts = narrow_ts(this, FALSE, this->tsi); } @@ -621,6 +573,7 @@ static status_t select_and_install(private_child_create_t *this, this->tsr->destroy_offset(this->tsr, offsetof(traffic_selector_t, destroy)); this->tsi->destroy_offset(this->tsi, offsetof(traffic_selector_t, destroy)); + if (this->initiator) { this->tsi = my_ts; @@ -643,9 +596,38 @@ static status_t select_and_install(private_child_create_t *this, this->mode = MODE_TUNNEL; } } + return SUCCESS; +} - if (!this->initiator) +/** + * Install a CHILD_SA: + * - FAILED: failure to install SAs + * - NOT_FOUND: TS unacceptable (only responder), or failure to install policies + */ +static status_t install_child_sa(private_child_create_t *this) +{ + status_t status, status_i, status_o; + chunk_t nonce_i, nonce_r; + chunk_t encr_i = chunk_empty, encr_r = chunk_empty; + chunk_t integ_i = chunk_empty, integ_r = chunk_empty; + linked_list_t *my_ts, *other_ts; + + if (this->initiator) { + nonce_i = this->my_nonce; + nonce_r = this->other_nonce; + + my_ts = this->tsi; + other_ts = this->tsr; + } + else + { + nonce_i = this->other_nonce; + nonce_r = this->my_nonce; + + my_ts = this->tsr; + other_ts = this->tsi; + /* use a copy of the traffic selectors, as the POST hook should not * change payloads */ my_ts = this->tsr->clone_offset(this->tsr, @@ -673,7 +655,9 @@ static status_t select_and_install(private_child_create_t *this, /* addresses might have changed since we originally sent the request, update * them before we configure any policies and install the SAs */ - this->child_sa->update(this->child_sa, me, other, NULL, + this->child_sa->update(this->child_sa, + this->ike_sa->get_my_host(this->ike_sa), + this->ike_sa->get_other_host(this->ike_sa), NULL, this->ike_sa->has_condition(this->ike_sa, COND_NAT_ANY)); this->child_sa->set_policies(this->child_sa, my_ts, other_ts); @@ -691,12 +675,8 @@ static status_t select_and_install(private_child_create_t *this, this->ipcomp = IPCOMP_NONE; } status_i = status_o = FAILED; - if (this->dh) - { - array_insert_create(&kes, ARRAY_HEAD, this->dh); - } if (this->keymat->derive_child_keys(this->keymat, this->proposal, - kes, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r)) + this->kes, nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r)) { if (this->initiator) { @@ -711,9 +691,10 @@ static status_t select_and_install(private_child_create_t *this, TRUE, this->tfcv3); } if (this->rekey) - { /* during rekeyings we install the outbound SA and/or policies - * separately: as responder when we receive the delete for the old - * SA, as initiator pretty much immediately in the ike-rekey task, + { + /* during rekeyings we install the outbound SA and/or policies + * separately: as responder, when we receive the delete for the old + * SA, as initiator, pretty much immediately in the ike-rekey task, * unless there was a rekey collision that we lost */ if (this->initiator) { @@ -769,14 +750,13 @@ static status_t select_and_install(private_child_create_t *this, this->initiator, encr_i, encr_r, integ_i, integ_r); charon->bus->child_keys(charon->bus, this->child_sa, - this->initiator, kes, nonce_i, nonce_r); + this->initiator, this->kes, nonce_i, nonce_r); } } chunk_clear(&integ_i); chunk_clear(&integ_r); chunk_clear(&encr_i); chunk_clear(&encr_r); - array_destroy(kes); if (status != SUCCESS) { @@ -813,6 +793,92 @@ static status_t select_and_install(private_child_create_t *this, return SUCCESS; } +/** + * Select a proposal + */ +static bool select_proposal(private_child_create_t *this, bool no_ke) +{ + proposal_selection_flag_t flags = 0; + + if (!this->proposals) + { + DBG1(DBG_IKE, "SA payload missing in message"); + return FALSE; + } + + if (no_ke) + { + flags |= PROPOSAL_SKIP_KE; + } + if (!this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN) && + !lib->settings->get_bool(lib->settings, "%s.accept_private_algs", + FALSE, lib->ns)) + { + flags |= PROPOSAL_SKIP_PRIVATE; + } + if (!lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns)) + { + flags |= PROPOSAL_PREFER_SUPPLIED; + } + this->proposal = this->config->select_proposal(this->config, + this->proposals, flags); + if (!this->proposal) + { + DBG1(DBG_IKE, "no acceptable proposal found"); + charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_CHILD, + this->proposals); + return FALSE; + } + return TRUE; +} + +/** + * Add a KE payload if a key exchange is used. As responder we might already + * have stored the object in the list of completed exchanges. + */ +static bool add_ke_payload(private_child_create_t *this, + message_t *message) +{ + key_exchange_t *ke; + ke_payload_t *pld; + + if (this->ke) + { + ke = this->ke; + } + else if (!array_get(this->kes, ARRAY_TAIL, &ke)) + { + return TRUE; + } + + pld = ke_payload_create_from_key_exchange(PLV2_KEY_EXCHANGE, ke); + if (!pld) + { + DBG1(DBG_IKE, "creating KE payload failed"); + return FALSE; + } + message->add_payload(message, (payload_t*)pld); + return TRUE; +} + +/** + * Build payloads in additional exchanges when using multiple key exchanges + */ +static bool build_payloads_multi_ke(private_child_create_t *this, + message_t *message) +{ + if (!add_ke_payload(this, message)) + { + return FALSE; + } + if (this->link.ptr) + { + message->add_notify(message, FALSE, ADDITIONAL_KEY_EXCHANGE, this->link); + } + return TRUE; +} + /** * build the payloads for the message */ @@ -820,11 +886,14 @@ static bool build_payloads(private_child_create_t *this, message_t *message) { sa_payload_t *sa_payload; nonce_payload_t *nonce_payload; - ke_payload_t *ke_payload; ts_payload_t *ts_payload; kernel_feature_t features; - /* add SA payload */ + if (message->get_exchange_type(message) == IKE_FOLLOWUP_KE) + { + return build_payloads_multi_ke(this, message); + } + if (this->initiator) { sa_payload = sa_payload_create_from_proposals_v2(this->proposals); @@ -843,17 +912,14 @@ static bool build_payloads(private_child_create_t *this, message_t *message) message->add_payload(message, (payload_t*)nonce_payload); } - /* diffie hellman exchange, if PFS enabled */ - if (this->dh) + if (this->link.ptr) { - ke_payload = ke_payload_create_from_key_exchange(PLV2_KEY_EXCHANGE, - this->dh); - if (!ke_payload) - { - DBG1(DBG_IKE, "creating KE payload failed"); - return FALSE; - } - message->add_payload(message, (payload_t*)ke_payload); + message->add_notify(message, FALSE, ADDITIONAL_KEY_EXCHANGE, this->link); + } + + if (!add_ke_payload(this, message)) + { + return FALSE; } /* add TSi/TSr payloads */ @@ -962,6 +1028,180 @@ static void handle_notify(private_child_create_t *this, notify_payload_t *notify } } +/** + * Collect all key exchanges from the proposal + */ +static void determine_key_exchanges(private_child_create_t *this) +{ + transform_type_t t = KEY_EXCHANGE_METHOD; + uint16_t alg; + int i = 1; + + if (!this->proposal->get_algorithm(this->proposal, t, &alg, NULL)) + { /* no PFS */ + return; + } + + this->key_exchanges[0].type = t; + this->key_exchanges[0].method = alg; + + for (t = ADDITIONAL_KEY_EXCHANGE_1; t <= ADDITIONAL_KEY_EXCHANGE_7; t++) + { + if (this->proposal->get_algorithm(this->proposal, t, &alg, NULL)) + { + this->key_exchanges[i].type = t; + this->key_exchanges[i].method = alg; + i++; + } + } +} + +/** + * Check if additional key exchanges are required + */ +static bool additional_key_exchange_required(private_child_create_t *this) +{ + int i; + + for (i = this->ke_index; i < MAX_KEY_EXCHANGES; i++) + { + if (this->key_exchanges[i].type && !this->key_exchanges[i].done) + { + return TRUE; + } + } + return FALSE; +} + +/** + * Clear data on key exchanges + */ +static void clear_key_exchanges(private_child_create_t *this) +{ + int i; + + for (i = 0; i < MAX_KEY_EXCHANGES; i++) + { + this->key_exchanges[i].type = 0; + this->key_exchanges[i].method = 0; + this->key_exchanges[i].done = FALSE; + } + this->ke_index = 0; + + array_destroy_offset(this->kes, offsetof(key_exchange_t, destroy)); + this->kes = NULL; +} + +/** + * Process a KE payload + */ +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); + + /* the proposal is selected after processing the KE payload, so this is + * only relevant for additional key exchanges */ + if (method && method != received) + { + DBG1(DBG_IKE, "key exchange method in received payload %N doesn't " + "match negotiated %N", key_exchange_method_names, received, + key_exchange_method_names, method); + this->ke_failed = TRUE; + return; + } + + this->ke_method = received; + + if (!this->initiator) + { + DESTROY_IF(this->ke); + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + received); + if (!this->ke) + { + DBG1(DBG_IKE, "key exchange method %N not supported", + key_exchange_method_names, received); + } + } + else if (this->ke) + { + if (this->ke->get_method(this->ke) != received) + { + DBG1(DBG_IKE, "key exchange method %N in received payload doesn't " + "match %N", key_exchange_method_names, received, + key_exchange_method_names, this->ke->get_method(this->ke)); + this->ke_failed = TRUE; + } + } + + if (this->ke && !this->ke_failed) + { + if (!this->ke->set_public_key(this->ke, ke->get_key_exchange_data(ke))) + { + DBG1(DBG_IKE, "applying key exchange public key failed"); + this->ke_failed = TRUE; + } + } +} + +/** + * Check if the proposed KE method in CREATE_CHILD_SA (received via KE payload) + * is valid according to the selected proposal. + */ +static bool check_ke_method(private_child_create_t *this, uint16_t *req) +{ + uint16_t alg; + + if (!this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD, + this->ke_method)) + { + if (this->proposal->get_algorithm(this->proposal, KEY_EXCHANGE_METHOD, + &alg, NULL)) + { + if (req) + { + *req = alg; + } + return FALSE; + } + /* the selected proposal does not use a key exchange method */ + DBG1(DBG_IKE, "ignoring KE payload, agreed on a non-PFS proposal"); + DESTROY_IF(this->ke); + this->ke = NULL; + this->ke_method = KE_NONE; + /* ignore errors that occurred while handling the KE payload */ + this->ke_failed = FALSE; + } + return TRUE; +} + +/** + * Check if the proposed key exchange method is valid as responder or whether + * we should request another KE payload. + */ +static bool check_ke_method_r(private_child_create_t *this, message_t *message) +{ + uint16_t alg; + + if (!check_ke_method(this, &alg)) + { + DBG1(DBG_IKE, "key exchange method %N unacceptable, requesting %N", + key_exchange_method_names, this->ke_method, + key_exchange_method_names, alg); + alg = htons(alg); + message->add_notify(message, FALSE, INVALID_KE_PAYLOAD, + chunk_from_thing(alg)); + return FALSE; + } + else if (this->ke_method != KE_NONE && !this->ke) + { + message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); + return FALSE; + } + return TRUE; +} + /** * Read payloads from message */ @@ -970,7 +1210,6 @@ static void process_payloads(private_child_create_t *this, message_t *message) enumerator_t *enumerator; payload_t *payload; sa_payload_t *sa_payload; - ke_payload_t *ke_payload; ts_payload_t *ts_payload; /* defaults to TUNNEL mode */ @@ -986,24 +1225,7 @@ static void process_payloads(private_child_create_t *this, message_t *message) this->proposals = sa_payload->get_proposals(sa_payload); break; case PLV2_KEY_EXCHANGE: - ke_payload = (ke_payload_t*)payload; - if (!this->initiator) - { - this->dh_group = ke_payload->get_key_exchange_method( - ke_payload); - this->dh = this->keymat->keymat.create_ke( - &this->keymat->keymat, this->dh_group); - } - else if (this->dh) - { - this->dh_failed = this->dh->get_method(this->dh) != - ke_payload->get_key_exchange_method(ke_payload); - } - if (this->dh && !this->dh_failed) - { - this->dh_failed = !this->dh->set_public_key(this->dh, - ke_payload->get_key_exchange_data(ke_payload)); - } + process_ke_payload(this, (ke_payload_t*)payload); break; case PLV2_TS_INITIATOR: ts_payload = (ts_payload_t*)payload; @@ -1149,6 +1371,36 @@ static bool check_for_generic_label(private_child_create_t *this) return FALSE; } +METHOD(task_t, build_i_multi_ke, status_t, + private_child_create_t *this, message_t *message) +{ + key_exchange_method_t method; + + message->set_exchange_type(message, IKE_FOLLOWUP_KE); + DESTROY_IF(this->ke); + method = this->key_exchanges[this->ke_index].method; + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + method); + if (!this->ke) + { + DBG1(DBG_IKE, "negotiated key exchange method %N not supported", + key_exchange_method_names, method); + return FAILED; + } + if (!this->link.ptr) + { + DBG1(DBG_IKE, "%N notify missing", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + return FAILED; + } + + if (!build_payloads_multi_ke(this, message)) + { + return FAILED; + } + return NEED_MORE; +} + METHOD(task_t, build_i, status_t, private_child_create_t *this, message_t *message) { @@ -1156,6 +1408,7 @@ METHOD(task_t, build_i, status_t, host_t *vip; peer_cfg_t *peer_cfg; linked_list_t *list; + bool no_ke = TRUE; switch (message->get_exchange_type(message)) { @@ -1167,11 +1420,7 @@ METHOD(task_t, build_i, status_t, message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); return SUCCESS; } - if (!this->retry && this->dh_group == KE_NONE) - { /* during a rekeying the group might already be set */ - this->dh_group = this->config->get_algorithm(this->config, - KEY_EXCHANGE_METHOD); - } + no_ke = FALSE; break; case IKE_AUTH: switch (defer_child_sa(this)) @@ -1253,8 +1502,7 @@ METHOD(task_t, build_i, status_t, this->child.label->get_string(this->child.label)); } - this->proposals = this->config->get_proposals(this->config, - this->dh_group == KE_NONE); + this->proposals = this->config->get_proposals(this->config, no_ke); this->mode = this->config->get_mode(this->config); this->child.if_id_in_def = this->ike_sa->get_if_id(this->ike_sa, TRUE); @@ -1289,22 +1537,35 @@ METHOD(task_t, build_i, status_t, if (!allocate_spi(this)) { - DBG1(DBG_IKE, "unable to allocate SPIs from kernel"); return FAILED; } + if (!no_ke && !this->retry) + { /* during a rekeying the method might already be set */ + if (this->ke_method == KE_NONE) + { + this->ke_method = this->config->get_algorithm(this->config, + KEY_EXCHANGE_METHOD); + } + } + if (!update_and_check_proposals(this)) { - DBG1(DBG_IKE, "requested DH group %N not contained in any of our " - "proposals", - key_exchange_method_names, this->dh_group); + DBG1(DBG_IKE, "requested key exchange method %N not contained in any " + "of our proposals", key_exchange_method_names, this->ke_method); return FAILED; } - if (this->dh_group != KE_NONE) + if (this->ke_method != KE_NONE) { - this->dh = this->keymat->keymat.create_ke(&this->keymat->keymat, - this->dh_group); + this->ke = this->keymat->keymat.create_ke(&this->keymat->keymat, + this->ke_method); + if (!this->ke) + { + DBG1(DBG_IKE, "selected key exchange method %N not supported", + key_exchange_method_names, this->ke_method); + return FAILED; + } } if (this->config->has_option(this->config, OPT_IPCOMP)) @@ -1339,6 +1600,67 @@ METHOD(task_t, build_i, status_t, return NEED_MORE; } +/** + * Process payloads in a IKE_FOLLOWUP_KE message or a CREATE_CHILD_SA response + */ +static void process_link(private_child_create_t *this, message_t *message) +{ + notify_payload_t *notify; + chunk_t link; + + notify = message->get_notify(message, ADDITIONAL_KEY_EXCHANGE); + if (notify) + { + link = notify->get_notification_data(notify); + if (this->initiator) + { + chunk_free(&this->link); + this->link = chunk_clone(link); + } + else if (!chunk_equals_const(this->link, link)) + { + DBG1(DBG_IKE, "data in %N notify doesn't match", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + chunk_free(&this->link); + } + } + else + { + chunk_free(&this->link); + } +} + +/** + * Process payloads in additional exchanges when using multiple key exchanges + */ +static void process_payloads_multi_ke(private_child_create_t *this, + message_t *message) +{ + ke_payload_t *ke; + + ke = (ke_payload_t*)message->get_payload(message, PLV2_KEY_EXCHANGE); + if (ke) + { + process_ke_payload(this, ke); + } + else + { + DBG1(DBG_IKE, "KE payload missing in message"); + this->ke_failed = TRUE; + } + process_link(this, message); +} + +METHOD(task_t, process_r_multi_ke, status_t, + private_child_create_t *this, message_t *message) +{ + if (message->get_exchange_type(message) == IKE_FOLLOWUP_KE) + { + process_payloads_multi_ke(this, message); + } + return NEED_MORE; +} + METHOD(task_t, process_r, status_t, private_child_create_t *this, message_t *message) { @@ -1533,12 +1855,115 @@ static bool select_label(private_child_create_t *this) return TRUE; } +/** + * Called when a key exchange is done, returns TRUE once all are done. + */ +static bool key_exchange_done(private_child_create_t *this) +{ + bool additional_ke; + + if (!this->ke) + { + return TRUE; + } + + this->key_exchanges[this->ke_index++].done = TRUE; + additional_ke = additional_key_exchange_required(this); + + array_insert_create(&this->kes, ARRAY_TAIL, this->ke); + this->ke = NULL; + + return additional_ke ? FALSE : TRUE; +} + +/** + * Complete the current key exchange and install the CHILD_SA if all are done + * as responder. + */ +static bool key_exchange_done_and_install_r(private_child_create_t *this, + message_t *message, bool ike_auth) +{ + bool all_done = FALSE; + + if (key_exchange_done(this)) + { + chunk_clear(&this->link); + all_done = TRUE; + } + else if (!this->link.ptr) + { + this->link = chunk_clone(chunk_from_chars(0x42)); + } + + if (!build_payloads(this, message)) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + handle_child_sa_failure(this, message); + return TRUE; + } + + if (all_done) + { + switch (install_child_sa(this)) + { + case SUCCESS: + break; + case NOT_FOUND: + message->add_notify(message, TRUE, TS_UNACCEPTABLE, + chunk_empty); + handle_child_sa_failure(this, message); + return TRUE; + case FAILED: + default: + message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, + chunk_empty); + handle_child_sa_failure(this, message); + return TRUE; + } + if (!this->rekey) + { /* invoke the child_up() hook if we are not rekeying */ + charon->bus->child_updown(charon->bus, this->child_sa, TRUE); + } + } + return all_done; +} + +METHOD(task_t, build_r_multi_ke, status_t, + private_child_create_t *this, message_t *message) +{ + if (!this->ke) + { + message->add_notify(message, FALSE, INVALID_SYNTAX, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + if (this->ke_failed) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + if (!this->link.ptr) + { + DBG1(DBG_IKE, "%N notify missing", notify_type_names, + ADDITIONAL_KEY_EXCHANGE); + message->add_notify(message, FALSE, STATE_NOT_FOUND, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + if (!key_exchange_done_and_install_r(this, message, FALSE)) + { + return NEED_MORE; + } + return SUCCESS; +} + METHOD(task_t, build_r, status_t, private_child_create_t *this, message_t *message) { payload_t *payload; enumerator_t *enumerator; - bool no_dh = TRUE, ike_auth = FALSE; + bool no_ke = TRUE, ike_auth = FALSE; switch (message->get_exchange_type(message)) { @@ -1551,18 +1976,11 @@ METHOD(task_t, build_r, status_t, chunk_empty); return SUCCESS; } - if (this->dh_failed) - { - DBG1(DBG_IKE, "applying DH public value failed"); - message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, - chunk_empty); - return SUCCESS; - } - no_dh = FALSE; + no_ke = FALSE; break; case IKE_AUTH: if (!this->ike_sa->has_condition(this->ike_sa, COND_AUTHENTICATED)) - { /* wait until all authentication round completed */ + { /* wait until all authentication rounds completed */ return NEED_MORE; } if (this->ike_sa->has_condition(this->ike_sa, COND_REDIRECTED)) @@ -1601,15 +2019,23 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } - if (this->config == NULL) + if (!this->config) { this->config = select_child_cfg(this); } - if (this->config == NULL) + if (!this->config || !this->tsi || !this->tsr) { - DBG1(DBG_IKE, "traffic selectors %#R === %#R unacceptable", - this->tsr, this->tsi); - charon->bus->alert(charon->bus, ALERT_TS_MISMATCH, this->tsi, this->tsr); + if (!this->tsi || !this->tsr) + { + DBG1(DBG_IKE, "TS payloads missing in message"); + } + else + { + DBG1(DBG_IKE, "traffic selectors %#R === %#R unacceptable", + this->tsr, this->tsi); + charon->bus->alert(charon->bus, ALERT_TS_MISMATCH, this->tsi, + this->tsr); + } message->add_notify(message, FALSE, TS_UNACCEPTABLE, chunk_empty); handle_child_sa_failure(this, message); return SUCCESS; @@ -1641,6 +2067,29 @@ METHOD(task_t, build_r, status_t, } enumerator->destroy(enumerator); + if (!select_proposal(this, no_ke)) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + + if (!check_ke_method_r(this, message)) + { /* the peer will retry, we don't handle this as failure */ + return SUCCESS; + } + + /* this flag might get reset if the check above notices a proposal without + * KE was selected */ + if (this->ke_failed) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + + determine_key_exchanges(this); + if (!select_label(this)) { message->add_notify(message, FALSE, TS_UNACCEPTABLE, chunk_empty); @@ -1655,6 +2104,15 @@ METHOD(task_t, build_r, status_t, this->ike_sa->get_other_host(this->ike_sa), this->config, &this->child); + this->other_spi = this->proposal->get_spi(this->proposal); + if (!allocate_spi(this)) + { + message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); + handle_child_sa_failure(this, message); + return SUCCESS; + } + this->proposal->set_spi(this->proposal, this->my_spi); + if (this->ipcomp_received != IPCOMP_NONE) { if (this->config->has_option(this->config, OPT_IPCOMP)) @@ -1668,7 +2126,7 @@ METHOD(task_t, build_r, status_t, } } - switch (select_and_install(this, no_dh, ike_auth)) + switch (narrow_and_check_ts(this, ike_auth)) { case SUCCESS: break; @@ -1676,13 +2134,6 @@ METHOD(task_t, build_r, status_t, message->add_notify(message, FALSE, TS_UNACCEPTABLE, chunk_empty); handle_child_sa_failure(this, message); return SUCCESS; - case INVALID_ARG: - { - uint16_t group = htons(this->dh_group); - message->add_notify(message, FALSE, INVALID_KE_PAYLOAD, - chunk_from_thing(group)); - return SUCCESS; - } case FAILED: default: message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); @@ -1690,16 +2141,11 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } - if (!build_payloads(this, message)) + if (!key_exchange_done_and_install_r(this, message, ike_auth)) { - message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty); - handle_child_sa_failure(this, message); - return SUCCESS; - } - - if (!this->rekey) - { /* invoke the child_up() hook if we are not rekeying */ - charon->bus->child_updown(charon->bus, this->child_sa, TRUE); + this->public.task.build = _build_r_multi_ke; + this->public.task.process = _process_r_multi_ke; + return NEED_MORE; } return SUCCESS; } @@ -1755,12 +2201,50 @@ static status_t delete_failed_sa(private_child_create_t *this) return SUCCESS; } +/** + * Complete the current key exchange and install the CHILD_SA if all are done + * as initiator. + */ +static status_t key_exchange_done_and_install_i(private_child_create_t *this, + message_t *message, bool ike_auth) +{ + if (key_exchange_done(this)) + { + if (install_child_sa(this) == SUCCESS) + { + if (!this->rekey) + { /* invoke the child_up() hook if we are not rekeying */ + charon->bus->child_updown(charon->bus, this->child_sa, + TRUE); + } + return SUCCESS; + } + handle_child_sa_failure(this, message); + return delete_failed_sa(this); + } + return NEED_MORE; +} + +METHOD(task_t, process_i_multi_ke, status_t, + private_child_create_t *this, message_t *message) +{ + process_payloads_multi_ke(this, message); + + if (this->ke_failed) + { + handle_child_sa_failure(this, message); + return delete_failed_sa(this); + } + + return key_exchange_done_and_install_i(this, message, FALSE); +} + METHOD(task_t, process_i, status_t, private_child_create_t *this, message_t *message) { enumerator_t *enumerator; payload_t *payload; - bool no_dh = TRUE, ike_auth = FALSE; + bool no_ke = TRUE, ike_auth = FALSE; switch (message->get_exchange_type(message)) { @@ -1768,11 +2252,11 @@ 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_dh = FALSE; + no_ke = FALSE; break; case IKE_AUTH: if (!this->ike_sa->has_condition(this->ike_sa, COND_AUTHENTICATED)) - { /* wait until all authentication round completed */ + { /* wait until all authentication rounds completed */ return NEED_MORE; } if (defer_child_sa(this) == NEED_MORE) @@ -1828,28 +2312,27 @@ METHOD(task_t, process_i, status_t, case INVALID_KE_PAYLOAD: { chunk_t data; - uint16_t group = KE_NONE; + uint16_t alg = KE_NONE; data = notify->get_notification_data(notify); - if (data.len == sizeof(group)) + if (data.len == sizeof(alg)) { - memcpy(&group, data.ptr, data.len); - group = ntohs(group); + alg = untoh16(data.ptr); } if (this->retry) { - DBG1(DBG_IKE, "already retried with DH group %N, " - "ignore requested %N", key_exchange_method_names, - this->dh_group, key_exchange_method_names, group); + DBG1(DBG_IKE, "already retried with key exchange method " + "%N, ignore requested %N", key_exchange_method_names, + this->ke_method, key_exchange_method_names, alg); handle_child_sa_failure(this, message); /* an error in CHILD_SA creation is not critical */ return SUCCESS; } - DBG1(DBG_IKE, "peer didn't accept DH group %N, " + DBG1(DBG_IKE, "peer didn't accept key exchange method %N, " "it requested %N", key_exchange_method_names, - this->dh_group, key_exchange_method_names, group); + this->ke_method, key_exchange_method_names, alg); this->retry = TRUE; - this->dh_group = group; + this->ke_method = alg; this->child_sa->set_state(this->child_sa, CHILD_RETRYING); this->public.task.migrate(&this->public.task, this->ike_sa); enumerator->destroy(enumerator); @@ -1899,31 +2382,54 @@ METHOD(task_t, process_i, status_t, return delete_failed_sa(this); } - if (this->dh_failed) + if (!select_proposal(this, no_ke)) { - DBG1(DBG_IKE, "applying DH public value failed"); handle_child_sa_failure(this, message); return delete_failed_sa(this); } + this->other_spi = this->proposal->get_spi(this->proposal); + this->proposal->set_spi(this->proposal, this->my_spi); + + if (!check_ke_method(this, NULL)) + { + handle_child_sa_failure(this, message); + return delete_failed_sa(this); + } + + if (this->ke_failed) + { + handle_child_sa_failure(this, message); + return delete_failed_sa(this); + } + + determine_key_exchanges(this); + if (!select_label(this)) { handle_child_sa_failure(this, message); return delete_failed_sa(this); } - if (select_and_install(this, no_dh, ike_auth) == SUCCESS) - { - if (!this->rekey) - { /* invoke the child_up() hook if we are not rekeying */ - charon->bus->child_updown(charon->bus, this->child_sa, TRUE); - } - } - else + if (narrow_and_check_ts(this, ike_auth) != SUCCESS) { handle_child_sa_failure(this, message); return delete_failed_sa(this); } + + if (key_exchange_done_and_install_i(this, message, ike_auth) == NEED_MORE) + { + /* if the installation failed, we delete the failed SA, i.e. build() was + * changed, otherwise, we switch to multi-KE mode */ + if (this->public.task.build == _build_i) + { + /* if we don't have the notify, we handle it in build() */ + process_link(this, message); + this->public.task.build = _build_i_multi_ke; + this->public.task.process = _process_i_multi_ke; + } + return NEED_MORE; + } return SUCCESS; } @@ -1963,10 +2469,10 @@ METHOD(child_create_t, use_label, void, this->child.label = label ? label->clone(label) : NULL; } -METHOD(child_create_t, use_dh_group, void, - private_child_create_t *this, key_exchange_method_t dh_group) +METHOD(child_create_t, use_ke_method, void, + private_child_create_t *this, key_exchange_method_t ke_method) { - this->dh_group = dh_group; + this->ke_method = ke_method; } METHOD(child_create_t, get_child, child_sa_t*, @@ -2007,6 +2513,7 @@ METHOD(task_t, migrate, void, { chunk_free(&this->my_nonce); chunk_free(&this->other_nonce); + chunk_free(&this->link); if (this->tsr) { this->tsr->destroy_offset(this->tsr, offsetof(traffic_selector_t, destroy)); @@ -2026,15 +2533,16 @@ METHOD(task_t, migrate, void, DESTROY_IF(this->child_sa); DESTROY_IF(this->proposal); DESTROY_IF(this->nonceg); - DESTROY_IF(this->dh); - this->dh_failed = FALSE; + DESTROY_IF(this->ke); + this->ke_failed = FALSE; + clear_key_exchanges(this); if (this->proposals) { this->proposals->destroy_offset(this->proposals, offsetof(proposal_t, destroy)); } if (!this->rekey && !this->retry) { - this->dh_group = KE_NONE; + this->ke_method = KE_NONE; } this->ike_sa = ike_sa; this->keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa); @@ -2042,7 +2550,7 @@ METHOD(task_t, migrate, void, this->proposals = NULL; this->tsi = NULL; this->tsr = NULL; - this->dh = NULL; + this->ke = NULL; this->nonceg = NULL; this->child_sa = NULL; this->mode = MODE_TUNNEL; @@ -2051,6 +2559,7 @@ METHOD(task_t, migrate, void, this->other_cpi = 0; this->established = FALSE; this->public.task.build = _build_i; + this->public.task.process = _process_i; } METHOD(task_t, destroy, void, @@ -2058,6 +2567,7 @@ METHOD(task_t, destroy, void, { chunk_free(&this->my_nonce); chunk_free(&this->other_nonce); + chunk_free(&this->link); if (this->tsr) { this->tsr->destroy_offset(this->tsr, offsetof(traffic_selector_t, destroy)); @@ -2085,7 +2595,8 @@ METHOD(task_t, destroy, void, DESTROY_IF(this->packet_tsi); DESTROY_IF(this->packet_tsr); DESTROY_IF(this->proposal); - DESTROY_IF(this->dh); + DESTROY_IF(this->ke); + clear_key_exchanges(this); if (this->proposals) { this->proposals->destroy_offset(this->proposals, offsetof(proposal_t, destroy)); @@ -2114,7 +2625,7 @@ child_create_t *child_create_create(ike_sa_t *ike_sa, .use_marks = _use_marks, .use_if_ids = _use_if_ids, .use_label = _use_label, - .use_dh_group = _use_dh_group, + .use_ke_method = _use_ke_method, .task = { .get_type = _get_type, .migrate = _migrate, @@ -2125,7 +2636,7 @@ child_create_t *child_create_create(ike_sa_t *ike_sa, .config = config, .packet_tsi = tsi ? tsi->clone(tsi) : NULL, .packet_tsr = tsr ? tsr->clone(tsr) : NULL, - .dh_group = KE_NONE, + .ke_method = KE_NONE, .keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa), .mode = MODE_TUNNEL, .tfcv3 = TRUE, diff --git a/src/libcharon/sa/ikev2/tasks/child_create.h b/src/libcharon/sa/ikev2/tasks/child_create.h index 62de4c686..2e8c8bed7 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.h +++ b/src/libcharon/sa/ikev2/tasks/child_create.h @@ -81,13 +81,13 @@ struct child_create_t { void (*use_label)(child_create_t *this, sec_label_t *label); /** - * Initially propose a specific DH group to override configuration. + * Initially propose a specific KE method to override configuration. * - * This is used during rekeying to prefer the previously negotiated group. + * This is used during rekeying to prefer the previously negotiated method. * - * @param dh_group DH group to use + * @param ke_method KE method to use */ - void (*use_dh_group)(child_create_t *this, key_exchange_method_t dh_group); + void (*use_ke_method)(child_create_t *this, key_exchange_method_t ke_method); /** * Get the lower of the two nonces, used for rekey collisions. diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 3ef175891..5883a5ef5 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -201,16 +201,16 @@ METHOD(task_t, build_i, status_t, if (!this->child_create) { proposal_t *proposal; - uint16_t dh_group; + uint16_t ke_method; this->child_create = child_create_create(this->ike_sa, config->get_ref(config), TRUE, NULL, NULL); proposal = this->child_sa->get_proposal(this->child_sa); if (proposal->get_algorithm(proposal, KEY_EXCHANGE_METHOD, - &dh_group, NULL)) - { /* reuse the DH group negotiated previously */ - this->child_create->use_dh_group(this->child_create, dh_group); + &ke_method, NULL)) + { /* reuse the KE method negotiated previously */ + this->child_create->use_ke_method(this->child_create, ke_method); } } reqid = this->child_sa->get_reqid_ref(this->child_sa); From c200bd166825ac825a344f35f119de3b4fb4a726 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 20 Jul 2018 14:12:48 +0200 Subject: [PATCH 30/46] unit-tests: Fix CHILD_SA rekey tests after INVALID_KE_PAYLOAD handling changed The responder doesn't create a CHILD_SA and allocate an SPI anymore when responding with an INVALID_KE_PAYLOAD notify. --- src/libcharon/tests/suites/test_child_rekey.c | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 6629cb378..1092d609e 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -189,8 +189,8 @@ START_TEST(test_regular_ke_invalid) assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, spi_b, CHILD_REKEYED); - assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_ipsec_sas_installed(b, spi_a, spi_b, 6); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, spi_a, spi_b, 5); assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ @@ -198,8 +198,8 @@ START_TEST(test_regular_ke_invalid) 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, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_ipsec_sas_installed(a, spi_a, 5, 6); + assert_child_sa_state(a, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, spi_a, 4, 5); assert_hook(); /* INFORMATIONAL { D } --> */ @@ -207,34 +207,34 @@ START_TEST(test_regular_ke_invalid) 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, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 2); - assert_ipsec_sas_installed(b, spi_b, 5, 6); + assert_ipsec_sas_installed(b, spi_b, 4, 5); 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, 5, CHILD_INSTALLED); + assert_child_sa_state(a, 4, CHILD_INSTALLED); assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, spi_a, 5, 6); + assert_ipsec_sas_installed(a, spi_a, 4, 5); 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, 5, 6); + assert_ipsec_sas_installed(a, 4, 5); destroy_rekeyed(b, spi_b); assert_child_sa_count(b, 1); - assert_ipsec_sas_installed(b, 5, 6); + assert_ipsec_sas_installed(b, 4, 5); /* child_updown */ assert_hook(); /* because the DH group should get reused another rekeying should complete * without additional exchange */ - initiate_rekey(a, 5); + initiate_rekey(a, 4); /* this should never get called as this results in a successful rekeying */ assert_hook_not_called(child_updown); @@ -242,47 +242,47 @@ START_TEST(test_regular_ke_invalid) assert_hook_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_child_sa_state(b, 6, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 8, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_ipsec_sas_installed(b, 5, 6, 8); + assert_child_sa_state(b, 5, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 4, 5, 7); assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ assert_hook_called(child_rekey); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 5, CHILD_DELETING, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_ipsec_sas_installed(a, 5, 7, 8); + assert_child_sa_state(a, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 4, 6, 7); assert_hook(); /* INFORMATIONAL { D } --> */ assert_hook_not_called(child_rekey); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_child_sa_state(b, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(b, 8, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 2); - assert_ipsec_sas_installed(b, 6, 7, 8); + assert_ipsec_sas_installed(b, 5, 6, 7); 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, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 7, CHILD_INSTALLED); + assert_child_sa_state(a, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED); assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, 5, 7, 8); + assert_ipsec_sas_installed(a, 4, 6, 7); assert_hook(); /* simulate the execution of the scheduled jobs */ - destroy_rekeyed(a, 5); + destroy_rekeyed(a, 4); assert_child_sa_count(a, 1); - assert_ipsec_sas_installed(a, 7, 8); - destroy_rekeyed(b, 6); + assert_ipsec_sas_installed(a, 6, 7); + destroy_rekeyed(b, 5); assert_child_sa_count(b, 1); - assert_ipsec_sas_installed(b, 7, 8); + assert_ipsec_sas_installed(b, 6, 7); /* child_updown */ assert_hook(); @@ -1145,14 +1145,14 @@ START_TEST(test_collision_ke_invalid) /* Eight nonces and SPIs are needed (SPI 1 and 2 are used for the initial * CHILD_SA): * N1/3 -----\ /----- N2/4 - * \--/-----> N3/5 - * N4/6 <-------/ /---- INVAL_KE + * \--/-----> N3/- + * N4/- <-------/ /---- INVAL_KE * INVAL_KE -----\ / * <-----\--/ - * N5/7 -----\ \-------> - * \ /---- N6/8 - * \--/----> N7/9 - * N8/10 <--------/ /---- ... + * N5/5 -----\ \-------> + * \ /---- N6/6 + * \--/----> N7/7 + * N8/8 <--------/ /---- ... * ... ------\ * * We test this four times, each time a different nonce is the lowest. @@ -1165,10 +1165,10 @@ START_TEST(test_collision_ke_invalid) /* SPIs of the kept CHILD_SA */ uint32_t spi_a, spi_b; } data[] = { - { { 0x00, 0xFF, 0xFF, 0xFF }, 7, 2,10, 8 }, - { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 8, 7, 9 }, - { { 0xFF, 0xFF, 0x00, 0xFF }, 7, 2,10, 8 }, - { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 8, 7, 9 }, + { { 0x00, 0xFF, 0xFF, 0xFF }, 5, 2, 8, 6 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 6, 5, 7 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 5, 2, 8, 6 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 6, 5, 7 }, }; /* make sure the nonces of the first try don't affect the retries */ @@ -1212,17 +1212,17 @@ START_TEST(test_collision_ke_invalid) /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 9); + assert_hook_rekey(child_rekey, 2, 7); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 9, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 10); + assert_hook_rekey(child_rekey, 1, 8); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a,10, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(a, 8, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ @@ -1368,13 +1368,13 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* Seven nonces and SPIs are needed (SPI 1 and 2 are used for the initial * CHILD_SA): * N1/3 -----\ /----- N2/4 - * \--/-----> N3/5 - * N4/6 <-------/ /---- INVAL_KE + * \--/-----> N3/- + * N4/- <-------/ /---- INVAL_KE * INVAL_KE -----\ / * <-----\--/ - * N5/7 -----\ \-------> - * <-----\--------- N6/8 - * N7/9 -------\-------> + * N5/5 -----\ \-------> + * <-----\--------- N6/6 + * N7/7 -------\-------> * <-------\------- DELETE * ... ------\ \-----> * /---- TEMP_FAIL @@ -1434,16 +1434,16 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 9); + assert_hook_rekey(child_rekey, 1, 7); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, 9, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_hook(); /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ - assert_hook_rekey(child_rekey, 2, 8); + assert_hook_rekey(child_rekey, 2, 6); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); - assert_child_sa_state(b, 8, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_hook(); /* we don't expect this hook to get called anymore */ @@ -1453,13 +1453,13 @@ START_TEST(test_collision_ke_invalid_delayed_retry) assert_single_notify(OUT, TEMPORARY_FAILURE); exchange_test_helper->process_message(exchange_test_helper, b, msg); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); - assert_child_sa_state(b, 8, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 9, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_scheduler(); @@ -1467,7 +1467,7 @@ START_TEST(test_collision_ke_invalid_delayed_retry) assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 9, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_scheduler(); @@ -1475,17 +1475,17 @@ START_TEST(test_collision_ke_invalid_delayed_retry) assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(b, 8, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 2); assert_scheduler(); /* simulate the execution of the scheduled jobs */ destroy_rekeyed(a, 1); assert_child_sa_count(a, 1); - assert_ipsec_sas_installed(a, 8, 9); + assert_ipsec_sas_installed(a, 6, 7); destroy_rekeyed(b, 2); assert_child_sa_count(b, 1); - assert_ipsec_sas_installed(b, 8, 9); + assert_ipsec_sas_installed(b, 6, 7); /* child_rekey/child_updown */ assert_hook(); From 95275d2fe5bd2047fce5acdb8c58edde8ed4b930 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 29 Jun 2020 14:12:05 +0200 Subject: [PATCH 31/46] child-rekey: Support CHILD_SA rekeying with multiple key exchanges --- src/libcharon/sa/ikev2/tasks/child_create.c | 4 + src/libcharon/sa/ikev2/tasks/child_delete.c | 5 - src/libcharon/sa/ikev2/tasks/child_rekey.c | 418 ++++++++++++++------ 3 files changed, 296 insertions(+), 131 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index a04b14d55..132c3de46 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -2196,6 +2196,10 @@ static status_t delete_failed_sa(private_child_create_t *this) { this->public.task.build = _build_i_delete; this->public.task.process = (void*)return_success; + /* destroying it here allows the rekey task to differentiate between + * this and the multi-KE case */ + this->child_sa->destroy(this->child_sa); + this->child_sa = NULL; return NEED_MORE; } return SUCCESS; diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index 2e2668bbe..eff0e6cc6 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -187,11 +187,6 @@ static void install_outbound(private_child_delete_t *this, DBG1(DBG_IKE, "CHILD_SA not found after rekeying"); return; } - if (this->initiator && is_redundant(this, child_sa)) - { /* if we won the rekey collision we don't want to install the - * redundant SA created by the peer */ - return; - } status = child_sa->install_outbound(child_sa); if (status != SUCCESS) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 5883a5ef5..6ffeb52c5 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2018 Tobias Brunner + * Copyright (C) 2009-2023 Tobias Brunner * Copyright (C) 2005-2007 Martin Willi * Copyright (C) 2005 Jan Hutter * @@ -84,13 +84,32 @@ struct private_child_rekey_t { task_t *collision; /** - * Indicate that peer destroyed the redundant child from collision. - * This happens if a peer's delete notification for the redundant - * child gets processed before the rekey job. If so, we must not - * touch the child created in the collision since it points to - * memory already freed. + * State flags */ - bool other_child_destroyed; + enum { + + /** + * Set if we use multiple key exchanges and already processed the + * CREATE_CHILD_SA response and started sending IKE_FOLLOWUP_KEs. + */ + CHILD_REKEY_FOLLOWUP_KE = (1<<0), + + /** + * Set if we adopted a completed passive task, otherwise (i.e. for + * multi-KE rekeyings) we just reference it. + */ + CHILD_REKEY_ADOPTED_PASSIVE = (1<<1), + + /** + * Indicate that the peer destroyed the redundant child from a + * collision. This happens if a peer's delete notification for the + * redundant child gets processed before the active rekey job is + * complete. If so, we must not touch the child created in the collision + * since it points to memory already freed. + */ + CHILD_REKEY_OTHER_DESTROYED = (1<<2), + + } flags; }; /** @@ -169,8 +188,6 @@ METHOD(task_t, build_i, status_t, private_child_rekey_t *this, message_t *message) { notify_payload_t *notify; - uint32_t reqid; - child_cfg_t *config; this->child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, this->spi, TRUE); @@ -194,15 +211,16 @@ METHOD(task_t, build_i, status_t, message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); return SUCCESS; } - config = this->child_sa->get_config(this->child_sa); - /* our CHILD_CREATE task does the hard work for us */ if (!this->child_create) { + child_cfg_t *config; proposal_t *proposal; uint16_t ke_method; + uint32_t reqid; + config = this->child_sa->get_config(this->child_sa); this->child_create = child_create_create(this->ike_sa, config->get_ref(config), TRUE, NULL, NULL); @@ -212,21 +230,21 @@ METHOD(task_t, build_i, status_t, { /* reuse the KE method negotiated previously */ this->child_create->use_ke_method(this->child_create, ke_method); } - } - reqid = this->child_sa->get_reqid_ref(this->child_sa); - if (reqid) - { - this->child_create->use_reqid(this->child_create, reqid); - charon->kernel->release_reqid(charon->kernel, reqid); - } - this->child_create->use_marks(this->child_create, + reqid = this->child_sa->get_reqid_ref(this->child_sa); + if (reqid) + { + this->child_create->use_reqid(this->child_create, reqid); + charon->kernel->release_reqid(charon->kernel, reqid); + } + this->child_create->use_marks(this->child_create, this->child_sa->get_mark(this->child_sa, TRUE).value, this->child_sa->get_mark(this->child_sa, FALSE).value); - this->child_create->use_if_ids(this->child_create, + this->child_create->use_if_ids(this->child_create, this->child_sa->get_if_id(this->child_sa, TRUE), this->child_sa->get_if_id(this->child_sa, FALSE)); - this->child_create->use_label(this->child_create, + this->child_create->use_label(this->child_create, this->child_sa->get_label(this->child_sa)); + } if (this->child_create->task.build(&this->child_create->task, message) != NEED_MORE) @@ -259,14 +277,48 @@ METHOD(task_t, process_r, status_t, return NEED_MORE; } +/** + * Check if we are actively rekeying and, optionally, if we already sent an + * IKE_FOLLOWUP_KE message. + */ +static bool actively_rekeying(private_child_rekey_t *this, bool *followup_sent) +{ + enumerator_t *enumerator; + task_t *task; + bool found = FALSE; + + enumerator = this->ike_sa->create_task_enumerator(this->ike_sa, + TASK_QUEUE_ACTIVE); + while (enumerator->enumerate(enumerator, (void**)&task)) + { + if (task->get_type(task) == TASK_CHILD_REKEY) + { + private_child_rekey_t *rekey = (private_child_rekey_t*)task; + + if (this->child_sa == rekey->child_sa) + { + if (followup_sent) + { + *followup_sent = rekey->flags & CHILD_REKEY_FOLLOWUP_KE; + } + found = TRUE; + } + break; + } + } + enumerator->destroy(enumerator); + return found; +} + METHOD(task_t, build_r, status_t, private_child_rekey_t *this, message_t *message) { notify_payload_t *notify; child_cfg_t *config; - uint32_t reqid; - child_sa_state_t state; child_sa_t *child_sa; + child_sa_state_t state = CHILD_INSTALLED; + uint32_t reqid; + bool followup_sent; if (!this->child_sa) { @@ -284,118 +336,195 @@ METHOD(task_t, build_r, status_t, message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } - - /* let the CHILD_CREATE task build the response */ - reqid = this->child_sa->get_reqid_ref(this->child_sa); - if (reqid) + if (actively_rekeying(this, &followup_sent) && followup_sent) { - this->child_create->use_reqid(this->child_create, reqid); - charon->kernel->release_reqid(charon->kernel, reqid); - } - this->child_create->use_marks(this->child_create, - this->child_sa->get_mark(this->child_sa, TRUE).value, - this->child_sa->get_mark(this->child_sa, FALSE).value); - this->child_create->use_if_ids(this->child_create, - this->child_sa->get_if_id(this->child_sa, TRUE), - this->child_sa->get_if_id(this->child_sa, FALSE)); - this->child_create->use_label(this->child_create, - this->child_sa->get_label(this->child_sa)); - config = this->child_sa->get_config(this->child_sa); - this->child_create->set_config(this->child_create, config->get_ref(config)); - this->child_create->task.build(&this->child_create->task, message); - - state = this->child_sa->get_state(this->child_sa); - this->child_sa->set_state(this->child_sa, CHILD_REKEYING); - - if (message->get_payload(message, PLV2_SECURITY_ASSOCIATION) == NULL) - { /* rekeying failed, reuse old child */ - this->child_sa->set_state(this->child_sa, state); + DBG1(DBG_IKE, "peer initiated rekeying, but we did too and already " + "sent IKE_FOLLOWUP_KE"); + message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } - child_sa = this->child_create->get_child(this->child_create); - this->child_sa->set_state(this->child_sa, CHILD_REKEYED); - this->child_sa->set_rekey_spi(this->child_sa, - child_sa->get_spi(child_sa, FALSE)); + if (message->get_exchange_type(message) == CREATE_CHILD_SA) + { + reqid = this->child_sa->get_reqid_ref(this->child_sa); + if (reqid) + { + this->child_create->use_reqid(this->child_create, reqid); + charon->kernel->release_reqid(charon->kernel, reqid); + } + this->child_create->use_marks(this->child_create, + this->child_sa->get_mark(this->child_sa, TRUE).value, + this->child_sa->get_mark(this->child_sa, FALSE).value); + this->child_create->use_if_ids(this->child_create, + this->child_sa->get_if_id(this->child_sa, TRUE), + this->child_sa->get_if_id(this->child_sa, FALSE)); + this->child_create->use_label(this->child_create, + this->child_sa->get_label(this->child_sa)); + config = this->child_sa->get_config(this->child_sa); + this->child_create->set_config(this->child_create, + config->get_ref(config)); + state = this->child_sa->get_state(this->child_sa); + this->child_sa->set_state(this->child_sa, CHILD_REKEYING); + } - /* invoke rekey hook */ - charon->bus->child_rekey(charon->bus, this->child_sa, - this->child_create->get_child(this->child_create)); + if (this->child_create->task.build(&this->child_create->task, + message) == NEED_MORE) + { + /* additional key exchanges */ + this->flags |= CHILD_REKEY_FOLLOWUP_KE; + return NEED_MORE; + } + + child_sa = this->child_create->get_child(this->child_create); + if (child_sa && child_sa->get_state(child_sa) == CHILD_INSTALLED) + { + this->child_sa->set_state(this->child_sa, CHILD_REKEYED); + this->child_sa->set_rekey_spi(this->child_sa, + child_sa->get_spi(child_sa, FALSE)); + + /* FIXME: this might trigger twice if there was a collision */ + charon->bus->child_rekey(charon->bus, this->child_sa, child_sa); + } + else if (this->child_sa->get_state(this->child_sa) == CHILD_REKEYING) + { /* rekeying failed, reuse old child */ + this->child_sa->set_state(this->child_sa, state); + } return SUCCESS; } +/** + * Remove the passive rekey task that's waiting for IKE_FOLLOWUP_KE requests + * that will never come. + */ +static void remove_passive_rekey_task(private_child_rekey_t *this) +{ + enumerator_t *enumerator; + task_t *task; + + enumerator = this->ike_sa->create_task_enumerator(this->ike_sa, + TASK_QUEUE_PASSIVE); + while (enumerator->enumerate(enumerator, &task)) + { + if (task->get_type(task) == TASK_CHILD_REKEY) + { + this->ike_sa->remove_task(this->ike_sa, enumerator); + task->destroy(task); + break; + } + } + enumerator->destroy(enumerator); +} + /** * Handle a rekey collision */ static child_sa_t *handle_collision(private_child_rekey_t *this, - child_sa_t **to_install) + child_sa_t **to_install, bool multi_ke) { - child_sa_t *to_delete; + private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + chunk_t this_nonce, other_nonce; + child_sa_t *to_delete, *child_sa; - if (this->collision->get_type(this->collision) == TASK_CHILD_REKEY) - { - chunk_t this_nonce, other_nonce; - private_child_rekey_t *other = (private_child_rekey_t*)this->collision; - - this_nonce = this->child_create->get_lower_nonce(this->child_create); - other_nonce = other->child_create->get_lower_nonce(other->child_create); - - /* if we have the lower nonce, delete rekeyed SA. If not, delete - * the redundant. */ - if (memcmp(this_nonce.ptr, other_nonce.ptr, - min(this_nonce.len, other_nonce.len)) > 0) + if (this->collision->get_type(this->collision) == TASK_CHILD_DELETE) + { /* CHILD_DELETE, which we only adopt if it is for the CHILD_SA we are + * ourselves rekeying */ + to_delete = this->child_create->get_child(this->child_create); + if (multi_ke) { - child_sa_t *child_sa; - - *to_install = this->child_create->get_child(this->child_create); - to_delete = this->child_sa; - DBG1(DBG_IKE, "CHILD_SA rekey collision won, deleting old child " - "%s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); - /* don't touch child other created, it has already been deleted */ - if (!this->other_child_destroyed) - { - /* disable close action and updown event for redundant child */ - child_sa = other->child_create->get_child(other->child_create); - if (child_sa) - { - child_sa->set_close_action(child_sa, ACTION_NONE); - if (child_sa->get_state(child_sa) != CHILD_REKEYED) - { - child_sa->set_state(child_sa, CHILD_REKEYED); - } - } - } + DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, abort incomplete " + "multi-KE rekeying"); } else { - to_delete = this->child_create->get_child(this->child_create); - DBG1(DBG_IKE, "CHILD_SA rekey collision lost, deleting redundant " - "child %s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); - } - } - else - { /* CHILD_DELETE */ - child_delete_t *del = (child_delete_t*)this->collision; - - /* we didn't had a chance to compare the nonces, so we delete - * the CHILD_SA the other is not deleting. */ - if (del->get_child(del) != this->child_sa) - { - to_delete = this->child_sa; - DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, deleting old child " - "%s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); - } - else - { - to_delete = this->child_create->get_child(this->child_create); DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, deleting redundant " "child %s{%d}", to_delete->get_name(to_delete), to_delete->get_unique_id(to_delete)); } + return to_delete; } + + this_nonce = this->child_create->get_lower_nonce(this->child_create); + other_nonce = other->child_create->get_lower_nonce(other->child_create); + + /* the SA with the lowest nonce should be deleted (if already complete), + * check if we or the peer created it */ + if (memcmp(this_nonce.ptr, other_nonce.ptr, + min(this_nonce.len, other_nonce.len)) < 0) + { + to_delete = this->child_create->get_child(this->child_create); + if (multi_ke) + { + DBG1(DBG_IKE, "CHILD_SA rekey collision lost, abort incomplete " + "multi-KE rekeying"); + } + else + { + DBG1(DBG_IKE, "CHILD_SA rekey collision lost, deleting " + "redundant child %s{%d}", to_delete->get_name(to_delete), + to_delete->get_unique_id(to_delete)); + } + return to_delete; + } + + *to_install = this->child_create->get_child(this->child_create); + to_delete = this->child_sa; + + /* the passive rekeying is complete only if it was single-KE. otherwise, + * the peer would either have stopped before sending IKE_FOLLOWUP_KE when + * it noticed it lost, or it responded with TEMPORARY_FAILURE to our + * CREATE_CHILD_SA request if it already started sending them. */ + if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + { + /* we don't want to install the peer's redundant outbound SA */ + this->child_sa->set_rekey_spi(this->child_sa, 0); + /* don't touch child other created if it has already been deleted */ + if (!(this->flags & CHILD_REKEY_OTHER_DESTROYED)) + { + /* disable close action and updown event for redundant child the + * other is expected to delete */ + child_sa = other->child_create->get_child(other->child_create); + if (child_sa) + { + child_sa->set_close_action(child_sa, ACTION_NONE); + if (child_sa->get_state(child_sa) != CHILD_REKEYED) + { + child_sa->set_state(child_sa, CHILD_REKEYED); + } + } + } + if (multi_ke) + { + DBG1(DBG_IKE, "CHILD_SA rekey collision won, continue with " + "multi-KE rekeying"); + /* change the state back, we are not done rekeying yet */ + this->child_sa->set_state(this->child_sa, CHILD_REKEYING); + } + else + { + DBG1(DBG_IKE, "CHILD_SA rekey collision won, deleting old child " + "%s{%d}", to_delete->get_name(to_delete), + to_delete->get_unique_id(to_delete)); + } + this->collision->destroy(this->collision); + } + else + { + /* the peer will not continue with its multi-KE rekeying, so we must + * remove the passive task that's waiting for IKE_FOLLOWUP_KEs */ + if (multi_ke) + { + DBG1(DBG_IKE, "CHILD_SA rekey collision won, continue with " + "multi-KE rekeying and remove passive %N task", + task_type_names, TASK_CHILD_REKEY); + } + else + { + DBG1(DBG_IKE, "CHILD_SA rekey collision won, remove passive %N " + "task", task_type_names, TASK_CHILD_REKEY); + } + remove_passive_rekey_task(this); + } + this->collision = NULL; return to_delete; } @@ -404,7 +533,7 @@ METHOD(task_t, process_i, status_t, { protocol_id_t protocol; uint32_t spi; - child_sa_t *to_delete, *to_install = NULL; + child_sa_t *child_sa, *to_delete = NULL, *to_install = NULL; if (message->get_notify(message, NO_ADDITIONAL_SAS)) { @@ -459,25 +588,43 @@ METHOD(task_t, process_i, status_t, if (this->child_create->task.process(&this->child_create->task, message) == NEED_MORE) { - /* bad DH group while rekeying, retry, or failure requiring deletion */ - return NEED_MORE; + if (message->get_notify(message, INVALID_KE_PAYLOAD) || + !this->child_create->get_child(this->child_create)) + { /* bad key exchange mechanism, retry, or failure requiring delete */ + return NEED_MORE; + } + /* multiple key exchanges */ + this->flags |= CHILD_REKEY_FOLLOWUP_KE; + /* there will only be a collision while we process a CREATE_CHILD_SA + * response, later we just respond with TEMPORARY_FAILURE and ignore + * the passive task - if we lost, the returned SA is the one we created + * in this task, since it's not complete yet, we abort the task */ + if (this->collision) + { + to_delete = handle_collision(this, &to_install, TRUE); + } + return (to_delete && to_delete != this->child_sa) ? SUCCESS : NEED_MORE; } - if (message->get_payload(message, PLV2_SECURITY_ASSOCIATION) == NULL) + + child_sa = this->child_create->get_child(this->child_create); + if (!child_sa || child_sa->get_state(child_sa) != CHILD_INSTALLED) { /* establishing new child failed, reuse old and try again. but not when - * we received a delete in the meantime */ + * we received a delete in the meantime or passively rekeyed the SA */ if (!this->collision || - this->collision->get_type(this->collision) != TASK_CHILD_DELETE) + (this->collision->get_type(this->collision) != TASK_CHILD_DELETE && + !(this->flags & CHILD_REKEY_ADOPTED_PASSIVE))) { schedule_delayed_rekey(this); } return SUCCESS; } - /* check for rekey collisions */ + /* there won't be a collision if this task is for a multi-KE rekeying, as a + * collision during CREATE_CHILD_SA was cleaned up above */ if (this->collision) { - to_delete = handle_collision(this, &to_install); + to_delete = handle_collision(this, &to_install, FALSE); } else { @@ -571,18 +718,25 @@ METHOD(child_rekey_t, collide, bool, } /* ignore passive tasks that did not successfully create a CHILD_SA */ other_child = rekey->child_create->get_child(rekey->child_create); - if (!other_child || - other_child->get_state(other_child) != CHILD_INSTALLED) + if (!other_child) { return FALSE; } + if (other_child->get_state(other_child) != CHILD_INSTALLED) + { + DBG1(DBG_IKE, "colliding passive rekeying is not yet complete", + task_type_names, TASK_CHILD_REKEY); + /* we do reference the task to check its state later */ + this->collision = other; + return FALSE; + } } else if (other->get_type(other) == TASK_CHILD_DELETE) { child_delete_t *del = (child_delete_t*)other; if (is_redundant(this, del->get_child(del))) { - this->other_child_destroyed = TRUE; + this->flags |= CHILD_REKEY_OTHER_DESTROYED; return FALSE; } if (del->get_child(del) != this->child_sa) @@ -596,9 +750,15 @@ METHOD(child_rekey_t, collide, bool, /* shouldn't happen */ return FALSE; } + DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, TASK_CHILD_REKEY, task_type_names, other->get_type(other)); - DESTROY_IF(this->collision); + + if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + { + DESTROY_IF(this->collision); + } + this->flags |= CHILD_REKEY_ADOPTED_PASSIVE; this->collision = other; return TRUE; } @@ -615,7 +775,10 @@ METHOD(task_t, migrate, void, { this->child_create->task.migrate(&this->child_create->task, ike_sa); } - DESTROY_IF(this->collision); + if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + { + DESTROY_IF(this->collision); + } this->ike_sa = ike_sa; this->collision = NULL; @@ -632,7 +795,10 @@ METHOD(task_t, destroy, void, { this->child_delete->task.destroy(&this->child_delete->task); } - DESTROY_IF(this->collision); + if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + { + DESTROY_IF(this->collision); + } chunk_free(&this->spi_data); free(this); } From e05d86b27afc18693153364e56b5a3627682a814 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 4 Nov 2019 17:27:20 +0100 Subject: [PATCH 32/46] key-exchange: Add dynamic parser for additional key exchange methods --- src/libstrongswan/crypto/key_exchange.c | 77 ++++++++++++++++++++++++- src/libstrongswan/crypto/key_exchange.h | 9 ++- src/libstrongswan/library.c | 4 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/libstrongswan/crypto/key_exchange.c b/src/libstrongswan/crypto/key_exchange.c index 5b0113989..b3fb9641f 100644 --- a/src/libstrongswan/crypto/key_exchange.c +++ b/src/libstrongswan/crypto/key_exchange.c @@ -18,6 +18,9 @@ #include "key_exchange.h" +#include +#include + ENUM_BEGIN(key_exchange_method_names, KE_NONE, MODP_1024_BIT, "KE_NONE", "MODP_768", @@ -475,10 +478,68 @@ static struct { }, }; +/** + * Proposal tokens for additional key exchanges. + */ +static hashtable_t *tokens; + +/** + * Mutex to safely access cached tokens. + */ +static mutex_t *mutex; + +/** + * Destroy an allocated proposal token. + */ +static void token_destroy(proposal_token_t *this) +{ + free(this->name); + free(this); +} + +/** + * Parse ke<1-7>_ for additional key exchange methods. + */ +static proposal_token_t *additional_key_exchange_parser(const char *algname) +{ + proposal_token_t *token; + const proposal_token_t *base; + u_int num; + char prefix[3], alg[256]; + + if (!algname || sscanf(algname, "%2s%1u_%255s", &prefix, &num, alg) != 3 || + !strcaseeq(prefix, "ke")) + { + return NULL; + } + mutex->lock(mutex); + token = tokens->get(tokens, algname); + if (token || num < 1 || num > 7) + { + goto done; + } + base = lib->proposal->get_token(lib->proposal, alg); + if (!base || base->type != KEY_EXCHANGE_METHOD) + { + goto done; + } + INIT(token, + .name = strdup(algname), + .type = ADDITIONAL_KEY_EXCHANGE_1 + num - 1, + .algorithm = base->algorithm, + .keysize = base->keysize, + ); + tokens->put(tokens, token->name, token); + +done: + mutex->unlock(mutex); + return token; +} + /* * Described in header */ -void diffie_hellman_init() +void key_exchange_init() { int i; @@ -498,6 +559,20 @@ void diffie_hellman_init() dh_params[i].public.exp_len = dh_params[i].public.prime.len; } } + + mutex = mutex_create(MUTEX_TYPE_RECURSIVE); + tokens = hashtable_create(hashtable_hash_str, hashtable_equals_str, 4); + lib->proposal->register_algname_parser(lib->proposal, + additional_key_exchange_parser); +} + +/* + * Described in header + */ +void key_exchange_deinit() +{ + tokens->destroy_function(tokens, (void*)token_destroy); + mutex->destroy(mutex); } /* diff --git a/src/libstrongswan/crypto/key_exchange.h b/src/libstrongswan/crypto/key_exchange.h index 73bf61f06..4aa4e264b 100644 --- a/src/libstrongswan/crypto/key_exchange.h +++ b/src/libstrongswan/crypto/key_exchange.h @@ -178,9 +178,14 @@ struct diffie_hellman_params_t { }; /** - * Initialize diffie hellman parameters during startup. + * Initialize DH parameters and KE token parser during startup. */ -void diffie_hellman_init(); +void key_exchange_init(); + +/** + * Deinitialize KE token parser during shutdown. + */ +void key_exchange_deinit(); /** * Get the parameters associated with the specified Diffie-Hellman group. diff --git a/src/libstrongswan/library.c b/src/libstrongswan/library.c index c65b38154..1521fb261 100644 --- a/src/libstrongswan/library.c +++ b/src/libstrongswan/library.c @@ -161,6 +161,8 @@ void library_deinit() /* make sure the cache is clear before unloading plugins */ lib->credmgr->flush_cache(lib->credmgr, CERT_ANY); + key_exchange_deinit(); + this->public.streams->destroy(this->public.streams); this->public.watcher->destroy(this->public.watcher); this->public.scheduler->destroy(this->public.scheduler); @@ -436,7 +438,7 @@ bool library_init(char *settings, const char *namespace) #endif /* INTEGRITY_TEST */ } - diffie_hellman_init(); + key_exchange_init(); return !this->init_failed; } From c5a6938b9e7cecb0ed250a607ca64d2413663523 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 22 Oct 2020 14:15:31 +0200 Subject: [PATCH 33/46] proposal: Add prefix for additional key exchanges when logging proposals --- src/libstrongswan/crypto/proposal/proposal.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index ba6b3f0f2..ff7884d9a 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -849,7 +849,7 @@ static int print_alg(private_proposal_t *this, printf_hook_data_t *data, enumerator = array_create_enumerator(this->transforms); while (enumerator->enumerate(enumerator, &entry)) { - char *prefix = "/"; + char *prefix = "/", ake_prefix[5] = ""; if (type != entry->type) { @@ -860,14 +860,19 @@ static int print_alg(private_proposal_t *this, printf_hook_data_t *data, prefix = ""; *first = FALSE; } + if (is_ke_transform(type) && type != KEY_EXCHANGE_METHOD) + { + sprintf(ake_prefix, "KE%d_", type - ADDITIONAL_KEY_EXCHANGE_1 + 1); + } if (names) { - written += print_in_hook(data, "%s%N", prefix, names, entry->alg); + written += print_in_hook(data, "%s%s%N", prefix, ake_prefix, + names, entry->alg); } else { - written += print_in_hook(data, "%sUNKNOWN_%u_%u", prefix, - entry->type, entry->alg); + written += print_in_hook(data, "%s%sUNKNOWN_%u_%u", prefix, + ake_prefix, entry->type, entry->alg); } if (entry->key_size) { From 355f917532e7c581b9dee28a1b88aebef002832f Mon Sep 17 00:00:00 2001 From: Andreas Steffen Date: Tue, 19 Nov 2019 20:44:39 +0100 Subject: [PATCH 34/46] vici: List additional key exchanges Co-authored-by: Tobias Brunner --- src/libcharon/plugins/vici/vici_query.c | 27 +++++++++++++++++++++++-- src/swanctl/commands/list_sas.c | 23 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/libcharon/plugins/vici/vici_query.c b/src/libcharon/plugins/vici/vici_query.c index bacb7b101..8ba614fb6 100644 --- a/src/libcharon/plugins/vici/vici_query.c +++ b/src/libcharon/plugins/vici/vici_query.c @@ -1,6 +1,6 @@ /* * Copyright (C) 2015-2020 Tobias Brunner - * Copyright (C) 2015-2018 Andreas Steffen + * Copyright (C) 2015-2019 Andreas Steffen * Copyright (C) 2014 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -172,6 +172,27 @@ static void list_label(vici_builder_t *b, child_sa_t *child, child_cfg_t *cfg) } } +/** + * List additional key exchanges + */ +static void list_ake(vici_builder_t *b, proposal_t *proposal) +{ + transform_type_t transform; + char ake_str[5]; + uint16_t alg; + int ake; + + for (ake = 1; ake <= 7; ake++) + { + transform = ADDITIONAL_KEY_EXCHANGE_1 + ake - 1; + if (proposal->get_algorithm(proposal, transform, &alg, NULL)) + { + sprintf(ake_str, "ake%d", ake); + b->add_kv(b, ake_str, "%N", key_exchange_method_names, alg); + } + } +} + /** * List IPsec-related details about a CHILD_SA */ @@ -235,6 +256,7 @@ static void list_child_ipsec(vici_builder_t *b, child_sa_t *child) { b->add_kv(b, "dh-group", "%N", key_exchange_method_names, alg); } + list_ake(b, proposal); if (proposal->get_algorithm(proposal, EXTENDED_SEQUENCE_NUMBERS, &alg, NULL) && alg == EXT_SEQ_NUMBERS) { @@ -493,6 +515,7 @@ static void list_ike(private_vici_query_t *this, vici_builder_t *b, { b->add_kv(b, "dh-group", "%N", key_exchange_method_names, alg); } + list_ake(b, proposal); } add_condition(b, ike_sa, "ppk", COND_PPK); @@ -1382,7 +1405,7 @@ CALLBACK(get_algorithms, vici_message_t*, enumerator->destroy(enumerator); b->end_section(b); - b->begin_section(b, "dh"); + b->begin_section(b, "ke"); enumerator = lib->crypto->create_ke_enumerator(lib->crypto); while (enumerator->enumerate(enumerator, &group, &plugin_name)) { diff --git a/src/swanctl/commands/list_sas.c b/src/swanctl/commands/list_sas.c index a4d794c3f..321bd044d 100755 --- a/src/swanctl/commands/list_sas.c +++ b/src/swanctl/commands/list_sas.c @@ -1,6 +1,7 @@ /* + * Copyright (C) 2016-2019 Andreas Steffen + * Copyright (C) 2015-2020 Tobias Brunner * Copyright (C) 2014 Martin Willi - * Copyright (C) 2016 Andreas Steffen * * Copyright (C) secunet Security Networks AG * @@ -100,6 +101,24 @@ CALLBACK(sa_list, int, return 0; } +/** + * Print additional key exchanges + */ +static void print_ake(hashtable_t *sa) +{ + char ake_str[5]; + int ake; + + for (ake = 1; ake <= 7; ake++) + { + sprintf(ake_str, "ake%d", ake); + if (sa->get(sa, ake_str)) + { + printf("/KE%d_%s", ake, sa->get(sa, ake_str)); + } + } +} + CALLBACK(child_sas, int, hashtable_t *ike, vici_res_t *res, char *name) { @@ -145,6 +164,7 @@ CALLBACK(child_sas, int, { printf("/%s", child->get(child, "dh-group")); } + print_ake(child); if (child->get(child, "esn")) { printf("/ESN"); @@ -290,6 +310,7 @@ CALLBACK(ike_sa, int, } printf("/%s", ike->get(ike, "prf-alg")); printf("/%s", ike->get(ike, "dh-group")); + print_ake(ike); if (streq(ike->get(ike, "ppk"), "yes")) { printf("/PPK"); From d4fb07911fc965e3be87f6dec5b75c15fb90ac62 Mon Sep 17 00:00:00 2001 From: Andreas Steffen Date: Tue, 6 Jul 2021 12:11:51 +0200 Subject: [PATCH 35/46] vici: Increase maximum proposal length --- src/libcharon/plugins/vici/vici_config.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libcharon/plugins/vici/vici_config.c b/src/libcharon/plugins/vici/vici_config.c index c46b8872b..2d548ab3a 100644 --- a/src/libcharon/plugins/vici/vici_config.c +++ b/src/libcharon/plugins/vici/vici_config.c @@ -54,6 +54,11 @@ #include +/** + * Maximum proposal length + */ +#define MAX_PROPOSAL_LEN 2048 + /** * Magic value for an undefined lifetime */ @@ -599,7 +604,7 @@ static void free_child_data(child_data_t *data) */ static bool parse_proposal(linked_list_t *list, protocol_id_t proto, chunk_t v) { - char buf[BUF_LEN]; + char buf[MAX_PROPOSAL_LEN]; proposal_t *proposal; if (!vici_stringify(v, buf, sizeof(buf))) From 37c56affa14b21aac207128fa41d2f01c7dab79b Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 17 Jun 2020 18:39:44 +0200 Subject: [PATCH 36/46] unit-tests: Support multiple proposals in exchange tests --- .../tests/utils/exchange_test_helper.c | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 8c57c8877..23f1132c0 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -98,21 +98,27 @@ static ike_cfg_t *create_ike_cfg(bool initiator, exchange_test_sa_conf_t *conf) .remote = "127.0.0.1", .remote_port = IKEV2_UDP_PORT, }; + enumerator_t *enumerator; ike_cfg_t *ike_cfg; - char *proposal = NULL; + char *proposals = NULL, *proposal; if (conf) { ike.childless = initiator ? conf->initiator.childless : conf->responder.childless; - proposal = initiator ? conf->initiator.ike : conf->responder.ike; + proposals = initiator ? conf->initiator.ike : conf->responder.ike; } ike_cfg = ike_cfg_create(&ike); - if (proposal) + if (proposals) { - ike_cfg->add_proposal(ike_cfg, + enumerator = enumerator_create_token(proposals, ",", ""); + while (enumerator->enumerate(enumerator, &proposal)) + { + ike_cfg->add_proposal(ike_cfg, proposal_create_from_string(PROTO_IKE, proposal)); + } + enumerator->destroy(enumerator); } else { @@ -124,21 +130,27 @@ static ike_cfg_t *create_ike_cfg(bool initiator, exchange_test_sa_conf_t *conf) static child_cfg_t *create_child_cfg(bool initiator, exchange_test_sa_conf_t *conf) { + enumerator_t *enumerator; child_cfg_t *child_cfg; child_cfg_create_t child = { .mode = MODE_TUNNEL, }; - char *proposal = NULL; + char *proposals = NULL, *proposal; child_cfg = child_cfg_create(initiator ? "init" : "resp", &child); if (conf) { - proposal = initiator ? conf->initiator.esp : conf->responder.esp; + proposals = initiator ? conf->initiator.esp : conf->responder.esp; } - if (proposal) + if (proposals) { - child_cfg->add_proposal(child_cfg, + enumerator = enumerator_create_token(proposals, ",", ""); + while (enumerator->enumerate(enumerator, &proposal)) + { + child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP, proposal)); + } + enumerator->destroy(enumerator); } else { From 329a7b331de7532964fec0f3d1528ea07977a3d1 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 4 Nov 2019 16:17:30 +0100 Subject: [PATCH 37/46] unit-tests: Add tests for IKE_SA rekeying with multiple key exchanges --- src/libcharon/tests/suites/test_ike_rekey.c | 1073 ++++++++++++++++- .../tests/utils/exchange_test_helper.c | 12 + 2 files changed, 1081 insertions(+), 4 deletions(-) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index efc27542a..98c588b1e 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Tobias Brunner + * Copyright (C) 2016-2020 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -103,6 +103,110 @@ START_TEST(test_regular) } END_TEST +/** + * Config for multiple KE exchange tests + */ +static exchange_test_sa_conf_t multi_ke_conf = { + .initiator = { + .ike = "aes256-sha256-modp3072-ke1_ecp256", + }, + .responder = { + .ike = "aes256-sha256-modp3072-ke1_ecp256", + }, +}; + +/** + * Regular IKE_SA rekeying with multiple key exchanges either initiated by the + * original initiator or responder of the IKE_SA. + */ +START_TEST(test_regular_multi_ke) +{ + ike_sa_t *a, *b, *new_sa; + status_t s; + + 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 { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_rekey); + assert_no_notify(IN, REKEY_SA); + 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 { SA, Nr, KEr, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, ADDITIONAL_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_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_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(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_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(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + /** * IKE_SA rekeying where the responder does not agree with the DH group selected * by the initiator, either initiated by the original initiator or responder of @@ -204,6 +308,126 @@ START_TEST(test_regular_ke_invalid) } END_TEST +/** + * IKE_SA rekeying with multiple key exchanges where the responder does not + * agree with the first DH group selected by the initiator, either initiated by + * the original initiator or responder of the IKE_SA. + */ +START_TEST(test_regular_ke_invalid_multi_ke) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .ike = "aes128-sha256-modp2048-modp3072-ke1_ecp256", + }, + .responder = { + .ike = "aes128-sha256-modp3072-modp2048-ke1_ecp256", + }, + }; + ike_sa_t *a, *b, *sa; + status_t s; + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &conf); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &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); + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + TRUE, lib->ns); + lib->settings->set_bool(lib->settings, "%s.prefer_previous_dh_group", + FALSE, lib->ns); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_ESTABLISHED); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + + /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ + assert_single_notify(IN, INVALID_KE_PAYLOAD); + 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(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_rekey); + 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 { SA, Nr, KEr, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, ADDITIONAL_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_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_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); + sa = assert_ike_sa_checkout(3, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_DELETING); + assert_child_sa_count(a, 0); + sa = assert_ike_sa_checkout(3, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(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(); + + 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. @@ -277,11 +501,11 @@ START_TEST(test_collision) /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - /* as original initiator a is initiator of both SAs it could delete */ + /* as original initiator, a is initiator of both SAs it could delete */ sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); assert_ike_sa_state(sa, IKE_DELETING); assert_child_sa_count(sa, 0); - /* if b won it will delete the original SA a initiated */ + /* if b won, it will delete the original SA a initiated */ sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, data[_i].del_b_i == 1); assert_ike_sa_state(sa, IKE_REKEYED); @@ -296,7 +520,7 @@ START_TEST(test_collision) /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - /* if b wins it deletes the SA originally initiated by a */ + /* if b wins, it deletes the SA originally initiated by a */ sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, data[_i].del_b_i != 1); assert_ike_sa_state(sa, IKE_DELETING); @@ -359,6 +583,381 @@ START_TEST(test_collision) } END_TEST +/** + * Both peers initiate the IKE_SA rekeying with multiple key exchanges + * concurrently and should handle the collision properly depending on the + * nonces. + */ +START_TEST(test_collision_multi_ke) +{ + ike_sa_t *a, *b, *sa; + status_t status; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* When rekeyings collide we get two IKE_SAs with a total of four nonces. + * The IKE_SA with the lowest nonce SHOULD be deleted by the peer that + * created that IKE_SA. However, with multiple key exchanges, no IKE_SA + * has yet been established, so the losing peer just doesn't continue with + * IKE_FOLLOWUP_KE exchanges (i.e. that SA is not explicitly deleted later). + * The replaced IKE_SA is deleted by the peer that initiated the + * surviving SA. Four nonces and SPIs are needed (SPI 1 and 2 are used for + * the initial IKE_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the "deleted" IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + 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 { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + 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(); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* if a won, it must remove the passive task, otherwise the active task, + * no new SA is yet created */ + assert_num_tasks(a, data[_i].del_a_i == 1 ? 0 : 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, data[_i].del_a_i == 1 ? 1 : 0, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(a, IKE_REKEYING); + assert_ike_sa_count(2); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } --> */ + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, data[_i].del_b_i == 1 ? 0 : 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, data[_i].del_b_i == 1 ? 1 : 0, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(b, IKE_REKEYING); + assert_ike_sa_count(2); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> (direction depends on who won) */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + /* if a won, this message is sent to b */ + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i != 1); + exchange_test_helper->process_message(exchange_test_helper, sa, NULL); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(3); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i == 1); + exchange_test_helper->process_message(exchange_test_helper, sa, NULL); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + 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); + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i != 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(3); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i == 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(2); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * Both peers initiate an IKE_SA rekeying concurrently, but only one of them + * proposes multiple key exchanges, they should still handle the collision + * properly. + */ +START_TEST(test_collision_mixed) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .ike = "aes256-sha256-modp3072-ke1_ecp256,aes256-sha256-modp3072", + }, + .responder = { + .ike = "aes256-sha256-modp3072,aes256-sha256-modp3072-ke1_ecp256", + }, + }; + ike_sa_t *a, *b, *sa; + status_t status; + + /* let's accept what the peer proposes first */ + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + /* When rekeyings collide, we get two IKE_SAs with a total of four nonces. + * The IKE_SA with the lowest nonce SHOULD be deleted by the peer that + * created that IKE_SA. In this mixed scenario, there might already be + * a complete IKE_SA depending on who initiates the rekeying. This has then + * to be deleted properly (like with regular collisions). + * The replaced IKE_SA is deleted by the peer that initiated the + * surviving SA. Four nonces and SPIs are needed (SPI 1 and 2 are used for + * the initial IKE_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the "deleted" IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + 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 { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + 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(); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } */ + if (data[_i].del_a_i == 1) + { /* if A won, it will continue with the multi-KE rekeying, while B has to + * delete the redundant SA it created */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* B's rekeying is single KE, so we never expect a passive task */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(a, IKE_REKEYING); + /* B is expected to delete this redundant SA */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + assert_ike_sa_count(3); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 0, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(b, IKE_REKEYING); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, TRUE); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + assert_ike_sa_count(4); + /* ike_rekey */ + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_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); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(5); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(ike_rekey); + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, FALSE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(4); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_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); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(5); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, TRUE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(4); + assert_hook(); + } + else + { /* if lost, the active rekeying is aborted and the passive completed, + * there is nothing to delete */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(a, IKE_REKEYED); + assert_child_sa_count(a, 0); + /* the passively created SA is completed */ + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(3); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + 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); + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i != 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(3); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(1, 2, data[_i].del_a_i == 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(2); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + /** * Both peers initiate the IKE_SA rekeying concurrently but the proposed DH * groups are not the same. After handling the INVALID_KE_PAYLOAD they should @@ -951,6 +1550,207 @@ START_TEST(test_collision_delayed_response) } END_TEST +/** + * This is like a regular rekey collision, but one CREATE_CHILD_SA response + * is delayed: + * Peer A Peer B + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * -----------/----> + * handle KE <---------/------ send additional KE (if won) + * ---------/------> + * handle rekey <-------/ + * additional KE ----------------> handle KE (if lost) + * <---------------- + * ... the winner deletes the old SA + * + * If A wins the collision, this is just a regular collision as B will simply + * wait until A receives the response and continues with its IKE_FOLLOWUP_KE + * request. So we only look at the cases in which B wins. + * + * Besides the scenario depicted above, i.e. where the response arrives after + * handling B's IKE_FOLLOwUP_KE request, we also test when it arrives after + * handling the delete (A assumes that B didn't notice the collision, so it just + * completes the passive rekeying and the response will eventually be dropped + * because the SA is gone). + */ +START_TEST(test_collision_delayed_response_multi_ke) +{ + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + bool after_delete = _i >= 2; + + _i %= 2; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * IKE_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, B wins each time (with either of its nonces), + * but the response arrives at different times. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 5, 1, 2, 4, 6 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + 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 { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + 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(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } --> */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(b, IKE_REKEYING); + assert_ike_sa_count(2); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + /* a waits until it receives the CREATE_CHILD_SA response */ + 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(2); + assert_hook(); + + if (!after_delete) + { /* a receives the response right after the IKE_FOLLOWUP_KE, the passive + * rekeying is completed and the active aborted */ + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } (delayed) */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_ike_sa_state(a, IKE_REKEYED); + assert_child_sa_count(a, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(3); + assert_hook(); + } + + /* IKE_FOLLOWUP_KE { KEr } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(after_delete ? 3 : 4); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + if (after_delete) + { + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, a); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(3); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } (delayed) */ + /* the IKE_SA (a) does not exist anymore */ + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + assert_hook(); + } + else + { + assert_hook_not_called(ike_rekey); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, a); + + /* INFORMATIONAL { } --> */ + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + /* ike_rekey */ + assert_hook(); + } + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + /** * In this scenario one of the peers does not notice that there is a rekey * collision because the other request is dropped: @@ -1288,6 +2088,179 @@ START_TEST(test_collision_delayed_request_and_delete) } END_TEST +/** + * In this scenario one of the peers does not notice that there is a rekey + * collision because the other request is delayed: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * \ /---- send additional KE + * \-/----> detect collision + * handle KE <---------/ /---- TEMP_FAIL + * -----------/----> + * <---------/------ delete old SA + * delete ---------/------> + * rekey done / + * sa already gone <------/ + * + * In a variation of this scenario, the TEMP_FAIL notify arrives before + * the delete does. + */ +START_TEST(test_collision_delayed_request_multi_ke) +{ + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + bool after_delete = _i >= 3; + + _i %= 3; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + /* SPIs of the deleted IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00 }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + 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(); + + /* CREATE_CHILD_SA { SA, Ni, KEr, N(ADD_KE) } --> */ + assert_hook_not_called(ike_rekey); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> (delayed) */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_ike_sa_state(b, IKE_REKEYING); + + /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ + assert_hook_not_called(ike_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + /* a waits until it receives the CREATE_CHILD_SA response or a delete */ + 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(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + if (!after_delete) + { + assert_hook_rekey(ike_rekey, 1, 4); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYED); + assert_child_sa_count(a, 0); + sa = assert_ike_sa_checkout(4, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_scheduler(); + assert_hook(); + } + else + { + /* the SA will already be gone later */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + } + + /* IKE_FOLLOWUP_KE { KEr } --> */ + assert_hook_rekey(ike_rekey, 1, 4); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(4, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(after_delete ? 1 : 2); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + if (after_delete) + { + assert_hook_rekey(ike_rekey, 1, 4); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + sa = assert_ike_sa_checkout(4, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_hook(); + } + else + { + assert_hook_not_called(ike_rekey); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + } + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + /** * One of the hosts initiates a DELETE of the IKE_SA the other peer is * concurrently trying to rekey. @@ -1370,6 +2343,91 @@ START_TEST(test_collision_delete) } END_TEST +/** + * One of the hosts initiates a DELETE of the IKE_SA the other peer is + * concurrently attempting to rekey with multiple key exchanges. + * + * rekey ----------------> + * <---------------- + * additional ke ----\ /---- delete + * \-----/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * delete -----------/----> + * sa already gone <---------/ + */ +START_TEST(test_collision_delete_multi_ke) +{ + ike_sa_t *a, *b; + message_t *msg; + status_t s; + + 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); + } + /* this should never get called as this does not result in a successful + * rekeying on either side */ + assert_hook_not_called(ike_rekey); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + 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); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } */ + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_ike_sa_count(0); + + call_ikesa(b, delete, FALSE); + assert_ike_sa_state(b, IKE_DELETING); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_ike_sa_count(0); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { N(TEMP_FAIL) } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* ike_rekey */ + assert_hook(); +} +END_TEST + /** * One of the hosts initiates a DELETE of the IKE_SA the other peer is * concurrently trying to rekey. However, the delete request is delayed or @@ -1465,21 +2523,28 @@ Suite *ike_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_invalid, 0, 2); + tcase_add_loop_test(tc, test_regular_ke_invalid_multi_ke, 0, 2); suite_add_tcase(s, tc); tc = tcase_create("collisions 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); tcase_add_loop_test(tc, test_collision_ke_invalid, 0, 4); tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4); tcase_add_loop_test(tc, test_collision_dropped_request, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_request_and_delete, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_multi_ke, 0, 6); suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); 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); suite_add_tcase(s, tc); diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 23f1132c0..42530be44 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -277,6 +277,7 @@ METHOD(exchange_test_helper_t, establish_sa, void, ike_sa_id_t *id_i, *id_r; ike_sa_t *sa_i, *sa_r; child_cfg_t *child_i; + proposal_t *proposal; child_i = create_sa(this, init, resp, conf); @@ -294,6 +295,17 @@ METHOD(exchange_test_helper_t, establish_sa, void, /* <-- IKE_SA_INIT */ id_i->set_responder_spi(id_i, id_r->get_responder_spi(id_r)); process_message(this, sa_i, NULL); + + proposal = sa_i->get_proposal(sa_i); + if (proposal->get_algorithm(proposal, ADDITIONAL_KEY_EXCHANGE_1, NULL, + NULL)) + { + /* IKE_INTERMEDIATE --> */ + process_message(this, sa_r, NULL); + /* <-- IKE_INTERMEDIATE */ + process_message(this, sa_i, NULL); + } + /* IKE_AUTH --> */ process_message(this, sa_r, NULL); /* <-- IKE_AUTH */ From 37eeafa37f3fb8c5ddae624b412c2171bff36626 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Jun 2020 14:12:47 +0200 Subject: [PATCH 38/46] unit-tests: Add tests for CHILD_SA creation with multiple key exchanges --- .../tests/suites/test_child_create.c | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/suites/test_child_create.c b/src/libcharon/tests/suites/test_child_create.c index 085b440b6..bc855f76d 100644 --- a/src/libcharon/tests/suites/test_child_create.c +++ b/src/libcharon/tests/suites/test_child_create.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Tobias Brunner + * Copyright (C) 2016-2020 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -135,6 +135,73 @@ START_TEST(test_collision_ike_rekey) } END_TEST +/** + * One of the peers creates a new CHILD_SA using multiple key exchanges. + */ +START_TEST(test_multi_ke) +{ + peer_cfg_t *peer_cfg; + child_cfg_t *child_cfg; + child_cfg_create_t child = { + .mode = MODE_TUNNEL, + }; + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + assert_hook_not_called(child_updown); + child_cfg = child_cfg_create("child", &child); + child_cfg->add_proposal(child_cfg, + proposal_create_from_string(PROTO_ESP, + "aes256-sha256-modp3072-ke1_ecp256")); + /* as configs are selected based on TS only, use a different protocol */ + child_cfg->add_traffic_selector(child_cfg, TRUE, + traffic_selector_create_dynamic(6, 0, 65535)); + child_cfg->add_traffic_selector(child_cfg, FALSE, + traffic_selector_create_dynamic(6, 0, 65535)); + call_ikesa(a, initiate, child_cfg, NULL); + assert_child_sa_count(a, 1); + peer_cfg = b->get_peer_cfg(b); + peer_cfg->add_child_cfg(peer_cfg, child_cfg->get_ref(child_cfg)); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi, TSi, TSr } --> */ + assert_hook_not_called(child_updown); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 1); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 1); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi N(ADD_KE) } --> */ + assert_hook_updown(child_updown, TRUE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 2); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_updown(child_updown, TRUE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 2); + assert_hook(); + + /* make sure no message was sent after creating the CHILD_SA */ + ck_assert(!exchange_test_helper->sender->dequeue(exchange_test_helper->sender)); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + Suite *child_create_suite_create() { Suite *s; @@ -150,5 +217,9 @@ Suite *child_create_suite_create() tcase_add_test(tc, test_collision_ike_rekey); suite_add_tcase(s, tc); + tc = tcase_create("multiple key exchanges"); + tcase_add_test(tc, test_multi_ke); + suite_add_tcase(s, tc); + return s; } From 33e421320a5db9eb5749cfef8e9ef434305a6faa Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 29 Jun 2020 14:13:53 +0200 Subject: [PATCH 39/46] unit-tests: Add tests for CHILD_SA rekeying with multiple key exchanges --- src/libcharon/tests/suites/test_child_rekey.c | 1184 +++++++++++++++++ 1 file changed, 1184 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 1092d609e..8305fa200 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -132,6 +132,118 @@ START_TEST(test_regular) } END_TEST +/** + * Config for multiple KE exchange tests + */ +static exchange_test_sa_conf_t multi_ke_conf = { + .initiator = { + .esp = "aes256-sha256-modp3072-ke1_ecp256", + }, + .responder = { + .esp = "aes256-sha256-modp3072-ke1_ecp256", + }, +}; + +/** + * Regular CHILD_SA rekey with multiple key exchanges either initiated by the + * original initiator or responder of the IKE_SA. + */ +START_TEST(test_regular_multi_ke) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1, spi_b = 2-_i; + + if (_i) + { /* responder rekeys the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &multi_ke_conf); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_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); + + /* 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_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, spi_a, spi_b); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ + assert_hook_not_called(child_rekey); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + 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, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, spi_a, spi_b, 4); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + 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, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, spi_a, 3, 4); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + 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, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, spi_b, 3, 4); + assert_scheduler(); + assert_hook(); + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + 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, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, spi_a, 3, 4); + assert_scheduler(); + 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, 3, 4); + destroy_rekeyed(b, spi_b); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(a, 3, 4); + + /* child_updown */ + assert_hook(); + + 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 @@ -292,6 +404,203 @@ START_TEST(test_regular_ke_invalid) } END_TEST +/** + * CHILD_SA rekey with multiple key exchanges where the responder does not agree + * with the first key exchange method selected by the initiator, either + * initiated by the original initiator or responder of the IKE_SA. + */ +START_TEST(test_regular_ke_invalid_multi_ke) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .esp = "aes128-sha256-modp2048-modp3072-ke1_ecp256", + }, + .responder = { + .esp = "aes128-sha256-modp3072-modp2048-ke1_ecp256", + }, + }; + ike_sa_t *a, *b; + uint32_t spi_a = _i+1, spi_b = 2-_i; + + if (_i) + { /* responder rekeys the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &conf); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + 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); + + /* 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(); + + /* 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_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, spi_a, spi_b); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_not_called(child_rekey); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + 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, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, spi_a, spi_b, 5); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + 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, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, spi_a, 4, 5); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + 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, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, spi_b, 4, 5); + 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, 4, CHILD_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, spi_a, 4, 5); + 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, 4, 5); + destroy_rekeyed(b, spi_b); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 4, 5); + + /* child_updown */ + assert_hook(); + + /* because the DH group should get reused another rekeying should complete + * without additional exchange */ + initiate_rekey(a, 4); + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* 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, 5, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 4, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_not_called(child_rekey); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 4, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 4, 5); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 5, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 4, 5, 7); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_called(child_rekey); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 4, 6, 7); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 5, 6, 7); + 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, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 4, 6, 7); + assert_hook(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 4); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 6, 7); + destroy_rekeyed(b, 5); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 6, 7); + + /* child_updown */ + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * Check that the responder ignores soft expires while waiting for the delete * after a rekeying. @@ -654,6 +963,443 @@ START_TEST(test_collision) } END_TEST +/** + * Both peers initiate a multi-KE CHILD_SA rekeying concurrently and should + * handle the collision properly depending on the nonces. + */ +START_TEST(test_collision_multi_ke) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* When rekeyings collide we get two CHILD_SAs with a total of four nonces. + * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that + * created that CHILD_SA. However, with multiple key exchanges, no CHILD_SA + * has yet been established, so the losing peer just doesn't continue with + * IKE_FOLLOWUP_KE exchanges (i.e. that SA is not explicitly deleted later). + * The replaced CHILD_SA is deleted by the peer that initiated the + * surviving SA. Four nonces and SPIs are needed (SPI 1 and 2 are used for + * the initial CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 4, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 4, 3, 5 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* if a won, it must remove the passive task, otherwise the active task, + * no new SA is yet created */ + assert_num_tasks(a, data[_i].spi_del_a == 1 ? 0 : 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, data[_i].spi_del_a == 1 ? 1 : 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } --> */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, data[_i].spi_del_b == 2 ? 0 : 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, data[_i].spi_del_b == 2 ? 1 : 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + if (data[_i].spi_del_a == 1) + { + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_rekey(child_rekey, 2, 5); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 5); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + assert_hook_rekey(child_rekey, 1, data[_i].spi_a); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 3, 5); + assert_hook(); + } + else + { + /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ + assert_hook_rekey(child_rekey, 1, 6); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEr } --> */ + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_hook(); + } + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + if (data[_i].spi_del_a == 1) + { + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 3, 5); + assert_scheduler(); + + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 3, 5); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a); + destroy_rekeyed(b, data[_i].spi_del_a); + } + else + { + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_b); + destroy_rekeyed(b, data[_i].spi_del_b); + } + + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Both peers initiate a CHILD_SA rekeying concurrently, but only one of them + * proposes multiple key exchanges, they should still handle the collision + * properly. + */ +START_TEST(test_collision_mixed) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .esp = "aes256-sha256-modp3072-ke1_ecp256,aes256-sha256-modp3072", + }, + .responder = { + .esp = "aes256-sha256-modp3072,aes256-sha256-modp3072-ke1_ecp256", + }, + }; + ike_sa_t *a, *b; + + /* let's accept what the peer proposes first */ + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + /* When rekeyings collide we get two CHILD_SAs with a total of four nonces. + * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that + * created that CHILD_SA. However, with multiple key exchanges, no CHILD_SA + * has yet been established, so the losing peer just doesn't continue with + * IKE_FOLLOWUP_KE exchanges (i.e. that SA is not explicitly deleted later). + * The replaced CHILD_SA is deleted by the peer that initiated the + * surviving SA. Four nonces and SPIs are needed (SPI 1 and 2 are used for + * the initial CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 4, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 4, 3, 5 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* the single-KE passive task was completed and adopted above */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + if (data[_i].spi_del_a == 1) + { + assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_REKEYED, CHILD_OUTBOUND_REGISTERED); + } + else + { + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + } + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + if (data[_i].spi_del_a == 1) + { + /* CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } --> */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 1, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 1, 2, 4); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_rekey(child_rekey, 2, 5); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + + /* <-- IKE_FOLLOWUP_KE { KEr } */ + /* currently we call this again if we keep our own replacement as we + * already called it above */ + assert_hook_rekey(child_rekey, 1, data[_i].spi_a); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_ipsec_sas_installed(a, 1, 3, 5, 6); + assert_hook(); + } + else + { + /* CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } --> */ + assert_hook_rekey(child_rekey, 2, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_hook(); + } + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + if (data[_i].spi_del_a == 1) + { + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 2, 4, 3, 5); + assert_scheduler(); + + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 3); + assert_ipsec_sas_installed(a, 1, 3, 5, 6); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a); + destroy_rekeyed(a, data[_i].spi_del_b); + destroy_rekeyed(b, data[_i].spi_del_a); + destroy_rekeyed(b, data[_i].spi_del_b); + } + else + { + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_b); + destroy_rekeyed(b, data[_i].spi_del_b); + } + + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * This is like the rekey collision above, but one peer deletes the * redundant/old SA before the other peer receives the CREATE_CHILD_SA @@ -876,6 +1622,177 @@ START_TEST(test_collision_delayed_response) } END_TEST +/** + * This is like a regular rekey collision, but one CREATE_CHILD_SA response + * is delayed: + * Peer A Peer B + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * -----------/----> + * handle KE <---------/------ send additional KE (if won) + * ---------/------> + * handle rekey <-------/ + * additional KE ----------------> handle KE (if lost) + * <---------------- + * ... the winner deletes the old SA + * + * If A wins the collision, this is just a regular collision as B will simply + * abort its own rekeying and wait until A receives the response and continues + * with its IKE_FOLLOWUP_KE request. So we only look at the cases in which + * B wins. + * + * Besides the scenario depicted above, i.e. where the response arrives after + * handling B's IKE_FOLLOWUP_KE request, we also test when it arrives after + * handling the delete. + */ +START_TEST(test_collision_delayed_response_multi_ke) +{ + ike_sa_t *a, *b; + message_t *msg; + bool after_delete = _i >= 2; + + _i %= 2; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, B wins each time (with either of its nonces), + * but the response arrives at different times. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* IKE_FOLLOWUP_KE { KEr } --> */ + assert_hook_rekey(child_rekey, 2, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + if (!after_delete) + { /* a receives the response right after the IKE_FOLLOWUP_KE, the passive + * rekeying is completed and the active aborted */ + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + } + + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_child_sa_count(a, 2); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 6); + assert_child_sa_count(b, 2); + assert_scheduler(); + + if (after_delete) + { + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + } + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_b); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); + destroy_rekeyed(b, data[_i].spi_del_b); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * In this scenario one of the peers does not notice that there is a * rekey collision: @@ -1122,6 +2039,178 @@ START_TEST(test_collision_delayed_request_more) } END_TEST +/** + * In this scenario one of the peers does not notice that there is a + * rekey collision: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * \ /---- send additional KE + * \-/----> detect collision + * handle KE <---------/ /---- TEMP_FAIL + * -----------/----> + * <---------/------ delete old SA + * delete ---------/------> + * aborts rekeying <-------/ + * + * In a variation of this scenario, the TEMP_FAIL notify arrives before the + * delete does. + */ +START_TEST(test_collision_delayed_request_multi_ke) +{ + ike_sa_t *a, *b; + message_t *msg; + bool after_delete = _i >= 3; + + _i %= 3; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + } data[] = { + { { 0x00, 0xFF, 0xFF } }, + { { 0xFF, 0x00, 0xFF } }, + { { 0xFF, 0xFF, 0x00 } }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr, N(ADD_KE) } --> */ + assert_hook_not_called(child_rekey); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 1, 2); + assert_hook(); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + + /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ + assert_hook_rekey(child_rekey, 1, 5); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); + assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + if (!after_delete) + { + assert_hook_not_called(child_rekey); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_scheduler(); + assert_hook(); + } + else + { /* delay until we received the delete */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + } + + /* IKE_FOLLOWUP_KE { KEr } --> */ + assert_hook_rekey(child_rekey, 2, 4); + assert_payload(IN, PLV2_KEY_EXCHANGE); + assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_scheduler(); + + if (after_delete) + { + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + } + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 4, 5); + destroy_rekeyed(b, 2); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 4, 5); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * Both peers initiate the CHILD_SA reekying concurrently but the proposed DH * groups are not the same after handling the INVALID_KE_PAYLOAD they should @@ -1583,6 +2672,94 @@ START_TEST(test_collision_delete) } END_TEST +/** + * One of the hosts initiates a DELETE of the CHILD_SA the other peer is + * concurrently trying to rekey with multiple key exchanges. + * + * rekey ----------------> + * <---------------- + * additional ke ----\ /---- delete + * \-----/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * delete -----------/----> + * aborts rekeying <---------/ + */ +START_TEST(test_collision_delete_multi_ke) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1, spi_b = 2-_i; + + if (_i) + { /* responder rekeys the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &multi_ke_conf); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + } + initiate_rekey(a, spi_a); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr, N(ADD_KE) } */ + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + + call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); + assert_child_sa_state(b, spi_b, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + + /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ + assert_hook_not_called(child_updown); + assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_single_payload(OUT, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_updown); + /* we don't expect a job to retry the rekeying */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_scheduler(); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * One of the hosts initiates a DELETE of the CHILD_SA the other peer is * concurrently trying to rekey. However, the delete request is delayed or @@ -1950,22 +3127,29 @@ 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_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); tcase_add_test(tc, test_regular_responder_handle_hard_expire); suite_add_tcase(s, tc); tc = tcase_create("collisions 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); tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4); tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_request_more, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_multi_ke, 0, 6); tcase_add_loop_test(tc, test_collision_ke_invalid, 0, 4); tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3); suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); 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); tcase_add_loop_test(tc, test_collision_delete_drop_rekey, 0, 2); suite_add_tcase(s, tc); From 882ff93bfd0094652862bab001d16bbd4bd8863e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 22 Oct 2020 13:13:00 +0200 Subject: [PATCH 40/46] proposal: Accept NONE for additional key exchanges also for IKE proposals --- src/libstrongswan/crypto/proposal/proposal.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index ff7884d9a..b8c74906e 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -324,7 +324,8 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, if (is_ke_transform(type)) { - optional = this->protocol == PROTO_ESP || this->protocol == PROTO_AH; + optional = this->protocol == PROTO_ESP || this->protocol == PROTO_AH || + type != KEY_EXCHANGE_METHOD; } e1 = create_enumerator(this, type); From 1d5e92191122b109369c30d30328a815ae47d876 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 5 Nov 2019 17:03:42 +0100 Subject: [PATCH 41/46] proposal: Add helper to check if additional key exchanges are contained --- src/libstrongswan/crypto/proposal/proposal.c | 24 +++++++++++++++++++ src/libstrongswan/crypto/proposal/proposal.h | 8 +++++++ .../tests/suites/test_proposal.c | 18 ++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index b8c74906e..606aa4ca3 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -1426,3 +1426,27 @@ proposal_t *proposal_select(linked_list_t *configured, linked_list_t *supplied, } return selected; } + +/* + * Described in header + */ +bool proposal_has_additional_ke(proposal_t *public) +{ + private_proposal_t *this = (private_proposal_t*)public; + enumerator_t *enumerator; + entry_t *entry; + bool found = FALSE; + + enumerator = array_create_enumerator(this->transforms); + while (enumerator->enumerate(enumerator, &entry)) + { + if (entry->type != KEY_EXCHANGE_METHOD && + is_ke_transform(entry->type)) + { + found = TRUE; + break; + } + } + enumerator->destroy(enumerator); + return found; +} diff --git a/src/libstrongswan/crypto/proposal/proposal.h b/src/libstrongswan/crypto/proposal/proposal.h index 29fda8b8b..1005ed0a8 100644 --- a/src/libstrongswan/crypto/proposal/proposal.h +++ b/src/libstrongswan/crypto/proposal/proposal.h @@ -287,6 +287,14 @@ proposal_t *proposal_create_from_string(protocol_id_t protocol, proposal_t *proposal_select(linked_list_t *configured, linked_list_t *supplied, proposal_selection_flag_t flags); +/** + * Check whether this proposal contains algorithms for any additional key + * exchange method transform types. + * + * @return TRUE if found + */ +bool proposal_has_additional_ke(proposal_t *this); + /** * printf hook function for proposal_t. * diff --git a/src/libstrongswan/tests/suites/test_proposal.c b/src/libstrongswan/tests/suites/test_proposal.c index c42f9e4a6..7798bfb24 100644 --- a/src/libstrongswan/tests/suites/test_proposal.c +++ b/src/libstrongswan/tests/suites/test_proposal.c @@ -474,6 +474,20 @@ START_TEST(test_unknown_transform_types_select_success) } END_TEST +START_TEST(test_proposal_has_additional_ke) +{ + proposal_t *proposal; + + proposal = proposal_create_from_string(PROTO_IKE, "aes128-sha256-ecp256"); + ck_assert(!proposal_has_additional_ke(proposal)); + proposal->destroy(proposal); + + proposal = proposal_create_from_string(PROTO_IKE, "aes128-sha256-modp3072-ke1_ecp256"); + ck_assert(proposal_has_additional_ke(proposal)); + proposal->destroy(proposal); +} +END_TEST + START_TEST(test_chacha20_poly1305_key_length) { proposal_t *proposal; @@ -575,6 +589,10 @@ Suite *proposal_suite_create() tcase_add_test(tc, test_unknown_transform_types_select_success); suite_add_tcase(s, tc); + tc = tcase_create("proposal_has_additional_ke"); + tcase_add_test(tc, test_proposal_has_additional_ke); + suite_add_tcase(s, tc); + tc = tcase_create("chacha20/poly1305"); tcase_add_test(tc, test_chacha20_poly1305_key_length); suite_add_tcase(s, tc); From 012d99ecf4ce66eca44388fab9580a9c912d1e2f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 16 Jul 2021 13:52:19 +0200 Subject: [PATCH 42/46] proposal: Prevent selection of duplicate key exchange methods All additional (and the initial) key exchanges must use a different method. --- src/libstrongswan/crypto/proposal/proposal.c | 38 +++++++++++++++++-- .../tests/suites/test_proposal.c | 22 +++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index 606aa4ca3..869f08f09 100644 --- a/src/libstrongswan/crypto/proposal/proposal.c +++ b/src/libstrongswan/crypto/proposal/proposal.c @@ -21,6 +21,7 @@ #include "proposal.h" #include +#include #include #include @@ -316,7 +317,7 @@ METHOD(proposal_t, promote_transform, bool, */ static bool select_algo(private_proposal_t *this, proposal_t *other, transform_type_t type, proposal_selection_flag_t flags, - bool log, uint16_t *alg, uint16_t *ks) + hashtable_t *kes, bool log, uint16_t *alg, uint16_t *ks) { enumerator_t *e1, *e2; uint16_t alg1, alg2, ks1, ks2; @@ -359,9 +360,13 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, e1->destroy(e1); e1 = create_enumerator(this, type); - /* compare algs, order of algs in "first" is preferred */ + /* compare algs, order of algs in "e1" is preferred */ while (!found && e1->enumerate(e1, &alg1, &ks1)) { + if (is_ke_transform(type) && kes->get(kes, (void*)(uintptr_t)alg1)) + { + continue; + } e2->destroy(e2); e2 = other->create_enumerator(other, type); while (e2->enumerate(e2, &alg2, &ks2)) @@ -390,6 +395,23 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, return found; } +/** + * Hash an algorithm identifier + */ +static u_int hash_alg(const void *key) +{ + uint16_t alg = (uint16_t)(uintptr_t)key; + return chunk_hash(chunk_from_thing(alg)); +} + +/** + * Compare two algorithm identifiers + */ +static bool equals_alg(const void *key, const void *other_key) +{ + return (uint16_t)(uintptr_t)key == (uint16_t)(uintptr_t)other_key; +} + /** * Select algorithms from the given proposals, if selected is given, the result * is stored there and errors are logged. @@ -398,10 +420,13 @@ static bool select_algos(private_proposal_t *this, proposal_t *other, proposal_t *selected, proposal_selection_flag_t flags) { transform_type_t type; + hashtable_t *kes; array_t *types; bool skip_integrity = FALSE; int i; + kes = hashtable_create(hash_alg, equals_alg, 8); + types = merge_types(this, (private_proposal_t*)other); for (i = 0; i < array_count(types); i++) { @@ -416,7 +441,8 @@ static bool select_algos(private_proposal_t *this, proposal_t *other, { continue; } - if (select_algo(this, other, type, flags, selected != NULL, &alg, &ks)) + if (select_algo(this, other, type, flags, kes, selected != NULL, + &alg, &ks)) { if (alg == 0 && type != EXTENDED_SEQUENCE_NUMBERS) { /* 0 is "valid" for extended sequence numbers, for other @@ -427,6 +453,10 @@ static bool select_algos(private_proposal_t *this, proposal_t *other, { selected->add_algorithm(selected, type, alg, ks); } + if (is_ke_transform(type)) + { + kes->put(kes, (void*)(uintptr_t)alg, (void*)(uintptr_t)alg); + } if (type == ENCRYPTION_ALGORITHM && encryption_algorithm_is_aead(alg)) { @@ -442,10 +472,12 @@ static bool select_algos(private_proposal_t *this, proposal_t *other, type); } array_destroy(types); + kes->destroy(kes); return FALSE; } } array_destroy(types); + kes->destroy(kes); return TRUE; } diff --git a/src/libstrongswan/tests/suites/test_proposal.c b/src/libstrongswan/tests/suites/test_proposal.c index 7798bfb24..78724bb6f 100644 --- a/src/libstrongswan/tests/suites/test_proposal.c +++ b/src/libstrongswan/tests/suites/test_proposal.c @@ -143,6 +143,28 @@ static struct { { PROTO_IKE, "aes128-sha256-modp3072", "aes128-sha256-modp3072", "aes128-sha256-modp3072" }, { PROTO_IKE, "aes128-sha256-modp3072", "aes128-sha256-modp3072-none", "aes128-sha256-modp3072" }, { PROTO_IKE, "aes128-sha256-modp3072-none", "aes128-sha256-modp3072", "aes128-sha256-modp3072" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_modp3072", + "aes128-sha256-modp3072-ke1_modp3072", NULL }, + { PROTO_IKE, "aes128-sha256-modp3072-ecp256-ecp384-ke1_modp3072-ke1_ecp256-ke1_ecp384-ke2_modp3072-ke2_ecp256-ke2_ecp384", + "aes128-sha256-modp3072-ecp256-ecp384-ke1_modp3072-ke1_ecp256-ke1_ecp384-ke2_modp3072-ke2_ecp256-ke2_ecp384", + "aes128-sha256-modp3072-ke1_ecp256-ke2_ecp384" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_modp3072-ke1_none", + "aes128-sha256-modp3072-ke1_modp3072-ke1_none", + "aes128-sha256-modp3072" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_modp3072-ke1_none-ke2_modp3072-ke2_none", + "aes128-sha256-modp3072-ke1_modp3072-ke1_none-ke2_modp3072-ke2_none", + "aes128-sha256-modp3072" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_ecp256" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_ecp256" }, + { PROTO_IKE, "aes128-sha256-modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_modp3072-ke1_ecp256", + "aes128-sha256-modp3072-ke1_ecp256" }, + { PROTO_IKE, "aes128-sha256-ecp256-ke1_modp3072", + "aes128-sha256-modp3072-ecp256-ke1_ecp256-ke2_ecp384", NULL }, }; START_TEST(test_select) From 7ad610a140d20bca67b427e61b69d1661a0b6403 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 5 Nov 2019 16:42:58 +0100 Subject: [PATCH 43/46] ike-init: Indicate support for IKE_INTERMEDIATE --- src/libcharon/sa/ike_sa.h | 7 ++++- src/libcharon/sa/ikev2/tasks/ike_init.c | 36 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ike_sa.h b/src/libcharon/sa/ike_sa.h index 1c5333db8..ea81de210 100644 --- a/src/libcharon/sa/ike_sa.h +++ b/src/libcharon/sa/ike_sa.h @@ -161,7 +161,7 @@ enum ike_extension_t { EXT_IKE_MESSAGE_ID_SYNC = (1<<14), /** - * Postquantum Preshared Keys, draft-ietf-ipsecme-qr-ikev2 + * Postquantum Preshared Keys, RFC 8784 */ EXT_PPK = (1<<15), @@ -169,6 +169,11 @@ enum ike_extension_t { * Responder accepts childless IKE_SAs, RFC 6023 */ EXT_IKE_CHILDLESS = (1<<16), + + /** + * IKEv2 Intermediate Exchange, RFC 9242 + */ + EXT_IKE_INTERMEDIATE = (1<<17), }; /** diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index 4eda3a062..44a89f9eb 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -348,6 +348,7 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) proposal_t *proposal; enumerator_t *enumerator; ike_cfg_t *ike_cfg; + bool additional_ke = FALSE; id = this->ike_sa->get_id(this->ike_sa); @@ -372,6 +373,8 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) proposal_list->remove_at(proposal_list, enumerator); other_ke_methods->insert_last(other_ke_methods, proposal); } + additional_ke = additional_ke || + proposal_has_additional_ke(proposal); } enumerator->destroy(enumerator); /* add proposals that don't contain the selected group */ @@ -394,6 +397,7 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) this->proposal->set_spi(this->proposal, id->get_responder_spi(id)); } 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); @@ -467,6 +471,16 @@ static bool build_payloads(private_ike_init_t *this, message_t *message) message->add_notify(message, FALSE, CHILDLESS_IKEV2_SUPPORTED, chunk_empty); } + if (!this->old_sa && additional_ke) + { + if (this->initiator || + this->ike_sa->supports_extension(this->ike_sa, + EXT_IKE_INTERMEDIATE)) + { + message->add_notify(message, FALSE, INTERMEDIATE_EXCHANGE_SUPPORTED, + chunk_empty); + } + } return TRUE; } @@ -726,6 +740,13 @@ static void process_payloads(private_ike_init_t *this, message_t *message) EXT_IKE_CHILDLESS); } break; + case INTERMEDIATE_EXCHANGE_SUPPORTED: + if (!this->old_sa) + { + this->ike_sa->enable_extension(this->ike_sa, + EXT_IKE_INTERMEDIATE); + } + break; default: /* other notifies are handled elsewhere */ break; @@ -1163,6 +1184,14 @@ METHOD(task_t, build_r, status_t, if (key_exchange_done(this) == NEED_MORE) { + if (!this->old_sa && + !this->ike_sa->supports_extension(this->ike_sa, EXT_IKE_INTERMEDIATE)) + { + DBG1(DBG_IKE, "peer didn't send %N while proposing multiple key " + "exchanges", notify_type_names, INTERMEDIATE_EXCHANGE_SUPPORTED); + message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); + return FAILED; + } /* use other exchange type for additional key exchanges */ this->public.task.build = _build_r_multi_ke; this->public.task.process = _process_r_multi_ke; @@ -1412,6 +1441,13 @@ METHOD(task_t, process_i, status_t, if (key_exchange_done(this) == NEED_MORE) { + if (!this->old_sa && + !this->ike_sa->supports_extension(this->ike_sa, EXT_IKE_INTERMEDIATE)) + { + DBG1(DBG_IKE, "peer didn't send %N while accepting multiple key " + "exchanges", notify_type_names, INTERMEDIATE_EXCHANGE_SUPPORTED); + return FAILED; + } /* use other exchange type for additional key exchanges */ this->public.task.build = _build_i_multi_ke; this->public.task.process = _process_i_multi_ke; From d2b2e1b3fae8b49b4c412ee1492d18638d17e497 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 22 Aug 2022 15:43:16 +0200 Subject: [PATCH 44/46] ikev2: Make CHILD_SAs properly trackable during rekey collisions As the winner of a rekey collision, we previously always triggered the child_rekey() event once when creating the redundant SA on behalf of the peer in the passive child-rekey task and then a second time when creating the winning SA in the active task. However, both calls passed the replaced CHILD_SA as "old". This made tracking CHILD_SAs impossible because there was no transition from the redundant, "new" SA of the first event to the "new", winning SA of the second. Of course, when the second event was triggered, the redundant SA might not have existed anymore because the peer is expected to delete it, which could happen before the CREATE_CHILD_SA response arrives at the initiator. This refactoring ensures that the child_rekey() event is triggered in a way that makes the CHILD_SAs trackable in all reasonable (and even some unreasonable) scenarios. The event is generally only triggered once after installing the outbound SA for the new/winning CHILD_SA. This can be when processing the CREATE_CHILD_SA in the active child-rekey task, or when processing the DELETE for the old SA in a passive child-delete task. There are some cases where the event is still triggered twice, but it is now ensured that listeners can properly transition to the winning SA. Some corner cases are now also handled correctly, e.g. if a responder's DELETE for the new CHILD_SA arrives before its CREATE_CHILD_SA response that actually creates it on the initiator. Also handled properly are responders of rekeyings that incorrectly send a DELETE for the old CHILD_SA (previously this caused both, the new and the old SA, to get deleted). --- src/libcharon/sa/child_sa.c | 19 +- src/libcharon/sa/child_sa.h | 19 +- src/libcharon/sa/ikev2/task_manager_v2.c | 9 +- src/libcharon/sa/ikev2/tasks/child_create.c | 42 +- src/libcharon/sa/ikev2/tasks/child_create.h | 7 + src/libcharon/sa/ikev2/tasks/child_delete.c | 790 ++++---- src/libcharon/sa/ikev2/tasks/child_delete.h | 41 +- src/libcharon/sa/ikev2/tasks/child_rekey.c | 920 +++++++--- src/libcharon/sa/ikev2/tasks/child_rekey.h | 51 +- src/libcharon/tests/suites/test_child_rekey.c | 1599 ++++++++++++++--- 10 files changed, 2641 insertions(+), 856 deletions(-) diff --git a/src/libcharon/sa/child_sa.c b/src/libcharon/sa/child_sa.c index 97ee88acb..1f50c4952 100644 --- a/src/libcharon/sa/child_sa.c +++ b/src/libcharon/sa/child_sa.c @@ -131,9 +131,10 @@ struct private_child_sa_t { bool tfcv3; /** - * The outbound SPI of the CHILD_SA that replaced this one during a rekeying + * The "other" CHILD_SA involved in a passive rekeying (either replacing + * this one, or being replaced by it) */ - uint32_t rekey_spi; + child_sa_t *rekey_sa; /** * Protocol used to protect this SA, ESP|AH @@ -1588,16 +1589,16 @@ METHOD(child_sa_t, remove_outbound, void, this->outbound_state = CHILD_OUTBOUND_NONE; } -METHOD(child_sa_t, set_rekey_spi, void, - private_child_sa_t *this, uint32_t spi) +METHOD(child_sa_t, set_rekey_sa, void, + private_child_sa_t *this, child_sa_t *sa) { - this->rekey_spi = spi; + this->rekey_sa = sa; } -METHOD(child_sa_t, get_rekey_spi, uint32_t, +METHOD(child_sa_t, get_rekey_sa, child_sa_t*, private_child_sa_t *this) { - return this->rekey_spi; + return this->rekey_sa; } CALLBACK(reinstall_vip, void, @@ -2077,8 +2078,8 @@ child_sa_t *child_sa_create(host_t *me, host_t *other, child_cfg_t *config, .register_outbound = _register_outbound, .install_outbound = _install_outbound, .remove_outbound = _remove_outbound, - .set_rekey_spi = _set_rekey_spi, - .get_rekey_spi = _get_rekey_spi, + .set_rekey_sa = _set_rekey_sa, + .get_rekey_sa = _get_rekey_sa, .update = _update, .set_policies = _set_policies, .install_policies = _install_policies, diff --git a/src/libcharon/sa/child_sa.h b/src/libcharon/sa/child_sa.h index 0b7d11114..7c3763b0a 100644 --- a/src/libcharon/sa/child_sa.h +++ b/src/libcharon/sa/child_sa.h @@ -504,23 +504,24 @@ struct child_sa_t { status_t (*install_policies)(child_sa_t *this); /** - * Set the outbound SPI of the CHILD_SA that replaced this CHILD_SA during - * a rekeying. + * Set the CHILD_SA that either replaced this one or the CHILD_SA that is + * being replaced by this one during a passive rekeying (i.e. it links the + * two SAs bidirectionally). * - * @param spi outbound SPI of the CHILD_SA that replaced this CHILD_SA + * @param sa other CHILD_SA involved in a passive rekeying */ - void (*set_rekey_spi)(child_sa_t *this, uint32_t spi); + void (*set_rekey_sa)(child_sa_t *this, child_sa_t *sa); /** - * Get the outbound SPI of the CHILD_SA that replaced this CHILD_SA during - * a rekeying. + * Get the CHILD_SA that's linked to this in a passive rekeying (either + * replacing this one, or being replaced by it). * - * @return outbound SPI of the CHILD_SA that replaced this CHILD_SA + * @return other CHILD_SA involved in a passive rekeying */ - uint32_t (*get_rekey_spi)(child_sa_t *this); + child_sa_t *(*get_rekey_sa)(child_sa_t *this); /** - * Update hosts and ecapsulation mode in the kernel SAs and policies. + * Update hosts and encapsulation mode in the kernel SAs and policies. * * @param me the new local host * @param other the new remote host diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index c5cc34f0e..30dba22db 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -935,9 +935,10 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task) type = task->get_type(task); - /* do we have to check */ - if (type == TASK_IKE_REKEY || type == TASK_CHILD_REKEY || - type == TASK_CHILD_DELETE || type == TASK_IKE_DELETE) + /* collisions between a child-rekey and child-delete task are handled + * directly by the latter */ + if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE || + type == TASK_CHILD_REKEY) { /* find an exchange collision, and notify these tasks */ enumerator = array_create_enumerator(this->active_tasks); @@ -954,7 +955,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task) } continue; case TASK_CHILD_REKEY: - if (type == TASK_CHILD_REKEY || type == TASK_CHILD_DELETE) + if (type == TASK_CHILD_REKEY) { child_rekey_t *rekey = (child_rekey_t*)active; adopted = rekey->collide(rekey, task); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 132c3de46..c3a12780f 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -2184,7 +2184,7 @@ METHOD(task_t, build_i_delete, status_t, DBG1(DBG_IKE, "sending DELETE for %N CHILD_SA with SPI %.8x", protocol_id_names, this->proto, ntohl(this->my_spi)); } - return NEED_MORE; + return SUCCESS; } /** @@ -2195,7 +2195,6 @@ static status_t delete_failed_sa(private_child_create_t *this) if (this->my_spi && this->proto) { this->public.task.build = _build_i_delete; - this->public.task.process = (void*)return_success; /* destroying it here allows the rekey task to differentiate between * this and the multi-KE case */ this->child_sa->destroy(this->child_sa); @@ -2232,6 +2231,17 @@ static status_t key_exchange_done_and_install_i(private_child_create_t *this, METHOD(task_t, process_i_multi_ke, status_t, private_child_create_t *this, message_t *message) { + if (message->get_notify(message, TEMPORARY_FAILURE)) + { + DBG1(DBG_IKE, "received %N notify", notify_type_names, + TEMPORARY_FAILURE); + if (!this->rekey) + { /* the rekey task will retry itself if necessary */ + schedule_delayed_retry(this); + } + return SUCCESS; + } + process_payloads_multi_ke(this, message); if (this->ke_failed) @@ -2304,8 +2314,7 @@ METHOD(task_t, process_i, status_t, } case TEMPORARY_FAILURE: { - DBG1(DBG_IKE, "received %N notify, will retry later", - notify_type_names, type); + DBG1(DBG_IKE, "received %N notify", notify_type_names, type); enumerator->destroy(enumerator); if (!this->rekey) { /* the rekey task will retry itself if necessary */ @@ -2365,6 +2374,15 @@ METHOD(task_t, process_i, status_t, process_payloads(this, message); + if (!select_proposal(this, no_ke)) + { + handle_child_sa_failure(this, message); + return delete_failed_sa(this); + } + + this->other_spi = this->proposal->get_spi(this->proposal); + this->proposal->set_spi(this->proposal, this->my_spi); + if (this->ipcomp == IPCOMP_NONE && this->ipcomp_received != IPCOMP_NONE) { DBG1(DBG_IKE, "received an IPCOMP_SUPPORTED notify without requesting" @@ -2386,15 +2404,6 @@ METHOD(task_t, process_i, status_t, return delete_failed_sa(this); } - if (!select_proposal(this, no_ke)) - { - handle_child_sa_failure(this, message); - return delete_failed_sa(this); - } - - this->other_spi = this->proposal->get_spi(this->proposal); - this->proposal->set_spi(this->proposal, this->my_spi); - if (!check_ke_method(this, NULL)) { handle_child_sa_failure(this, message); @@ -2485,6 +2494,12 @@ METHOD(child_create_t, get_child, child_sa_t*, return this->child_sa; } +METHOD(child_create_t, get_other_spi, uint32_t, + private_child_create_t *this) +{ + return this->other_spi; +} + METHOD(child_create_t, set_config, void, private_child_create_t *this, child_cfg_t *cfg) { @@ -2623,6 +2638,7 @@ child_create_t *child_create_create(ike_sa_t *ike_sa, INIT(this, .public = { .get_child = _get_child, + .get_other_spi = _get_other_spi, .set_config = _set_config, .get_lower_nonce = _get_lower_nonce, .use_reqid = _use_reqid, diff --git a/src/libcharon/sa/ikev2/tasks/child_create.h b/src/libcharon/sa/ikev2/tasks/child_create.h index 2e8c8bed7..eae39e61c 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.h +++ b/src/libcharon/sa/ikev2/tasks/child_create.h @@ -103,6 +103,13 @@ struct child_create_t { */ child_sa_t* (*get_child) (child_create_t *this); + /** + * Get the SPI of the other peer's selected proposal, if available. + * + * @return other's SPI, 0 if unknown + */ + uint32_t (*get_other_spi)(child_create_t *this); + /** * Enforce a specific CHILD_SA config as responder. * diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index eff0e6cc6..e2e198b28 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2016 Tobias Brunner + * Copyright (C) 2009-2022 Tobias Brunner * Copyright (C) 2006-2007 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -76,10 +76,10 @@ struct private_child_delete_t { typedef struct { /** Deleted CHILD_SA */ child_sa_t *child_sa; - /** Whether the CHILD_SA was rekeyed */ - bool rekeyed; - /** Whether to enforce any delete action policy */ - bool check_delete_action; + /** The original state of the CHILD_SA */ + child_sa_state_t orig_state; + /** How this CHILD_SA collides with an active rekeying */ + child_rekey_collision_t collision; } entry_t; CALLBACK(match_child, bool, @@ -133,18 +133,448 @@ static void build_payloads(private_child_delete_t *this, message_t *message) default: break; } - entry->child_sa->set_state(entry->child_sa, CHILD_DELETING); } enumerator->destroy(enumerator); } /** - * Check if the given CHILD_SA is the redundant SA created in a rekey collision. + * Install the outbound SA of the CHILD_SA that replaced the given CHILD_SA + * in a rekeying. */ -static bool is_redundant(private_child_delete_t *this, child_sa_t *child) +static void conclude_rekeying(private_child_delete_t *this, child_sa_t *old) +{ + child_sa_t *child_sa; + + child_sa = old->get_rekey_sa(old); + old->set_rekey_sa(old, NULL); + child_sa->set_rekey_sa(child_sa, NULL); + child_rekey_conclude_rekeying(old, child_sa); +} + +/** + * Destroy and optionally reestablish the given CHILD_SA according to config. + */ +static status_t destroy_and_reestablish_internal(ike_sa_t *ike_sa, + child_sa_t *child_sa, + bool trigger_updown, + bool delete_action, + action_t forced_action) +{ + child_init_args_t args = {}; + child_cfg_t *child_cfg; + protocol_id_t protocol; + uint32_t spi; + action_t action; + status_t status = SUCCESS; + + child_sa->set_state(child_sa, CHILD_DELETED); + if (trigger_updown) + { + charon->bus->child_updown(charon->bus, child_sa, FALSE); + } + + protocol = child_sa->get_protocol(child_sa); + spi = child_sa->get_spi(child_sa, TRUE); + child_cfg = child_sa->get_config(child_sa); + child_cfg->get_ref(child_cfg); + args.reqid = child_sa->get_reqid_ref(child_sa); + args.label = child_sa->get_label(child_sa); + if (args.label) + { + args.label = args.label->clone(args.label); + } + action = forced_action ?: child_sa->get_close_action(child_sa); + + DBG1(DBG_IKE, "CHILD_SA %s{%u} closed", child_sa->get_name(child_sa), + child_sa->get_unique_id(child_sa)); + + ike_sa->destroy_child_sa(ike_sa, protocol, spi); + + if (delete_action) + { + if (action & ACTION_TRAP) + { + charon->traps->install(charon->traps, + ike_sa->get_peer_cfg(ike_sa), + child_cfg); + } + if (action & ACTION_START) + { + child_cfg->get_ref(child_cfg); + status = ike_sa->initiate(ike_sa, child_cfg, &args); + } + } + child_cfg->destroy(child_cfg); + if (args.reqid) + { + charon->kernel->release_reqid(charon->kernel, args.reqid); + } + DESTROY_IF(args.label); + return status; +} + +/* + * Described in header + */ +status_t child_delete_destroy_and_reestablish(ike_sa_t *ike_sa, + child_sa_t *child_sa) +{ + return destroy_and_reestablish_internal(ike_sa, child_sa, TRUE, TRUE, 0); +} + +/* + * Described in header + */ +status_t child_delete_destroy_and_force_reestablish(ike_sa_t *ike_sa, + child_sa_t *child_sa) +{ + return destroy_and_reestablish_internal(ike_sa, child_sa, TRUE, TRUE, + ACTION_START); +} + +/* + * Described in header + */ +void child_delete_destroy_rekeyed(ike_sa_t *ike_sa, child_sa_t *child_sa) +{ + time_t now, expire; + u_int delay; + + /* make sure the SA is in the correct state and the outbound SA is not + * installed */ + child_sa->remove_outbound(child_sa); + child_sa->set_state(child_sa, CHILD_DELETED); + + now = time_monotonic(NULL); + delay = lib->settings->get_int(lib->settings, "%s.delete_rekeyed_delay", + DELETE_REKEYED_DELAY, lib->ns); + + expire = child_sa->get_lifetime(child_sa, TRUE); + if (delay && (!expire || ((now + delay) < expire))) + { + DBG1(DBG_IKE, "delay closing of inbound CHILD_SA %s{%u} for %us", + child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa), + delay); + lib->scheduler->schedule_job(lib->scheduler, + (job_t*)delete_child_sa_job_create_id( + child_sa->get_unique_id(child_sa)), delay); + return; + } + else if (now < expire) + { + /* let it expire naturally */ + DBG1(DBG_IKE, "let rekeyed inbound CHILD_SA %s{%u} expire naturally " + "in %us", child_sa->get_name(child_sa), + child_sa->get_unique_id(child_sa), expire-now); + return; + } + /* no delay and no lifetime, destroy it immediately. since we suppress + * actions, there is no need to check the return value */ + destroy_and_reestablish_internal(ike_sa, child_sa, FALSE, FALSE, 0); +} + +/** + * Check if the SA should be ignored and kept until a concurrent active rekeying + * is concluded (the rekey task is responsible for destroying the CHILD_SA). + */ +static bool keep_while_rekeying(entry_t *entry) +{ + switch (entry->collision) + { + case CHILD_REKEY_COLLISION_NONE: + break; + case CHILD_REKEY_COLLISION_OLD: + /* if the peer deletes the SA we are trying to rekey and there + * hasn't been a collision, it might have sent the delete before our + * request arrived. but it could also be an incorrect delete sent + * after it processed our rekey request, which we'd have to ignore. + * the active rekey task will decide once it has the response */ + if (entry->orig_state == CHILD_REKEYING) + { + return TRUE; + } + /* if there was a collision, the peer is expected to delete the old + * SA only if it won the collision, the SA is in state CHILD_REKEYED + * in this case. we don't completely ignore the SA and conclude the + * rekeying for it now to switch to the new outbound SA (the peer + * will remove the old inbound SA once it receives the DELETE + * response), but don't destroy the old SA yet even though we return + * FALSE here. + * the active rekey task will later decide if the delete was + * legitimate or an incorrect delete for the old SA */ + break; + case CHILD_REKEY_COLLISION_PEER: + /* the peer deletes the SA it created itself before we received + * the rekey response, this is either the redundant SA, which + * would be fine, or the winning SA it already is deleting for + * some reason (presumably, after also sending a delete for the + * rekeyed SA). let the active rekey task decide once it receives + * the response and knows who won the collision */ + return TRUE; + } + return FALSE; +} + +/** + * Log an SA we are not yet closing completely. + */ +static void log_kept_sa(entry_t *entry) +{ + DBG1(DBG_IKE, "keeping %s CHILD_SA %s{%u} until active rekeying is " + "concluded", + entry->collision == CHILD_REKEY_COLLISION_OLD ? "rekeyed" + : "peer's", + entry->child_sa->get_name(entry->child_sa), + entry->child_sa->get_unique_id(entry->child_sa)); +} + +/** + * Destroy the children listed in this->child_sas, reestablish by policy + */ +static status_t destroy_and_reestablish(private_child_delete_t *this) +{ + enumerator_t *enumerator; + entry_t *entry; + child_sa_t *child_sa, *other; + status_t status = SUCCESS; + + enumerator = this->child_sas->create_enumerator(this->child_sas); + while (enumerator->enumerate(enumerator, (void**)&entry)) + { + child_sa = entry->child_sa; + other = child_sa->get_rekey_sa(child_sa); + + /* check if we have to keep the SA during a collision with an active + * rekey task */ + if (keep_while_rekeying(entry)) + { + /* if the peer deleted its own SA, reset the link to the old SA, + * which might already be reset if the peer deleted the old SA + * first (the active rekey task will eventually destroy both) */ + if (other && entry->collision == CHILD_REKEY_COLLISION_PEER) + { + child_sa->set_rekey_sa(child_sa, NULL); + other->set_rekey_sa(other, NULL); + + /* reset the state of the old SA until the active rekey task is + * done, but only if it's not also getting deleted by the peer + * and is already in state DELETING. note that we won't end up + * here if the peer deleted the old SA first as the link between + * the two SAs would already be reset then. so this is only the + * case if the peer sends the deletes for both SAs in the same + * message and the payload for the old one comes after the one + * for its own SA */ + if (other->get_state(other) == CHILD_REKEYED) + { + other->set_state(other, CHILD_REKEYING); + } + } + log_kept_sa(entry); + continue; + } + + child_sa->set_state(child_sa, CHILD_DELETED); + + if (entry->orig_state == CHILD_REKEYED) + { + /* conclude the rekeying as responder/loser. the initiator/winner + * already did this right after the rekeying was completed (or + * before a delete was initiated), but in some cases the outbound + * SA was not yet removed, make sure it is */ + if (other) + { + conclude_rekeying(this, child_sa); + } + else + { + child_sa->remove_outbound(child_sa); + } + + /* if this is a delete for the SA we are actively rekeying, let the + * rekey task handle the SA appropriately once the collision is + * resolved. otherwise, destroy the SA now, but usually delayed to + * process delayed packets */ + if (entry->collision == CHILD_REKEY_COLLISION_OLD) + { + log_kept_sa(entry); + } + else + { + child_delete_destroy_rekeyed(this->ike_sa, child_sa); + } + } + else + { + /* regular CHILD_SA delete, with one special case after a lost + * collision. usually, the peer will delete the old SA and we + * conclude the rekeying above. however, if it deletes its winning + * SA first, we assume it wants to delete the CHILD_SA and we + * conclude the rekeying here to trigger the events correctly */ + if (other && entry->orig_state == CHILD_INSTALLED) + { + conclude_rekeying(this, other); + } + status = destroy_and_reestablish_internal(this->ike_sa, child_sa, + TRUE, !this->initiator && + entry->orig_state == CHILD_INSTALLED, 0); + if (status != SUCCESS) + { + break; + } + } + } + enumerator->destroy(enumerator); + return status; +} + +/** + * Print a log message for every closed CHILD_SA + */ +static void log_children(private_child_delete_t *this) +{ + linked_list_t *my_ts, *other_ts; + enumerator_t *enumerator; + entry_t *entry; + child_sa_t *child_sa; + uint64_t bytes_in, bytes_out; + + enumerator = this->child_sas->create_enumerator(this->child_sas); + while (enumerator->enumerate(enumerator, (void**)&entry)) + { + child_sa = entry->child_sa; + my_ts = linked_list_create_from_enumerator( + child_sa->create_ts_enumerator(child_sa, TRUE)); + other_ts = linked_list_create_from_enumerator( + child_sa->create_ts_enumerator(child_sa, FALSE)); + if (this->expired) + { + DBG0(DBG_IKE, "closing expired CHILD_SA %s{%u} " + "with SPIs %.8x_i %.8x_o and TS %#R === %#R", + child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa), + ntohl(child_sa->get_spi(child_sa, TRUE)), + ntohl(child_sa->get_spi(child_sa, FALSE)), my_ts, other_ts); + } + else + { + child_sa->get_usestats(child_sa, TRUE, NULL, &bytes_in, NULL); + child_sa->get_usestats(child_sa, FALSE, NULL, &bytes_out, NULL); + + DBG0(DBG_IKE, "closing CHILD_SA %s{%u} with SPIs %.8x_i " + "(%llu bytes) %.8x_o (%llu bytes) and TS %#R === %#R", + child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa), + ntohl(child_sa->get_spi(child_sa, TRUE)), bytes_in, + ntohl(child_sa->get_spi(child_sa, FALSE)), bytes_out, + my_ts, other_ts); + } + my_ts->destroy(my_ts); + other_ts->destroy(other_ts); + } + enumerator->destroy(enumerator); +} + +METHOD(task_t, build_i, status_t, + private_child_delete_t *this, message_t *message) +{ + child_sa_t *child_sa, *other; + entry_t *entry; + + child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, + this->spi, TRUE); + if (!child_sa) + { + /* check if it is an outbound SA */ + child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, + this->spi, FALSE); + if (!child_sa) + { + /* child does not exist anymore, abort exchange */ + message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); + return SUCCESS; + } + /* we work only with the inbound SPI */ + this->spi = child_sa->get_spi(child_sa, TRUE); + } + + /* check if this SA is involved in a passive rekeying, either the old + * rekeyed one or the new one created by the peer */ + other = child_sa->get_rekey_sa(child_sa); + if (other) + { + if (child_sa->get_state(child_sa) == CHILD_REKEYED) + { + /* the peer was expected to delete this rekeyed SA. we don't send a + * DELETE, in particular, if this is triggered by an expire, because + * that could cause a collision if the CREATE_CHILD_SA response is + * delayed (the peer might interpret that as a deletion of the SA by + * a user and might then ignore the CREATE_CHILD_SA response once it + * arrives - like old strongSwan versions did - although it + * shouldn't as we properly replied to that request so only a delete + * for the new CHILD_SA should result in a deletion) */ + child_sa->set_state(child_sa, CHILD_DELETED); + conclude_rekeying(this, child_sa); + } + else + { + /* the rekeying for the new SA we are about to delete on the user's + * behalf has not yet been completed, that is, we are waiting for + * the delete for the old SA and have not yet fully installed this + * new one. we do that now so events are triggered properly when + * we delete it */ + DBG2(DBG_IKE, "complete rekeying for %s{%u} before deleting " + "replacement CHILD_SA %s{%u}", + other->get_name(other), other->get_unique_id(other), + child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa)); + conclude_rekeying(this, other); + } + } + + if (child_sa->get_state(child_sa) == CHILD_DELETED) + { + /* DELETEs for this CHILD_SA were already exchanged, but it was not yet + * destroyed to allow delayed packets to get processed, or we suppress + * the DELETE explicitly (see above) */ + destroy_and_reestablish_internal(this->ike_sa, child_sa, FALSE, FALSE, 0); + message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); + return SUCCESS; + } + + INIT(entry, + .child_sa = child_sa, + .orig_state = child_sa->get_state(child_sa), + ); + child_sa->set_state(child_sa, CHILD_DELETING); + this->child_sas->insert_last(this->child_sas, entry); + + log_children(this); + build_payloads(this, message); + + if (this->expired) + { + child_cfg_t *child_cfg; + + DBG1(DBG_IKE, "scheduling CHILD_SA recreate after hard expire"); + child_cfg = child_sa->get_config(child_sa); + this->ike_sa->queue_task(this->ike_sa, (task_t*) + child_create_create(this->ike_sa, child_cfg->get_ref(child_cfg), + FALSE, NULL, NULL)); + } + return NEED_MORE; +} + +/** + * Check if the given CHILD_SA is the SA created by the peer in a rekey + * collision and allow the active rekey task to collect the SPI if it's not yet + * known, in which case it could be for the SA we created in an active rekeying + * that we haven't yet completed. + */ +static child_rekey_collision_t possible_rekey_collision( + private_child_delete_t *this, + child_sa_t *child, uint32_t spi) { enumerator_t *tasks; task_t *task; + child_rekey_t *rekey; + child_rekey_collision_t collision = CHILD_REKEY_COLLISION_NONE; tasks = this->ike_sa->create_task_enumerator(this->ike_sa, TASK_QUEUE_ACTIVE); @@ -152,71 +582,17 @@ static bool is_redundant(private_child_delete_t *this, child_sa_t *child) { if (task->get_type(task) == TASK_CHILD_REKEY) { - child_rekey_t *rekey = (child_rekey_t*)task; - - if (rekey->is_redundant(rekey, child)) - { - tasks->destroy(tasks); - return TRUE; - } + rekey = (child_rekey_t*)task; + collision = rekey->handle_delete(rekey, child, spi); + break; } } tasks->destroy(tasks); - return FALSE; + return collision; } /** - * Install the outbound CHILD_SA with the given SPI - */ -static void install_outbound(private_child_delete_t *this, - protocol_id_t protocol, uint32_t spi) -{ - child_sa_t *child_sa; - linked_list_t *my_ts, *other_ts; - status_t status; - - if (!spi) - { - return; - } - - child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, - spi, FALSE); - if (!child_sa) - { - DBG1(DBG_IKE, "CHILD_SA not found after rekeying"); - return; - } - - status = child_sa->install_outbound(child_sa); - if (status != SUCCESS) - { - DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel"); - charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED, - child_sa); - /* FIXME: delete the new child_sa? */ - return; - } - - my_ts = linked_list_create_from_enumerator( - child_sa->create_ts_enumerator(child_sa, TRUE)); - other_ts = linked_list_create_from_enumerator( - child_sa->create_ts_enumerator(child_sa, FALSE)); - - DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established " - "with SPIs %.8x_i %.8x_o and TS %#R === %#R", - child_sa->get_name(child_sa), - child_sa->get_unique_id(child_sa), - ntohl(child_sa->get_spi(child_sa, TRUE)), - ntohl(child_sa->get_spi(child_sa, FALSE)), - my_ts, other_ts); - - my_ts->destroy(my_ts); - other_ts->destroy(other_ts); -} - -/** - * read in payloads and find the children to delete + * Read payloads and find the children to delete. */ static void process_payloads(private_child_delete_t *this, message_t *message) { @@ -242,8 +618,14 @@ static void process_payloads(private_child_delete_t *this, message_t *message) spis = delete_payload->create_spi_enumerator(delete_payload); while (spis->enumerate(spis, &spi)) { + child_rekey_collision_t collision = CHILD_REKEY_COLLISION_NONE; + child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, spi, FALSE); + if (!this->initiator) + { + collision = possible_rekey_collision(this, child_sa, spi); + } if (!child_sa) { DBG1(DBG_IKE, "received DELETE for unknown %N CHILD_SA with" @@ -258,43 +640,29 @@ static void process_payloads(private_child_delete_t *this, message_t *message) { continue; } - INIT(entry, - .child_sa = child_sa - ); - switch (child_sa->get_state(child_sa)) + else if (this->initiator) { - case CHILD_REKEYED: - entry->rekeyed = TRUE; - break; - case CHILD_DELETED: - /* already deleted but not yet destroyed, ignore */ - case CHILD_DELETING: - /* we don't send back a delete if we already initiated - * a delete ourself */ - if (!this->initiator) - { - free(entry); - continue; - } - break; - case CHILD_REKEYING: - /* we reply as usual, rekeying will fail */ - case CHILD_INSTALLED: - if (!this->initiator) - { - if (is_redundant(this, child_sa)) - { - entry->rekeyed = TRUE; - } - else - { - entry->check_delete_action = TRUE; - } - } - break; - default: - break; + DBG1(DBG_IKE, "ignore DELETE for %N CHILD_SA with SPI " + "%.8x in response, didn't request its deletion", + protocol_id_names, protocol, ntohl(spi)); + continue; } + + INIT(entry, + .child_sa = child_sa, + .orig_state = child_sa->get_state(child_sa), + .collision = collision, + ); + if (entry->orig_state == CHILD_DELETED || + entry->orig_state == CHILD_DELETING) + { + /* we either already deleted but have not yet destroyed the + * SA, which we ignore; or we're actively deleting it, in + * which case we don't send back a DELETE either */ + free(entry); + continue; + } + child_sa->set_state(child_sa, CHILD_DELETING); this->child_sas->insert_last(this->child_sas, entry); } spis->destroy(spis); @@ -303,213 +671,10 @@ static void process_payloads(private_child_delete_t *this, message_t *message) payloads->destroy(payloads); } -/** - * destroy the children listed in this->child_sas, reestablish by policy - */ -static status_t destroy_and_reestablish(private_child_delete_t *this) -{ - child_init_args_t args = {}; - enumerator_t *enumerator; - entry_t *entry; - child_sa_t *child_sa; - child_cfg_t *child_cfg; - protocol_id_t protocol; - uint32_t spi; - action_t action; - status_t status = SUCCESS; - time_t now, expire; - u_int delay; - - now = time_monotonic(NULL); - delay = lib->settings->get_int(lib->settings, "%s.delete_rekeyed_delay", - DELETE_REKEYED_DELAY, lib->ns); - - enumerator = this->child_sas->create_enumerator(this->child_sas); - while (enumerator->enumerate(enumerator, (void**)&entry)) - { - child_sa = entry->child_sa; - child_sa->set_state(child_sa, CHILD_DELETED); - /* signal child down event if we weren't rekeying */ - protocol = child_sa->get_protocol(child_sa); - if (!entry->rekeyed) - { - charon->bus->child_updown(charon->bus, child_sa, FALSE); - } - else - { - /* the following two calls are only relevant as responder/loser of - * rekeyings as the initiator/winner already did this right after - * the rekeying was completed, either way, we delay destroying - * the CHILD_SA, by default, so we can process delayed packets */ - install_outbound(this, protocol, child_sa->get_rekey_spi(child_sa)); - child_sa->remove_outbound(child_sa); - - expire = child_sa->get_lifetime(child_sa, TRUE); - if (delay && (!expire || ((now + delay) < expire))) - { - lib->scheduler->schedule_job(lib->scheduler, - (job_t*)delete_child_sa_job_create_id( - child_sa->get_unique_id(child_sa)), delay); - continue; - } - else if (now < expire) - { /* let it expire naturally */ - continue; - } - /* no delay and no lifetime, destroy it immediately */ - } - spi = child_sa->get_spi(child_sa, TRUE); - child_cfg = child_sa->get_config(child_sa); - child_cfg->get_ref(child_cfg); - args.reqid = child_sa->get_reqid_ref(child_sa); - args.label = child_sa->get_label(child_sa); - if (args.label) - { - args.label = args.label->clone(args.label); - } - action = child_sa->get_close_action(child_sa); - - this->ike_sa->destroy_child_sa(this->ike_sa, protocol, spi); - - if (entry->check_delete_action) - { /* enforce child_cfg policy if deleted passively */ - if (action & ACTION_TRAP) - { - charon->traps->install(charon->traps, - this->ike_sa->get_peer_cfg(this->ike_sa), - child_cfg); - } - if (action & ACTION_START) - { - child_cfg->get_ref(child_cfg); - status = this->ike_sa->initiate(this->ike_sa, child_cfg, &args); - } - } - child_cfg->destroy(child_cfg); - if (args.reqid) - { - charon->kernel->release_reqid(charon->kernel, args.reqid); - } - DESTROY_IF(args.label); - if (status != SUCCESS) - { - break; - } - } - enumerator->destroy(enumerator); - return status; -} - -/** - * send closing signals for all CHILD_SAs over the bus - */ -static void log_children(private_child_delete_t *this) -{ - linked_list_t *my_ts, *other_ts; - enumerator_t *enumerator; - entry_t *entry; - child_sa_t *child_sa; - uint64_t bytes_in, bytes_out; - - enumerator = this->child_sas->create_enumerator(this->child_sas); - while (enumerator->enumerate(enumerator, (void**)&entry)) - { - child_sa = entry->child_sa; - my_ts = linked_list_create_from_enumerator( - child_sa->create_ts_enumerator(child_sa, TRUE)); - other_ts = linked_list_create_from_enumerator( - child_sa->create_ts_enumerator(child_sa, FALSE)); - if (this->expired) - { - DBG0(DBG_IKE, "closing expired CHILD_SA %s{%d} " - "with SPIs %.8x_i %.8x_o and TS %#R === %#R", - child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa), - ntohl(child_sa->get_spi(child_sa, TRUE)), - ntohl(child_sa->get_spi(child_sa, FALSE)), my_ts, other_ts); - } - else - { - child_sa->get_usestats(child_sa, TRUE, NULL, &bytes_in, NULL); - child_sa->get_usestats(child_sa, FALSE, NULL, &bytes_out, NULL); - - DBG0(DBG_IKE, "closing CHILD_SA %s{%d} with SPIs %.8x_i " - "(%llu bytes) %.8x_o (%llu bytes) and TS %#R === %#R", - child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa), - ntohl(child_sa->get_spi(child_sa, TRUE)), bytes_in, - ntohl(child_sa->get_spi(child_sa, FALSE)), bytes_out, - my_ts, other_ts); - } - my_ts->destroy(my_ts); - other_ts->destroy(other_ts); - } - enumerator->destroy(enumerator); -} - -METHOD(task_t, build_i, status_t, - private_child_delete_t *this, message_t *message) -{ - child_sa_t *child_sa; - entry_t *entry; - - child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, - this->spi, TRUE); - if (!child_sa) - { /* check if it is an outbound sa */ - child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, - this->spi, FALSE); - if (!child_sa) - { /* child does not exist anymore */ - return SUCCESS; - } - /* we work only with the inbound SPI */ - this->spi = child_sa->get_spi(child_sa, TRUE); - } - - if (this->expired && child_sa->get_state(child_sa) == CHILD_REKEYED) - { /* the peer was expected to delete this SA, but if we send a DELETE - * we might cause a collision there if the CREATE_CHILD_SA response - * is delayed (the peer wouldn't know if we deleted this SA due to an - * expire or because of a forced delete by the user and might then - * ignore the CREATE_CHILD_SA response once it arrives) */ - child_sa->set_state(child_sa, CHILD_DELETED); - install_outbound(this, this->protocol, - child_sa->get_rekey_spi(child_sa)); - } - - if (child_sa->get_state(child_sa) == CHILD_DELETED) - { /* DELETEs for this CHILD_SA were already exchanged, but it was not yet - * destroyed to allow delayed packets to get processed */ - this->ike_sa->destroy_child_sa(this->ike_sa, this->protocol, this->spi); - message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); - return SUCCESS; - } - - INIT(entry, - .child_sa = child_sa, - .rekeyed = child_sa->get_state(child_sa) == CHILD_REKEYED, - ); - this->child_sas->insert_last(this->child_sas, entry); - log_children(this); - build_payloads(this, message); - - if (!entry->rekeyed && this->expired) - { - child_cfg_t *child_cfg; - - DBG1(DBG_IKE, "scheduling CHILD_SA recreate after hard expire"); - child_cfg = child_sa->get_config(child_sa); - this->ike_sa->queue_task(this->ike_sa, (task_t*) - child_create_create(this->ike_sa, child_cfg->get_ref(child_cfg), - FALSE, NULL, NULL)); - } - return NEED_MORE; -} - METHOD(task_t, process_i, status_t, private_child_delete_t *this, message_t *message) { process_payloads(this, message); - DBG1(DBG_IKE, "CHILD_SA closed"); return destroy_and_reestablish(this); } @@ -525,7 +690,6 @@ METHOD(task_t, build_r, status_t, private_child_delete_t *this, message_t *message) { build_payloads(this, message); - DBG1(DBG_IKE, "CHILD_SA closed"); return destroy_and_reestablish(this); } @@ -535,19 +699,6 @@ METHOD(task_t, get_type, task_type_t, return TASK_CHILD_DELETE; } -METHOD(child_delete_t , get_child, child_sa_t*, - private_child_delete_t *this) -{ - child_sa_t *child_sa = NULL; - entry_t *entry; - - if (this->child_sas->get_first(this->child_sas, (void**)&entry) == SUCCESS) - { - child_sa = entry->child_sa; - } - return child_sa; -} - METHOD(task_t, migrate, void, private_child_delete_t *this, ike_sa_t *ike_sa) { @@ -579,7 +730,6 @@ child_delete_t *child_delete_create(ike_sa_t *ike_sa, protocol_id_t protocol, .migrate = _migrate, .destroy = _destroy, }, - .get_child = _get_child, }, .ike_sa = ike_sa, .child_sas = linked_list_create(), diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.h b/src/libcharon/sa/ikev2/tasks/child_delete.h index ca57ae9cf..6dc2141cb 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.h +++ b/src/libcharon/sa/ikev2/tasks/child_delete.h @@ -1,4 +1,5 @@ /* + * Copyright (C) 2022 Tobias Brunner * Copyright (C) 2007 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -25,8 +26,8 @@ typedef struct child_delete_t child_delete_t; #include -#include #include +#include #include /** @@ -38,13 +39,6 @@ struct child_delete_t { * Implements the task_t interface */ task_t task; - - /** - * Get the CHILD_SA to delete by this task. - * - * @return child_sa - */ - child_sa_t* (*get_child) (child_delete_t *this); }; /** @@ -59,4 +53,35 @@ struct child_delete_t { child_delete_t *child_delete_create(ike_sa_t *ike_sa, protocol_id_t protocol, uint32_t spi, bool expired); +/** + * Destroy the given CHILD_SA and trigger events and configured actions. + * + * @param ike_sa IKE_SA the child_sa belongs to + * @param child_sa CHILD_SA to destroy and potentially reestablish + * @return status of reestablishment + */ +status_t child_delete_destroy_and_reestablish(ike_sa_t *ike_sa, + child_sa_t *child_sa); + +/** + * Destroy the given CHILD_SA and trigger events and force a recreation. + * + * @param ike_sa IKE_SA the child_sa belongs to + * @param child_sa CHILD_SA to destroy and reestablish + * @return status of reestablishment + */ +status_t child_delete_destroy_and_force_reestablish(ike_sa_t *ike_sa, + child_sa_t *child_sa); + +/** + * Destroy the given CHILD_SA with a configured delay, so delayed inbound + * packets can still be processed. + * + * @note The outbound SA should already be uninstalled when calling this. + * + * @param ike_sa IKE_SA the child_sa belongs to + * @param child_sa CHILD_SA to destroy and potentially reestablish + */ +void child_delete_destroy_rekeyed(ike_sa_t *ike_sa, child_sa_t *child_sa); + #endif /** CHILD_DELETE_H_ @}*/ diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 6ffeb52c5..6c73d0671 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -19,6 +19,7 @@ #include "child_rekey.h" #include +#include #include #include #include @@ -79,10 +80,16 @@ struct private_child_rekey_t { child_sa_t *child_sa; /** - * colliding task, may be delete or rekey + * Colliding passive rekey task */ task_t *collision; + /** + * SPIs of SAs the peer deleted and we haven't found while this task was + * active + */ + array_t *deleted_spis; + /** * State flags */ @@ -95,19 +102,33 @@ struct private_child_rekey_t { CHILD_REKEY_FOLLOWUP_KE = (1<<0), /** - * Set if we adopted a completed passive task, otherwise (i.e. for - * multi-KE rekeyings) we just reference it. + * Set if the passive rekey task is completed and we adopted it, + * otherwise (i.e. for multi-KE rekeyings) we just reference it. */ - CHILD_REKEY_ADOPTED_PASSIVE = (1<<1), + CHILD_REKEY_PASSIVE_INSTALLED = (1<<1), /** - * Indicate that the peer destroyed the redundant child from a - * collision. This happens if a peer's delete notification for the - * redundant child gets processed before the active rekey job is - * complete. If so, we must not touch the child created in the collision - * since it points to memory already freed. + * Indicates that the peer sent a DELETE for its own CHILD_SA of a + * collision. In regular rekeyings, this happens if a peer lost and + * the delete for the redundant SA gets processed before the active + * rekey job is complete. It could also mean the peer deleted its + * winning SA. */ - CHILD_REKEY_OTHER_DESTROYED = (1<<2), + CHILD_REKEY_OTHER_DELETED = (1<<2), + + /** + * Indicates that the peer sent a DELETE for the rekeyed/old CHILD_SA. + * This happens if the peer has won the rekey collision, but it might + * also happen if it incorrectly sent one after it replied to our + * CREATE_CHILD_SA request and the DELETE arrived before that response. + */ + CHILD_REKEY_OLD_SA_DELETED = (1<<3), + + /** + * After handling the collision, this indicates whether the peer deleted + * the winning replacement SA (either ours or its own). + */ + CHILD_REKEY_REPLACEMENT_DELETED = (1<<4), } flags; }; @@ -130,58 +151,90 @@ static void schedule_delayed_rekey(private_child_rekey_t *this) lib->scheduler->schedule_job(lib->scheduler, job, retry); } -/** - * Implementation of task_t.build for initiator, after rekeying - */ -static status_t build_i_delete(private_child_rekey_t *this, message_t *message) +METHOD(task_t, build_i_delete, status_t, + private_child_rekey_t *this, message_t *message) { /* update exchange type to INFORMATIONAL for the delete */ message->set_exchange_type(message, INFORMATIONAL); - return this->child_delete->task.build(&this->child_delete->task, message); } -/** - * Implementation of task_t.process for initiator, after rekeying - */ -static status_t process_i_delete(private_child_rekey_t *this, message_t *message) +METHOD(task_t, process_i_delete, status_t, + private_child_rekey_t *this, message_t *message) { return this->child_delete->task.process(&this->child_delete->task, message); } /** - * find a child using the REKEY_SA notify + * In failure cases, we don't use a child_delete task, but handle the deletes + * ourselves for more flexibility (in particular, adding multiple DELETE + * payloads to a single message). */ -static void find_child(private_child_rekey_t *this, message_t *message) +static void build_delete_old_sa(private_child_rekey_t *this, message_t *message) { - notify_payload_t *notify; + delete_payload_t *del; protocol_id_t protocol; uint32_t spi; - child_sa_t *child_sa; - notify = message->get_notify(message, REKEY_SA); - if (notify) + message->set_exchange_type(message, INFORMATIONAL); + + protocol = this->child_sa->get_protocol(this->child_sa); + spi = this->child_sa->get_spi(this->child_sa, TRUE); + + del = delete_payload_create(PLV2_DELETE, protocol); + del->add_spi(del, spi); + message->add_payload(message, (payload_t*)del); + + DBG1(DBG_IKE, "sending DELETE for %N CHILD_SA with SPI %.8x", + protocol_id_names, protocol, ntohl(spi)); +} + +METHOD(task_t, build_i_delete_replacement, status_t, + private_child_rekey_t *this, message_t *message) +{ + /* add the delete for the replacement we failed to create locally but the + * peer probably already has installed */ + this->child_create->task.build(&this->child_create->task, message); + return SUCCESS; +} + +METHOD(task_t, build_i_delete_old_destroy, status_t, + private_child_rekey_t *this, message_t *message) +{ + /* send the delete but then immediately destroy and possibly recreate the + * CHILD_SA as the peer deleted its replacement. treat this like the peer + * sent a delete for the original SA */ + build_delete_old_sa(this, message); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; +} + +/** + * Delete either both or only the replacement SA and then destroy and recreate + * the old SA. + */ +static status_t build_delete_recreate(private_child_rekey_t *this, + message_t *message, bool delete_old) +{ + if (delete_old) { - protocol = notify->get_protocol_id(notify); - spi = notify->get_spi(notify); - - if (protocol == PROTO_ESP || protocol == PROTO_AH) - { - child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, - spi, FALSE); - /* ignore rekeyed/deleted CHILD_SAs we keep around */ - if (child_sa && - child_sa->get_state(child_sa) != CHILD_DELETED) - { - this->child_sa = child_sa; - } - } - if (!this->child_sa) - { - this->protocol = protocol; - this->spi_data = chunk_clone(notify->get_spi_data(notify)); - } + build_delete_old_sa(this, message); } + this->child_create->task.build(&this->child_create->task, message); + child_delete_destroy_and_force_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; +} + +METHOD(task_t, build_i_delete_replacement_recreate, status_t, + private_child_rekey_t *this, message_t *message) +{ + return build_delete_recreate(this, message, FALSE); +} + +METHOD(task_t, build_i_delete_both_recreate, status_t, + private_child_rekey_t *this, message_t *message) +{ + return build_delete_recreate(this, message, TRUE); } METHOD(task_t, build_i, status_t, @@ -266,6 +319,41 @@ METHOD(task_t, build_i, status_t, return NEED_MORE; } +/** + * Find a CHILD_SA using the REKEY_SA notify + */ +static void find_child(private_child_rekey_t *this, message_t *message) +{ + notify_payload_t *notify; + protocol_id_t protocol; + uint32_t spi; + child_sa_t *child_sa; + + notify = message->get_notify(message, REKEY_SA); + if (notify) + { + protocol = notify->get_protocol_id(notify); + spi = notify->get_spi(notify); + + if (protocol == PROTO_ESP || protocol == PROTO_AH) + { + child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, + spi, FALSE); + /* ignore rekeyed/deleted CHILD_SAs we keep around */ + if (child_sa && + child_sa->get_state(child_sa) != CHILD_DELETED) + { + this->child_sa = child_sa; + } + } + if (!this->child_sa) + { + this->protocol = protocol; + this->spi_data = chunk_clone(notify->get_spi_data(notify)); + } + } +} + METHOD(task_t, process_r, status_t, private_child_rekey_t *this, message_t *message) { @@ -318,7 +406,7 @@ METHOD(task_t, build_r, status_t, child_sa_t *child_sa; child_sa_state_t state = CHILD_INSTALLED; uint32_t reqid; - bool followup_sent; + bool followup_sent = FALSE; if (!this->child_sa) { @@ -332,7 +420,9 @@ METHOD(task_t, build_r, status_t, } if (this->child_sa->get_state(this->child_sa) == CHILD_DELETING) { - DBG1(DBG_IKE, "unable to rekey, we are deleting the CHILD_SA"); + DBG1(DBG_IKE, "unable to rekey CHILD_SA %s{%u}, we are deleting it", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } @@ -379,11 +469,11 @@ METHOD(task_t, build_r, status_t, if (child_sa && child_sa->get_state(child_sa) == CHILD_INSTALLED) { this->child_sa->set_state(this->child_sa, CHILD_REKEYED); - this->child_sa->set_rekey_spi(this->child_sa, - child_sa->get_spi(child_sa, FALSE)); - - /* FIXME: this might trigger twice if there was a collision */ - charon->bus->child_rekey(charon->bus, this->child_sa, child_sa); + /* link the SAs to handle possible collisions */ + this->child_sa->set_rekey_sa(this->child_sa, child_sa); + child_sa->set_rekey_sa(child_sa, this->child_sa); + /* like installing the outbound SA, we only trigger the child-rekey + * event once the old SA is deleted */ } else if (this->child_sa->get_state(this->child_sa) == CHILD_REKEYING) { /* rekeying failed, reuse old child */ @@ -392,9 +482,40 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } +/** + * Check if the peer deleted the replacement SA we created while we waited for + * its completion. + */ +static bool is_our_replacement_deleted(private_child_rekey_t *this) +{ + uint32_t spi, peer_spi; + int i; + + if (!this->deleted_spis) + { + return FALSE; + } + + peer_spi = this->child_create->get_other_spi(this->child_create); + if (!peer_spi) + { + return FALSE; + } + + for (i = 0; i < array_count(this->deleted_spis); i++) + { + array_get(this->deleted_spis, i, &spi); + if (spi == peer_spi) + { + return TRUE; + } + } + return FALSE; +} + /** * Remove the passive rekey task that's waiting for IKE_FOLLOWUP_KE requests - * that will never come. + * that will never come if we won the collision. */ static void remove_passive_rekey_task(private_child_rekey_t *this) { @@ -416,42 +537,42 @@ static void remove_passive_rekey_task(private_child_rekey_t *this) } /** - * Handle a rekey collision + * Compare the nonces to determine if we lost the rekey collision. + * The SA with the lowest nonce should be deleted (if already complete), this + * checks if we or the peer created it */ -static child_sa_t *handle_collision(private_child_rekey_t *this, - child_sa_t **to_install, bool multi_ke) +static bool lost_collision(private_child_rekey_t *this) { private_child_rekey_t *other = (private_child_rekey_t*)this->collision; chunk_t this_nonce, other_nonce; - child_sa_t *to_delete, *child_sa; - if (this->collision->get_type(this->collision) == TASK_CHILD_DELETE) - { /* CHILD_DELETE, which we only adopt if it is for the CHILD_SA we are - * ourselves rekeying */ - to_delete = this->child_create->get_child(this->child_create); - if (multi_ke) - { - DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, abort incomplete " - "multi-KE rekeying"); - } - else - { - DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, deleting redundant " - "child %s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); - } - return to_delete; + if (!other) + { + return FALSE; } this_nonce = this->child_create->get_lower_nonce(this->child_create); other_nonce = other->child_create->get_lower_nonce(other->child_create); - /* the SA with the lowest nonce should be deleted (if already complete), - * check if we or the peer created it */ - if (memcmp(this_nonce.ptr, other_nonce.ptr, - min(this_nonce.len, other_nonce.len)) < 0) + return memcmp(this_nonce.ptr, other_nonce.ptr, + min(this_nonce.len, other_nonce.len)) < 0; +} + +/** + * Handle a rekey collision. Returns TRUE if we won the collision or there + * wasn't one. Also returns the SA that should be deleted and the winning SA + * of the collision, if any. + */ +static bool handle_collision(private_child_rekey_t *this, + child_sa_t **to_delete, child_sa_t **winning_sa, + bool multi_ke) +{ + private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + child_sa_t *other_sa; + + if (lost_collision(this)) { - to_delete = this->child_create->get_child(this->child_create); + *to_delete = this->child_create->get_child(this->child_create); if (multi_ke) { DBG1(DBG_IKE, "CHILD_SA rekey collision lost, abort incomplete " @@ -460,38 +581,68 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, else { DBG1(DBG_IKE, "CHILD_SA rekey collision lost, deleting " - "redundant child %s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); + "redundant child %s{%u}", (*to_delete)->get_name(*to_delete), + (*to_delete)->get_unique_id(*to_delete)); } - return to_delete; + /* check if the passive rekeying is completed */ + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) + { + *winning_sa = other->child_create->get_child(other->child_create); + + if (this->flags & CHILD_REKEY_OTHER_DELETED) + { + /* the peer deleted its own replacement SA while we waited + * for a response, set a flag to destroy the SA accordingly */ + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* if the peer has not triggered a rekey event yet by deleting + * its own SA before deleting the old SA (if it did so at all), + * we trigger that now so listeners can track this properly */ + if (!(this->flags & CHILD_REKEY_OLD_SA_DELETED) || + (*winning_sa)->get_outbound_state(*winning_sa) != CHILD_OUTBOUND_INSTALLED) + { + charon->bus->child_rekey(charon->bus, this->child_sa, + *winning_sa); + } + } + /* check if the peer already sent a delete for the old SA */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + else if (this->flags & CHILD_REKEY_OTHER_DELETED) + { + /* make sure the old SA is in the correct state if the peer + * deleted its own SA but not yet the old one (weird, but who + * knows...) */ + this->child_sa->set_state(this->child_sa, CHILD_REKEYED); + } + } + return FALSE; } - *to_install = this->child_create->get_child(this->child_create); - to_delete = this->child_sa; + *winning_sa = this->child_create->get_child(this->child_create); + *to_delete = this->child_sa; + + /* regular rekeying without collision (or we already concluded it for a + * multi-KE rekeying), check if the peer deleted the new SA already */ + if (!this->collision) + { + if (is_our_replacement_deleted(this)) + { + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* since we will destroy the winning SA, we have to trigger a rekey + * event before so listeners can track this properly */ + charon->bus->child_rekey(charon->bus, this->child_sa, *winning_sa); + } + return TRUE; + } /* the passive rekeying is complete only if it was single-KE. otherwise, * the peer would either have stopped before sending IKE_FOLLOWUP_KE when * it noticed it lost, or it responded with TEMPORARY_FAILURE to our * CREATE_CHILD_SA request if it already started sending them. */ - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { - /* we don't want to install the peer's redundant outbound SA */ - this->child_sa->set_rekey_spi(this->child_sa, 0); - /* don't touch child other created if it has already been deleted */ - if (!(this->flags & CHILD_REKEY_OTHER_DESTROYED)) - { - /* disable close action and updown event for redundant child the - * other is expected to delete */ - child_sa = other->child_create->get_child(other->child_create); - if (child_sa) - { - child_sa->set_close_action(child_sa, ACTION_NONE); - if (child_sa->get_state(child_sa) != CHILD_REKEYED) - { - child_sa->set_state(child_sa, CHILD_REKEYED); - } - } - } if (multi_ke) { DBG1(DBG_IKE, "CHILD_SA rekey collision won, continue with " @@ -502,8 +653,51 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, else { DBG1(DBG_IKE, "CHILD_SA rekey collision won, deleting old child " - "%s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); + "%s{%u}", (*to_delete)->get_name(*to_delete), + (*to_delete)->get_unique_id(*to_delete)); + } + + other_sa = other->child_create->get_child(other->child_create); + + /* check if the peer already sent a delete for our winning SA */ + if (is_our_replacement_deleted(this)) + { + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* similar to the case above, but here the peer might already have + * deleted its redundant SA, and it might have sent an incorrect + * delete for the old SA. if it did the latter first, then we will + * have concluded the rekeying and there was a rekey event from the + * old SA to the redundant one that we have to consider here */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED && other_sa && + other_sa->get_outbound_state(other_sa) == CHILD_OUTBOUND_INSTALLED) + { + charon->bus->child_rekey(charon->bus, other_sa, *winning_sa); + } + else + { + charon->bus->child_rekey(charon->bus, this->child_sa, + *winning_sa); + } + } + + /* check if the peer already sent a delete for its redundant SA */ + if (!(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + /* unlink the redundant SA the peer is expected to delete, disable + * events and make sure the outbound SA isn't installed/registered */ + this->child_sa->set_rekey_sa(this->child_sa, NULL); + if (other_sa) + { + other_sa->set_rekey_sa(other_sa, NULL); + other_sa->set_state(other_sa, CHILD_REKEYED); + other_sa->remove_outbound(other_sa); + } + } + else if (other_sa) + { + /* the peer already deleted its redundant SA, but we have not yet + * destroyed it, do so now */ + child_delete_destroy_rekeyed(this->ike_sa, other_sa); } this->collision->destroy(this->collision); } @@ -525,7 +719,181 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, remove_passive_rekey_task(this); } this->collision = NULL; - return to_delete; + return TRUE; +} + +/** + * Check if we can ignore a CHILD_SA_NOT_FOUND notify and log appropriate + * messages. + */ +static bool ignore_child_sa_not_found(private_child_rekey_t *this) +{ + private_child_rekey_t *other; + child_sa_t *other_sa; + + /* if the peer hasn't explicitly sent a delete for the CHILD_SA it wasn't + * able to find now, it might have lost the state, we can't ignore that and + * create a replacement */ + if (!(this->flags & CHILD_REKEY_OLD_SA_DELETED)) + { + DBG1(DBG_IKE, "peer didn't find CHILD_SA %s{%u} we tried to rekey, " + "create a replacement", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return FALSE; + } + + /* if the peer explicitly deleted the original CHILD_SA before our request + * arrived, we adhere to that wish and close the SA. + * this is the case where the peer received the DELETE response before + * our rekey request, see below for the case where it hasn't received the + * response yet and responded with TEMPORARY_FAILURE */ + if (!this->collision) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer deleted it before it received our request", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return TRUE; + } + + /* if there was a rekey collision and the peer deleted the original CHILD_SA + * before our request arrived and it has not deleted the new SA, we just + * abort our own rekeying and use the peer's replacement */ + if (!(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because " + "it was successfully rekeyed by the peer before it received " + "our request", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + return TRUE; + } + + /* the peer successfully rekeyed the same SA, deleted it, but then also + * deleted the CHILD_SA it created as replacement. adhere to that wish and + * close the replacement */ + other = (private_child_rekey_t*)this->collision; + other_sa = other->child_create->get_child(other->child_create); + + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because the other " + "peer already deleted its replacement CHILD_SA %s{%u} before " + "it received our request", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa), + other_sa->get_name(other_sa), other_sa->get_unique_id(other_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + child_delete_destroy_and_reestablish(this->ike_sa, other_sa); + return TRUE; +} + +/** + * Check if we can ignore failures to create the new CHILD_SA e.g. due to an + * error notify like TEMPORARY_FAILURE and log appropriate messages. + */ +static bool ignore_child_sa_failure(private_child_rekey_t *this) +{ + /* we are fine if there was a successful passive rekeying. the peer might + * not have detected the collision and responded with a TEMPORARY_FAILURE + * notify while deleting the old SA, which conflicted with our request */ + if (this->collision && (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) && + !(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because " + "the peer successfully rekeyed it before receiving our request%s", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa), + this->flags & CHILD_REKEY_OLD_SA_DELETED ? "" + : ", waiting for delete"); + + /* if the peer already deleted the rekeyed SA, destroy it, otherwise + * just wait for the delete */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + return TRUE; + } + + /* if the peer initiated a delete for the old SA before our rekey request + * reached it, the expected response is TEMPORARY_FAILURE. adhere to that + * wish and abort the rekeying. + * this is the case where the peer has not yet received the DELETE response + * when our rekey request arrived, see above for the case where it has + * already received the response and responded with CHILD_SA_NOT_FOUND */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer started to delete it before receiving our request", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return TRUE; + } + return FALSE; +} + +/** + * Check if we can ignore local failures to create the new CHILD_SA e.g. due to + * a KE or kernel problem and log an appropriate message. + */ +static status_t handle_local_failure(private_child_rekey_t *this) +{ + /* if we lost the collision, we are expected to delete the failed SA + * anyway, so just do that and rely on the passive rekeying, which + * deletes the old SA (or has already done so, in which case we destroy the + * SA now) */ + if (this->collision && lost_collision(this)) + { + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + this->public.task.build = _build_i_delete_replacement; + return NEED_MORE; + } + + /* the peer sent a delete for our winning replacement SA, no need to send a + * delete for it again and adhere to this wish to delete the SA. + * however, we are expected to send a delete for the original SA, unless, + * it was already deleted by the peer as well (which would be incorrect) */ + if (is_our_replacement_deleted(this)) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer meanwhile sent a delete for its replacement", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; + } + this->public.task.build = _build_i_delete_old_destroy; + return NEED_MORE; + } + + /* as the winner of the collision or if there wasn't one, we're expected to + * delete the original SA, but we also want to recreate it because we + * failed to install the replacement. because the peer already has the + * replacement partially installed, we also need to send a delete for the + * failed one */ + this->public.task.build = _build_i_delete_both_recreate; + + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + /* the peer already sent an incorrect delete for the original SA that + * arrived before the response to the rekeying, delete only the failed + * replacement and recreate the SA */ + DBG1(DBG_IKE, "peer sent an incorrect delete for CHILD_SA %s{%u} after " + "responding to our rekeying", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + this->public.task.build = _build_i_delete_replacement_recreate; + } + DBG1(DBG_IKE, "closing and recreating CHILD_SA %s{%u} after failing to " + "install replacement", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return NEED_MORE; } METHOD(task_t, process_i, status_t, @@ -533,7 +901,8 @@ METHOD(task_t, process_i, status_t, { protocol_id_t protocol; uint32_t spi; - child_sa_t *child_sa, *to_delete = NULL, *to_install = NULL; + child_sa_t *child_sa, *to_delete = NULL, *winning_sa = NULL; + bool collision_won; if (message->get_notify(message, NO_ADDITIONAL_SAS)) { @@ -547,74 +916,52 @@ METHOD(task_t, process_i, status_t, } if (message->get_notify(message, CHILD_SA_NOT_FOUND)) { - child_cfg_t *child_cfg; - child_init_args_t args = {}; - status_t status; - - if (this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_DELETE) - { /* ignore this error if we already deleted the CHILD_SA on the - * peer's behalf (could happen if the other peer does not detect - * the collision and did not respond with TEMPORARY_FAILURE) */ + /* ignore CHILD_SA_NOT_FOUND error notify in some cases, otherwise + * create a replacement SA */ + if (ignore_child_sa_not_found(this)) + { return SUCCESS; } - DBG1(DBG_IKE, "peer didn't find the CHILD_SA we tried to rekey"); - /* FIXME: according to RFC 7296 we should only create a new CHILD_SA if - * it does not exist yet, we currently have no good way of checking for - * that (we could go by name, but that might be tricky e.g. due to - * narrowing) */ - spi = this->child_sa->get_spi(this->child_sa, TRUE); - protocol = this->child_sa->get_protocol(this->child_sa); - child_cfg = this->child_sa->get_config(this->child_sa); - child_cfg->get_ref(child_cfg); - args.reqid = this->child_sa->get_reqid_ref(this->child_sa); - args.label = this->child_sa->get_label(this->child_sa); - if (args.label) - { - args.label = args.label->clone(args.label); - } - charon->bus->child_updown(charon->bus, this->child_sa, FALSE); - this->ike_sa->destroy_child_sa(this->ike_sa, protocol, spi); - status = this->ike_sa->initiate(this->ike_sa, - child_cfg->get_ref(child_cfg), &args); - if (args.reqid) - { - charon->kernel->release_reqid(charon->kernel, args.reqid); - } - DESTROY_IF(args.label); - return status; + return child_delete_destroy_and_force_reestablish(this->ike_sa, + this->child_sa); } if (this->child_create->task.process(&this->child_create->task, message) == NEED_MORE) { - if (message->get_notify(message, INVALID_KE_PAYLOAD) || - !this->child_create->get_child(this->child_create)) - { /* bad key exchange mechanism, retry, or failure requiring delete */ - return NEED_MORE; + if (message->get_notify(message, INVALID_KE_PAYLOAD)) + { + /* invalid KE method => retry, unless we can ignore it */ + return ignore_child_sa_failure(this) ? SUCCESS : NEED_MORE; } + else if (!this->child_create->get_child(this->child_create)) + { + /* local failure requiring a delete, check what we have to do */ + return handle_local_failure(this); + } + /* multiple key exchanges */ this->flags |= CHILD_REKEY_FOLLOWUP_KE; /* there will only be a collision while we process a CREATE_CHILD_SA - * response, later we just respond with TEMPORARY_FAILURE and ignore - * the passive task - if we lost, the returned SA is the one we created - * in this task, since it's not complete yet, we abort the task */ - if (this->collision) + * response, later we just respond with TEMPORARY_FAILURE, so handle + * it now */ + if (!handle_collision(this, &to_delete, &winning_sa, TRUE)) { - to_delete = handle_collision(this, &to_install, TRUE); + /* we lost the collision. since the SA is not complete yet, we just + * abort the task */ + return SUCCESS; } - return (to_delete && to_delete != this->child_sa) ? SUCCESS : NEED_MORE; + return NEED_MORE; } child_sa = this->child_create->get_child(this->child_create); if (!child_sa || child_sa->get_state(child_sa) != CHILD_INSTALLED) { - /* establishing new child failed, reuse old and try again. but not when - * we received a delete in the meantime or passively rekeyed the SA */ - if (!this->collision || - (this->collision->get_type(this->collision) != TASK_CHILD_DELETE && - !(this->flags & CHILD_REKEY_ADOPTED_PASSIVE))) + /* check if we can ignore remote errors like TEMPORARY_FAILURE */ + if (!ignore_child_sa_failure(this)) { + /* otherwise (e.g. for an IKE/CHILD rekey collision), reuse the old + * CHILD_SA and try again */ schedule_delayed_rekey(this); } return SUCCESS; @@ -622,143 +969,195 @@ METHOD(task_t, process_i, status_t, /* there won't be a collision if this task is for a multi-KE rekeying, as a * collision during CREATE_CHILD_SA was cleaned up above */ - if (this->collision) - { - to_delete = handle_collision(this, &to_install, FALSE); - } - else - { - to_install = this->child_create->get_child(this->child_create); - to_delete = this->child_sa; - } - if (to_install) - { - if (to_install->install_outbound(to_install) != SUCCESS) - { - DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel"); - charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED, - to_install); - /* FIXME: delete the child_sa? fail the task? */ - } - else - { - linked_list_t *my_ts, *other_ts; + collision_won = handle_collision(this, &to_delete, &winning_sa, FALSE); - my_ts = linked_list_create_from_enumerator( - to_install->create_ts_enumerator(to_install, TRUE)); - other_ts = linked_list_create_from_enumerator( - to_install->create_ts_enumerator(to_install, FALSE)); + if (this->flags & CHILD_REKEY_REPLACEMENT_DELETED) + { + DBG1(DBG_IKE, "peer meanwhile sent a delete for CHILD_SA %s{%u} with " + "SPIs %.8x_i %.8x_o, abort rekeying", + winning_sa->get_name(winning_sa), + winning_sa->get_unique_id(winning_sa), + ntohl(winning_sa->get_spi(winning_sa, TRUE)), + ntohl(winning_sa->get_spi(winning_sa, FALSE))); + child_delete_destroy_and_reestablish(this->ike_sa, winning_sa); + } + else if (collision_won) + { + /* only conclude the rekeying here if we won, otherwise, we either + * already concluded the rekeying or we will do so when the peer deletes + * the old SA */ + child_rekey_conclude_rekeying(this->child_sa, winning_sa); + } - DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established " - "with SPIs %.8x_i %.8x_o and TS %#R === %#R", - to_install->get_name(to_install), - to_install->get_unique_id(to_install), - ntohl(to_install->get_spi(to_install, TRUE)), - ntohl(to_install->get_spi(to_install, FALSE)), - my_ts, other_ts); + if (collision_won && + this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + /* the peer already deleted the rekeyed SA we were expected to delete + * with an incorrect delete to which we responded as usual but didn't + * destroy the SA yet */ + DBG1(DBG_IKE, "peer sent an incorrect delete for CHILD_SA %s{%u} after " + "responding to our rekeying", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + return SUCCESS; + } - my_ts->destroy(my_ts); - other_ts->destroy(other_ts); - } - } - if (to_delete->get_state(to_delete) != CHILD_REKEYED) - { /* disable updown event for old/redundant CHILD_SA */ - to_delete->set_state(to_delete, CHILD_REKEYED); - } - if (to_delete == this->child_sa) - { /* invoke rekey hook if rekeying successful and remove the old - * outbound SA as we installed the new one already above, but might not - * be using it yet depending on how SAs/policies are handled */ - this->child_sa->remove_outbound(this->child_sa); - charon->bus->child_rekey(charon->bus, this->child_sa, - this->child_create->get_child(this->child_create)); + /* disable updown event for old/redundant CHILD_SA */ + to_delete->set_state(to_delete, CHILD_REKEYED); + /* and make sure the outbound SA is not registered, unless it is still fully + * installed, which happens if the rekeying is aborted. we keep it installed + * as we can't establish a replacement until the delete is done */ + if (to_delete->get_outbound_state(to_delete) != CHILD_OUTBOUND_INSTALLED) + { + to_delete->remove_outbound(to_delete); } + spi = to_delete->get_spi(to_delete, TRUE); protocol = to_delete->get_protocol(to_delete); /* rekeying done, delete the obsolete CHILD_SA using a subtask */ this->child_delete = child_delete_create(this->ike_sa, protocol, spi, FALSE); - this->public.task.build = (status_t(*)(task_t*,message_t*))build_i_delete; - this->public.task.process = (status_t(*)(task_t*,message_t*))process_i_delete; + this->public.task.build = _build_i_delete; + this->public.task.process = _process_i_delete; return NEED_MORE; } +/* + * Described in header + */ +bool child_rekey_conclude_rekeying(child_sa_t *old, child_sa_t *new) +{ + linked_list_t *my_ts, *other_ts; + + if (new->install_outbound(new) != SUCCESS) + { + /* shouldn't happen after we were able to install the inbound SA */ + DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel"); + charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED, + new); + return FALSE; + } + + my_ts = linked_list_create_from_enumerator( + new->create_ts_enumerator(new, TRUE)); + other_ts = linked_list_create_from_enumerator( + new->create_ts_enumerator(new, FALSE)); + + DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established " + "with SPIs %.8x_i %.8x_o and TS %#R === %#R", + new->get_name(new), new->get_unique_id(new), + ntohl(new->get_spi(new, TRUE)), ntohl(new->get_spi(new, FALSE)), + my_ts, other_ts); + + my_ts->destroy(my_ts); + other_ts->destroy(other_ts); + + /* remove the old outbound SA after we installed the new one. otherwise, it + * might not get used yet depending on how SAs/policies are handled in the + * kernel */ + old->remove_outbound(old); + + DBG0(DBG_IKE, "rekeyed CHILD_SA %s{%u} with SPIs %.8x_i %.8x_o with " + "%s{%u} with SPIs %.8x_i %.8x_o", + old->get_name(old), old->get_unique_id(old), + ntohl(old->get_spi(old, TRUE)), ntohl(old->get_spi(old, FALSE)), + new->get_name(new), new->get_unique_id(new), + ntohl(new->get_spi(new, TRUE)), ntohl(new->get_spi(new, FALSE))); + charon->bus->child_rekey(charon->bus, old, new); + return TRUE; +} + METHOD(task_t, get_type, task_type_t, private_child_rekey_t *this) { return TASK_CHILD_REKEY; } -METHOD(child_rekey_t, is_redundant, bool, - private_child_rekey_t *this, child_sa_t *child) +METHOD(child_rekey_t, handle_delete, child_rekey_collision_t, + private_child_rekey_t *this, child_sa_t *child, uint32_t spi) { - if (this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_REKEY) + /* if we already completed our active rekeying and are deleting the + * old/redundant SA, there is no need to do anything special */ + if (this->child_delete) { - private_child_rekey_t *rekey = (private_child_rekey_t*)this->collision; - return child == rekey->child_create->get_child(rekey->child_create); + return CHILD_REKEY_COLLISION_NONE; } - return FALSE; + + if (!child) + { + /* check later if the SPI is the peer's of the SA we created (i.e. + * whether it deleted the new SA immediately after creation and we + * received that request before our active rekeying was complete) */ + array_insert_create_value(&this->deleted_spis, sizeof(uint32_t), + ARRAY_TAIL, &spi); + } + else if (child == this->child_sa) + { + /* the peer sent a delete for the old SA, might be because it won a + * collision, but could also be either because it initiated that before + * it received our CREATE_CHILD_SA request, or it incorrectly sent one + * as response to our request, we will check once we have the response + * to our rekeying */ + this->flags |= CHILD_REKEY_OLD_SA_DELETED; + return CHILD_REKEY_COLLISION_OLD; + } + else if (this->collision) + { + private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + + if (child == other->child_create->get_child(other->child_create)) + { + /* the peer deleted the redundant (or in rare cases the winning) SA + * it created before our active rekeying was complete, how we handle + * this depends on the response to our rekeying */ + this->flags |= CHILD_REKEY_OTHER_DELETED; + return CHILD_REKEY_COLLISION_PEER; + } + } + return CHILD_REKEY_COLLISION_NONE; } METHOD(child_rekey_t, collide, bool, private_child_rekey_t *this, task_t *other) { - /* the task manager only detects exchange collision, but not if - * the collision is for the same child. we check it here. */ - if (other->get_type(other) == TASK_CHILD_REKEY) - { - private_child_rekey_t *rekey = (private_child_rekey_t*)other; - child_sa_t *other_child; + private_child_rekey_t *rekey = (private_child_rekey_t*)other; + child_sa_t *other_child; - if (rekey->child_sa != this->child_sa) - { /* not the same child => no collision */ - return FALSE; - } - /* ignore passive tasks that did not successfully create a CHILD_SA */ - other_child = rekey->child_create->get_child(rekey->child_create); - if (!other_child) - { - return FALSE; - } - if (other_child->get_state(other_child) != CHILD_INSTALLED) - { - DBG1(DBG_IKE, "colliding passive rekeying is not yet complete", - task_type_names, TASK_CHILD_REKEY); - /* we do reference the task to check its state later */ - this->collision = other; - return FALSE; - } - } - else if (other->get_type(other) == TASK_CHILD_DELETE) + if (rekey->child_sa != this->child_sa) { - child_delete_t *del = (child_delete_t*)other; - if (is_redundant(this, del->get_child(del))) - { - this->flags |= CHILD_REKEY_OTHER_DESTROYED; - return FALSE; - } - if (del->get_child(del) != this->child_sa) - { - /* not the same child => no collision */ - return FALSE; - } - } - else - { - /* shouldn't happen */ + /* not the same child => no collision */ return FALSE; } - DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, - TASK_CHILD_REKEY, task_type_names, other->get_type(other)); - - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + other_child = rekey->child_create->get_child(rekey->child_create); + if (!other_child) { - DESTROY_IF(this->collision); + /* ignore passive tasks that did not successfully create a CHILD_SA */ + return FALSE; } - this->flags |= CHILD_REKEY_ADOPTED_PASSIVE; + if (other_child->get_state(other_child) != CHILD_INSTALLED) + { + DBG1(DBG_IKE, "colliding passive rekeying for CHILD_SA %s{%u} is not " + "yet complete", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + /* we do reference the task to check its state later */ + this->collision = other; + return FALSE; + } + if (this->collision && this->collision != other) + { + DBG1(DBG_IKE, "duplicate rekey collision for CHILD_SA %s{%u}???", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return FALSE; + } + /* once the passive rekeying is complete, we adopt the task */ + DBG1(DBG_IKE, "detected rekey collision for CHILD_SA %s{%u}", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + this->flags |= CHILD_REKEY_PASSIVE_INSTALLED; this->collision = other; return TRUE; } @@ -775,13 +1174,15 @@ METHOD(task_t, migrate, void, { this->child_create->task.migrate(&this->child_create->task, ike_sa); } - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { DESTROY_IF(this->collision); } + array_destroy(this->deleted_spis); this->ike_sa = ike_sa; this->collision = NULL; + this->flags = 0; } METHOD(task_t, destroy, void, @@ -795,10 +1196,11 @@ METHOD(task_t, destroy, void, { this->child_delete->task.destroy(&this->child_delete->task); } - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { DESTROY_IF(this->collision); } + array_destroy(this->deleted_spis); chunk_free(&this->spi_data); free(this); } @@ -818,7 +1220,7 @@ child_rekey_t *child_rekey_create(ike_sa_t *ike_sa, protocol_id_t protocol, .migrate = _migrate, .destroy = _destroy, }, - .is_redundant = _is_redundant, + .handle_delete = _handle_delete, .collide = _collide, }, .ike_sa = ike_sa, diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.h b/src/libcharon/sa/ikev2/tasks/child_rekey.h index 849df3a2b..a8daed743 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.h +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2020 Tobias Brunner + * Copyright (C) 2016-2022 Tobias Brunner * Copyright (C) 2007 Martin Willi * * Copyright (C) secunet Security Networks AG @@ -24,12 +24,25 @@ #define CHILD_REKEY_H_ typedef struct child_rekey_t child_rekey_t; +typedef enum child_rekey_collision_t child_rekey_collision_t; #include #include #include #include +/** + * Type of collision an active rekey task may have with an inbound DELETE. + */ +enum child_rekey_collision_t { + /** Unrelated SA or unknown SPI (might be for the SA this task creates) */ + CHILD_REKEY_COLLISION_NONE = 0, + /** Deleted SA is the one created by the peer in a collision */ + CHILD_REKEY_COLLISION_PEER, + /** Deleted SA is the SA the active task is rekeying */ + CHILD_REKEY_COLLISION_OLD, +}; + /** * Task of type TASK_CHILD_REKEY, rekey an established CHILD_SA. */ @@ -41,21 +54,27 @@ struct child_rekey_t { task_t task; /** - * Check if the given SA is the redundant CHILD_SA created during a rekey - * collision. + * Handle a DELETE for the given CHILD_SA/SPI that might be related to + * this rekeiyng if the CREATE_CHILD_SA response is delayed. * - * This is called if the other peer deletes the redundant SA before we were - * able to handle the CREATE_CHILD_SA response. + * This checks if the given SA is the CHILD_SA created by the peer during a + * rekey collision or if it's for the old SA. * - * @param child CHILD_SA to check - * @return TRUE if the SA is the redundant CHILD_SA + * If child is NULL, the SPI is collected as it might be for the SA this + * task is actively creating (the peer sends the inbound SA we don't know + * yet). + * + * @param child CHILD_SA to check + * @param spi SPI in case child is not known + * @return type of collision */ - bool (*is_redundant)(child_rekey_t *this, child_sa_t *child); + child_rekey_collision_t (*handle_delete)(child_rekey_t *this, + child_sa_t *child, uint32_t spi); /** - * Register a rekeying/delete task which collides with this one + * Register a rekey task which collides with this one. * - * If two peers initiate rekeying at the same time, the collision must + * If two peers initiate rekeyings at the same time, the collision must * be handled gracefully. The task manager is aware of what exchanges * are going on and notifies the active task by passing the passive. * @@ -77,4 +96,16 @@ struct child_rekey_t { child_rekey_t *child_rekey_create(ike_sa_t *ike_sa, protocol_id_t protocol, uint32_t spi); +/** + * Conclude the rekeying for the given CHILD_SAs by installing the outbound + * SA for the new CHILD_SA, uninstalling the one for the old and triggering + * an appropriate log message and event. + * + * @param old the old CHILD_SA + * @param new the new CHILD_SA + * @return TRUE if new outbound SA installed successfully + */ + +bool child_rekey_conclude_rekeying(child_sa_t *old, child_sa_t *new); + #endif /** CHILD_REKEY_H_ @}*/ diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 8305fa200..4b10e9205 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Tobias Brunner + * Copyright (C) 2016-2022 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -17,6 +17,7 @@ #include "test_suite.h" #include +#include #include #include #include @@ -76,7 +77,7 @@ START_TEST(test_regular) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + 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_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -85,7 +86,7 @@ START_TEST(test_regular) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 3); 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); @@ -94,7 +95,7 @@ START_TEST(test_regular) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -186,7 +187,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -196,7 +197,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 3); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -206,7 +207,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -297,7 +298,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + 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_REKEYED); @@ -306,7 +307,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 4); 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); @@ -315,7 +316,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 5); 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); @@ -351,7 +352,7 @@ START_TEST(test_regular_ke_invalid) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + 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, 5, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -369,7 +370,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_called(child_rekey); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -473,7 +474,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -483,7 +484,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 4); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -493,7 +494,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 5); 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); @@ -546,7 +547,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -566,7 +567,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_called(child_rekey); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -618,7 +619,7 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + 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, 2, CHILD_REKEYED); @@ -627,7 +628,7 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -635,14 +636,13 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_ipsec_sas_installed(a, 1, 3, 4); assert_hook(); - /* we don't expect this to get called anymore */ - assert_hook_not_called(child_rekey); /* this should not produce a message, if it does there won't be a delete * payload below */ call_ikesa(b, rekey_child_sa, PROTO_ESP, 2); assert_child_sa_state(b, 2, CHILD_REKEYED); /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -651,6 +651,10 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 3, 4); assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); @@ -696,7 +700,7 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + 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, 2, CHILD_REKEYED); @@ -705,7 +709,7 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -713,17 +717,19 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_ipsec_sas_installed(a, 1, 3, 4); assert_hook(); - /* we don't expect this to get called anymore */ - assert_hook_not_called(child_rekey); /* this is similar to a regular delete collision, but we don't actually - * want to send a delete back as that might conflict with a delayed - * CREATE_CHILD_SA response */ + * want to send a delete as that might conflict with a delayed + * CREATE_CHILD_SA response and the peer is expected to delete it anyway */ + assert_hook_rekey(child_rekey, 2, 4); call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); assert_child_sa_count(b, 1); assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); /* the expire causes the outbound SA to get installed */ assert_ipsec_sas_installed(b, 3, 4); + assert_hook(); + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_no_jobs_scheduled(); assert_single_payload(IN, PLV2_DELETE); @@ -755,6 +761,305 @@ START_TEST(test_regular_responder_handle_hard_expire) } END_TEST +/** + * Check that the responder and initiator handle deletes for the new SA + * properly while waiting for the delete after a rekeying (e.g. if a script or + * user deletes the new, not fully installed, SA manually). + */ +START_TEST(test_regular_responder_delete) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* this should not get called until the new SA is deleted */ + assert_hook_not_called(child_updown); + + /* 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, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_rekey(child_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 3, 4); + assert_hook(); + + assert_hook_rekey(child_rekey, 2, 4); + call_ikesa(b, delete_child_sa, PROTO_ESP, 4, FALSE); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + /* the delete causes the outbound SA to get installed/uninstalled */ + assert_ipsec_sas_installed(b, 2, 3, 4); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 3, 4); + assert_scheduler(); + assert_hook(); + + /* child_updown */ + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_updown); + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_updown); + + /* simulate the execution of the scheduled job */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, 2); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * This simulates what happens if the responder for some reason lost the + * CHILD_SA the initiator is trying to rekey. + */ +START_TEST(test_regular_responder_lost_sa) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* destroy the CHILD_SA on the responder without notification */ + call_ikesa(b, destroy_child_sa, PROTO_ESP, 2); + assert_child_sa_count(b, 0); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_notify(IN, REKEY_SA); + assert_single_notify(OUT, CHILD_SA_NOT_FOUND); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_updown(child_updown, TRUE); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 4, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } */ + assert_hook_updown(child_updown, TRUE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 4, 5); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Helper to add DELETE payload. + */ +typedef struct { + listener_t listener; + uint32_t spi; +} incorrect_delete_listener_t; + +/** + * Add a DELETE payload to a message. + */ +static bool add_delete(incorrect_delete_listener_t *listener, ike_sa_t *ike_sa, + message_t *message, bool incoming, bool plain) +{ + delete_payload_t *payload; + + if (plain && !incoming && message->get_request(message)) + { + payload = delete_payload_create(PLV2_DELETE, PROTO_ESP); + payload->add_spi(payload, listener->spi); + message->add_payload(message, (payload_t*)payload); + return FALSE; + } + return TRUE; +} + +/** + * Send a DELETE for the given SPI from an SA. + */ +static void send_child_delete(ike_sa_t *sa, uint32_t spi) +{ + incorrect_delete_listener_t del = { + .listener = { .message = (void*)add_delete, }, + .spi = spi, + }; + + exchange_test_helper->add_listener(exchange_test_helper, &del.listener); + call_ikesa(sa, send_dpd); +} + +/** + * This simulates incorrect behavior by some IKEv2 responders, which send + * a delete for the old CHILD_SA even if there was no collision (don't know + * how they'd behave if there was a collision, maybe they'd send two). + * This is an issue if the DELETE arrives before the CREATE_CHILD_SA response. + */ +START_TEST(test_regular_responder_incorrect_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* 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, 2, CHILD_REKEYED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4); + assert_hook(); + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* inject an incorrect delete for the old CHILD_SA by the responder, + * without messing with its internal state */ + send_child_delete(b, 2); + + /* <-- INFORMATIONAL { D } (incorrect behavior!) */ + 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, 1, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 3, 4); + assert_hook(); + assert_scheduler(); + + /* INFORMATIONAL { D } (response to incorrect DELETE) --> + * this is ignored here because the DPD task doesn't handle the DELETE, so + * we simulate handling of the delete via expire, does not delay destroy */ + assert_no_jobs_scheduled(); + assert_hook_rekey(child_rekey, 2, 4); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 3, 4); + assert_hook(); + assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled job */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 3, 4); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 3, 4); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * Both peers initiate the CHILD_SA rekeying concurrently and should handle * the collision properly depending on the nonces. @@ -804,7 +1109,7 @@ START_TEST(test_collision) /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -812,7 +1117,7 @@ START_TEST(test_collision) assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -821,17 +1126,14 @@ START_TEST(test_collision) /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ if (data[_i].spi_del_a == 1) - { /* currently we call this again if we keep our own replacement as we - * already called it above */ + { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_hook(); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(a, 1, 3, 5, 6); } else @@ -843,10 +1145,10 @@ START_TEST(test_collision) CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 3, 6); } + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ if (data[_i].spi_del_b == 2) { @@ -854,11 +1156,9 @@ START_TEST(test_collision) exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(b, 2, 4, 5, 6); } else @@ -870,19 +1170,28 @@ START_TEST(test_collision) CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(b, 1, 2, 4, 5); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - data[_i].spi_del_b == 2 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, @@ -896,13 +1205,23 @@ START_TEST(test_collision) { assert_ipsec_sas_installed(b, 2, 3, 4, 5); } - assert_scheduler(); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + if (data[_i].spi_del_a == 1) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - data[_i].spi_del_a == 1 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, @@ -916,7 +1235,10 @@ START_TEST(test_collision) { assert_ipsec_sas_installed(a, 1, 3, 4, 6); } - assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1051,7 +1373,7 @@ START_TEST(test_collision_multi_ke) if (data[_i].spi_del_a == 1) { /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -1073,7 +1395,7 @@ START_TEST(test_collision_multi_ke) else { /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1093,12 +1415,10 @@ START_TEST(test_collision_multi_ke) assert_hook(); } - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (data[_i].spi_del_a == 1) { /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 5); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1106,8 +1426,10 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 3, 5); assert_scheduler(); + assert_hook(); /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1115,14 +1437,12 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 3, 5); assert_scheduler(); - - /* simulate the execution of the scheduled jobs */ - destroy_rekeyed(a, data[_i].spi_del_a); - destroy_rekeyed(b, data[_i].spi_del_a); + assert_hook(); } else { /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 6); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1130,8 +1450,10 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 6); assert_scheduler(); + assert_hook(); /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1139,12 +1461,18 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 4, 6); assert_scheduler(); - - /* simulate the execution of the scheduled jobs */ - destroy_rekeyed(a, data[_i].spi_del_b); - destroy_rekeyed(b, data[_i].spi_del_b); + assert_hook(); } + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a == 1 ? data[_i].spi_del_a + : data[_i].spi_del_b); + destroy_rekeyed(b, data[_i].spi_del_a == 1 ? data[_i].spi_del_a + : data[_i].spi_del_b); + assert_child_sa_count(a, 1); assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); assert_child_sa_count(b, 1); @@ -1231,7 +1559,7 @@ START_TEST(test_collision_mixed) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1239,25 +1567,30 @@ START_TEST(test_collision_mixed) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ - assert_hook_not_called(child_rekey); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - /* the single-KE passive task was completed and adopted above */ - assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); if (data[_i].spi_del_a == 1) - { + { /* a's multi-KE SA is the winner */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* the single-KE passive task was completed and adopted */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, 6, CHILD_REKEYED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(a, 6, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_hook(); } else - { + { /* b's single-KE SA is the winner */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* the single-KE passive task was completed and adopted */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_hook(); } assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 2, 6); - assert_hook(); if (data[_i].spi_del_a == 1) { @@ -1267,24 +1600,25 @@ START_TEST(test_collision_mixed) assert_num_tasks(b, 1, TASK_QUEUE_PASSIVE); assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 1, 2, 4); assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 1, 2, 4, 5); assert_hook(); /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); @@ -1292,10 +1626,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 2, 6); assert_scheduler(); + assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - /* currently we call this again if we keep our own replacement as we - * already called it above */ assert_hook_rekey(child_rekey, 1, data[_i].spi_a); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); @@ -1320,12 +1653,10 @@ START_TEST(test_collision_mixed) assert_hook(); } - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (data[_i].spi_del_a == 1) { /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -1334,8 +1665,11 @@ START_TEST(test_collision_mixed) assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 1, 2, 4, 5); assert_scheduler(); + assert_hook(); + /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 5); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1344,6 +1678,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 2, 4, 3, 5); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); @@ -1360,10 +1697,13 @@ START_TEST(test_collision_mixed) destroy_rekeyed(a, data[_i].spi_del_b); destroy_rekeyed(b, data[_i].spi_del_a); destroy_rekeyed(b, data[_i].spi_del_b); + + assert_hook(); } else { /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 6); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1371,6 +1711,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 6); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1384,8 +1727,13 @@ START_TEST(test_collision_mixed) /* simulate the execution of the scheduled jobs */ destroy_rekeyed(a, data[_i].spi_del_b); destroy_rekeyed(b, data[_i].spi_del_b); + + assert_hook(); } + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + assert_child_sa_count(a, 1); assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); assert_child_sa_count(b, 1); @@ -1458,7 +1806,7 @@ START_TEST(test_collision_delayed_response) /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1466,7 +1814,7 @@ START_TEST(test_collision_delayed_response) assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1481,56 +1829,59 @@ START_TEST(test_collision_delayed_response) { assert_hook_rekey(child_rekey, 2, data[_i].spi_b); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(b, 2, 4, 5, 6); + assert_hook(); } else { assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_hook(); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); /* <-- INFORMATIONAL { D } */ - assert_hook_not_called(child_rekey); - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_no_jobs_scheduled(); if (data[_i].spi_del_b == 2) { + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); + assert_hook(); } else { - assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, - CHILD_OUTBOUND_NONE); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); } - assert_child_sa_count(a, 2); assert_scheduler(); + assert_child_sa_count(a, 2); /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); if (data[_i].spi_del_b == 2) { assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(b, 2, 4, 5, 6); @@ -1550,35 +1901,45 @@ START_TEST(test_collision_delayed_response) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + /* the second job here is for the retransmit of the delete */ + assert_jobs_scheduled(2); if (data[_i].spi_del_a == 1) { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_hook(); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); - assert_ipsec_sas_installed(a, 1, 3, 5, 6); } else { assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_hook(); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); - assert_ipsec_sas_installed(a, 1, 3, 4, 6); } + assert_scheduler(); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 3); + assert_ipsec_sas_installed(a, 1, 3, 6, + data[_i].spi_del_a == 1 ? 5 : 4); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, @@ -1589,6 +1950,9 @@ START_TEST(test_collision_delayed_response) data[_i].spi_del_b == 2 ? 6 : 3); assert_child_sa_count(b, 3); assert_scheduler(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1598,9 +1962,9 @@ START_TEST(test_collision_delayed_response) CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 3); assert_ipsec_sas_installed(a, 1, 3, 6, data[_i].spi_del_a == 1 ? 5 : 4); + assert_child_sa_count(a, 3); assert_scheduler(); /* simulate the execution of the scheduled jobs */ @@ -1622,6 +1986,284 @@ START_TEST(test_collision_delayed_response) } END_TEST +/** + * This is like the rekey collision above, but one peer deletes the + * redundant/old SA and then also the new one before the other peer receives + * the CREATE_CHILD_SA response: + * + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * ----\ / + * \----/-----> + * handle delete <--------/------- delete old SA + * --------/-------> + * handle delete <------/--------- delete new SA + * ------/---------> + * ignore rekey <----/ + */ +START_TEST(test_collision_delayed_response_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 4, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 4, 3, 5 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should not get called until the replacement SA is deleted */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 5); + assert_hook(); + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + if (data[_i].spi_del_b == 2) + { + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5, 6); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_hook(); + } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + + /* <-- INFORMATIONAL { D } */ + if (data[_i].spi_del_b == 2) + { + assert_hook_rekey(child_rekey, 1, 6); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + assert_hook(); + } + assert_child_sa_count(a, 2); + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5, 6); + } + else + { + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 3); + assert_scheduler(); + assert_hook(); + + /* trigger a delete for the new CHILD_SA */ + call_ikesa(b, delete_child_sa, PROTO_ESP, data[_i].spi_b, FALSE); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 2, 4, 5, + data[_i].spi_del_b == 2 ? 6 : 3); + + /* child_updown */ + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); + assert_hook_not_called(child_updown); + if (data[_i].spi_del_b == 2) + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_a, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + } + else + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + } + assert_child_sa_count(a, 2); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, data[_i].spi_del_b == 2 ? 5 : 4); + assert_scheduler(); + assert_hook(); + /* child_rekey */ + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_hook_updown(child_updown, FALSE); + if (data[_i].spi_del_a == 1) + { + assert_hook_rekey(child_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + assert_ipsec_sas_installed(a, 1, 3); + assert_hook(); + } + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 2); + assert_hook(); + + /* we don't expect these hooks to get called anymore */ + assert_hook_not_called(child_updown); + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_ipsec_sas_installed(b, 2, data[_i].spi_del_b == 2 ? 5 : 4); + assert_child_sa_count(b, 2); + assert_scheduler(); + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, data[_i].spi_del_a == 1 ? 6 : 3); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a); + destroy_rekeyed(a, data[_i].spi_del_b); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, data[_i].spi_del_a); + destroy_rekeyed(b, data[_i].spi_del_b); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * This is like a regular rekey collision, but one CREATE_CHILD_SA response * is delayed: @@ -1717,7 +2359,7 @@ START_TEST(test_collision_delayed_response_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1732,29 +2374,33 @@ START_TEST(test_collision_delayed_response_multi_ke) assert_ipsec_sas_installed(b, 2, 4, 6); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (!after_delete) { /* a receives the response right after the IKE_FOLLOWUP_KE, the passive * rekeying is completed and the active aborted */ /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); } /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 6); + assert_jobs_scheduled(after_delete ? 0 : 1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); assert_child_sa_count(a, 2); assert_scheduler(); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1768,12 +2414,14 @@ START_TEST(test_collision_delayed_response_multi_ke) if (after_delete) { /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); } /* simulate the execution of the scheduled jobs */ @@ -1803,14 +2451,27 @@ END_TEST * -------\--------> * \ /---- delete old SA * \-/----> detect collision - * detect collision <---------/ /---- TEMP_FAIL - * delete -----------/----> + * handle delete <---------/ /---- TEMP_FAIL + * -----------/----> * aborts rekeying <---------/ + * + * Besides the scenario depicted above, i.e. where the response arrives after + * handling B's delete request, we also test when it arrives before that: + * + * ... + * \ /---- delete old SA + * \-/----> detect collision + * aborts rekeying <---------/------ TEMP_FAIL + * handle delete <--------/ + * ----------------> */ START_TEST(test_collision_delayed_request) { ike_sa_t *a, *b; message_t *msg; + bool before_delete = _i >= 3; + + _i %= 3; exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1846,7 +2507,7 @@ START_TEST(test_collision_delayed_request) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1860,32 +2521,74 @@ START_TEST(test_collision_delayed_request) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_hook_not_called(child_rekey); assert_single_notify(OUT, TEMPORARY_FAILURE); exchange_test_helper->process_message(exchange_test_helper, b, msg); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_hook(); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, 1, 4, 5); - assert_scheduler(); + if (before_delete) + { + /* delay the DELETE request from b to a so TEMP_FAIL arrives before */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + } + else + { + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + } /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_no_jobs_scheduled(); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, 1, 4, 5); - assert_scheduler(); + assert_hook_not_called(child_rekey); + if (before_delete) + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_scheduler(); + } + else + { + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + } + assert_hook(); + + if (before_delete) + { + /* <-- INFORMATIONAL { D } (delayed) */ + assert_hook_rekey(child_rekey, 1, 5); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + } + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1969,7 +2672,7 @@ START_TEST(test_collision_delayed_request_more) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1983,17 +2686,20 @@ START_TEST(test_collision_delayed_request_more) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 5); assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -2010,8 +2716,9 @@ START_TEST(test_collision_delayed_request_more) assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 4, 5); + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ - assert_no_jobs_scheduled(); + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); @@ -2039,6 +2746,171 @@ START_TEST(test_collision_delayed_request_more) } END_TEST +/** + * Similar to above one peer fails to notice the collision but the + * CREATE_CHILD_SA request is even more delayed: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * handle delete <-------\-------- delete old SA + * ---------\------> + * handle delete <---------\------ delete new SA + * -----------\----> + * \---> + * /---- CHILD_SA_NOT_FOUND + * aborts rekeying <----------/ + */ +START_TEST(test_collision_delayed_request_more_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + } data[] = { + { { 0x00, 0xFF, 0xFF } }, + { { 0xFF, 0x00, 0xFF } }, + { { 0xFF, 0xFF, 0x00 } }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* this should not get called until the new SA is deleted */ + assert_hook_not_called(child_updown); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_hook(); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_rekey(child_rekey, 2, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + + /* child_updown() */ + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* this is expected later */ + assert_hook_not_called(child_updown); + + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_scheduler(); + + /* trigger a delete for the new CHILD_SA */ + call_ikesa(b, delete_child_sa, PROTO_ESP, 5, FALSE); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + + /* child_updown() */ + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_scheduler(); + assert_hook(); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_updown); + assert_single_notify(OUT, CHILD_SA_NOT_FOUND); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_hook_updown(child_updown, FALSE); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_updown); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, 2); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * In this scenario one of the peers does not notice that there is a * rekey collision: @@ -2056,7 +2928,7 @@ END_TEST * aborts rekeying <-------/ * * In a variation of this scenario, the TEMP_FAIL notify arrives before the - * delete does. + * delete does, similar to the non-multi-KE scenario above. */ START_TEST(test_collision_delayed_request_multi_ke) { @@ -2119,7 +2991,7 @@ START_TEST(test_collision_delayed_request_multi_ke) exchange_test_helper->process_message(exchange_test_helper, b, msg); /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); @@ -2157,17 +3029,18 @@ START_TEST(test_collision_delayed_request_multi_ke) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 5); + assert_jobs_scheduled(after_delete ? 0 : 1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 5); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -2181,7 +3054,7 @@ START_TEST(test_collision_delayed_request_multi_ke) if (after_delete) { /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_no_jobs_scheduled(); + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -2268,127 +3141,141 @@ START_TEST(test_collision_ke_invalid) /* this should never get called as this results in a successful rekeying */ assert_hook_not_called(child_updown); + /* this should not be called until the active rekeyings are concluded */ + assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; - 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, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* CREATE_CHILD_SA { N(INVAL_KE) } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 7); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 8); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 8, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + + /* child_rekey */ assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ if (data[_i].spi_del_a == 1) - { /* currently we call this again if we keep our own replacement as we - * already called it above */ + { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_hook(); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); + assert_hook(); } else { + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); + assert_hook(); } + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ if (data[_i].spi_del_b == 2) { assert_hook_rekey(child_rekey, 2, data[_i].spi_b); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); + assert_hook(); } else { + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); + assert_hook(); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); - - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - data[_i].spi_del_b == 2 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 3); - assert_scheduler(); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + if (data[_i].spi_del_a == 1) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 1, data[_i].spi_a); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - data[_i].spi_del_a == 1 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 3); - assert_scheduler(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -2487,47 +3374,43 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* this should never get called as this results in a successful rekeying */ assert_hook_not_called(child_updown); + /* this should not be called until b doesn't notice a collision */ + assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; - 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, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* CREATE_CHILD_SA { N(INVAL_KE) } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* delay the CREATE_CHILD_SA request from a to b */ msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 7); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + + /* child_rekey */ assert_hook(); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ assert_hook_rekey(child_rekey, 2, 6); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -2535,30 +3418,36 @@ START_TEST(test_collision_ke_invalid_delayed_retry) assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_hook_not_called(child_rekey); assert_single_notify(OUT, TEMPORARY_FAILURE); exchange_test_helper->process_message(exchange_test_helper, b, msg); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_hook(); /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_scheduler(); - - /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_rekey(child_rekey, 1, 7); assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_scheduler(); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_scheduler(); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -2588,6 +3477,147 @@ START_TEST(test_collision_ke_invalid_delayed_retry) } END_TEST +/** + * This simulates incorrect behavior by a hypothetical IKEv2 responder, which + * might send a delete for the old CHILD_SA even if it lost the collision + * (compared to the incorrect delete without collision, see above, this hasn't + * been observed in the wild). + * This is an issue if the DELETE arrives before the CREATE_CHILD_SA response. + */ +START_TEST(test_collision_responder_incorrect_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* make sure the responder looses the collision */ + exchange_test_helper->nonce_first_byte = 0xff; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = 0x00; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = 0xff; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = 0xff; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_scheduler(); + + /* inject an incorrect delete for the old CHILD_SA by the responder, + * without messing with its internal state */ + send_child_delete(b, 2); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(2); + assert_hook_rekey(child_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 3); + assert_ipsec_sas_installed(a, 1, 3, 5, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } (response to incorrect DELETE) --> */ + assert_no_jobs_scheduled(); + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + /* simulate handling of the delete via expire, does not delay destroy */ + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 3, 4, 5); + assert_hook(); + assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 1); + destroy_rekeyed(a, 6); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 3, 5); + destroy_rekeyed(b, 4); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 3, 5); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * One of the hosts initiates a DELETE of the CHILD_SA the other peer is * concurrently trying to rekey. @@ -2640,18 +3670,21 @@ START_TEST(test_collision_delete) */ /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + /* the SA is not destroyed until we get the CREATE_CHILD_SA response */ + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* we don't expect a job to retry the rekeying */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_scheduler(); assert_hook(); @@ -2727,18 +3760,20 @@ START_TEST(test_collision_delete_multi_ke) assert_hook(); /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* we don't expect a job to retry the rekeying */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_scheduler(); assert_hook(); @@ -2858,7 +3893,7 @@ END_TEST * /---- CHILD_SA_NOT_FOUND * aborts rekeying <----------/ */ - START_TEST(test_collision_delete_drop_rekey) +START_TEST(test_collision_delete_drop_rekey) { ike_sa_t *a, *b; message_t *msg; @@ -2891,11 +3926,12 @@ END_TEST */ /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* INFORMATIONAL { D } --> */ @@ -2916,10 +3952,11 @@ END_TEST assert_hook(); /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* no jobs or tasks should get scheduled/queued */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_scheduler(); assert_hook(); @@ -2935,26 +3972,133 @@ END_TEST END_TEST /** - * FIXME: Not sure what we can do about the following: - * * One of the hosts initiates a rekeying of a CHILD_SA and after responding to * it the other peer deletes the new SA. However, the rekey response is * delayed or dropped, so the peer doing the rekeying receives a delete for an - * unknown CHILD_SA and then has a rekeyed CHILD_SA that should not exist. + * unknown CHILD_SA and has to consider this when processing the rekey response. * * rekey ----------------> * /---- rekey * unknown SA <----------/----- delete new SA * ----------/-----> - * <--------/ - * - * The peers' states are now out of sync. - * - * Perhaps the rekey initiator could keep track of deletes for non-existing SAs - * while rekeying and then check against the SPIs when handling the - * CREATE_CHILD_SA response. + * delete SA <--------/ */ +START_TEST(test_collision_delete_delayed_response) +{ + ike_sa_t *a, *b; + message_t *msg; + uint32_t spi_a = _i+1, spi_b = 2-_i; + if (_i) + { /* responder rekeys the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + initiate_rekey(a, spi_a); + + /* 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_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, spi_a, spi_b, 4); + assert_hook(); + + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + assert_hook_rekey(child_rekey, spi_b, 4); + assert_hook_not_called(child_updown); + call_ikesa(b, delete_child_sa, PROTO_ESP, 4, FALSE); + assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, spi_b, 3, 4); + assert_child_sa_count(b, 2); + assert_hook(); + assert_hook(); + + /* this is not expected to get called until the response is processed */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_updown); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, spi_b); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } (delayed) */ + assert_hook_rekey(child_rekey, spi_a, 3); + assert_hook_updown(child_updown, FALSE); + /* the job scheduled here is for the retransmit of the delete */ + assert_jobs_scheduled(1); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_scheduler(); + assert_hook(); + assert_hook(); + + /* this is not expected to get called anymore */ + assert_hook_not_called(child_rekey); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + 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_count(b, 1); + assert_ipsec_sas_installed(b, spi_b); + assert_scheduler(); + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + 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_count(a, 1); + assert_ipsec_sas_installed(a, spi_a); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, spi_a); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, spi_b); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(a); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST /** * One of the hosts initiates a rekey of the IKE_SA of the CHILD_SA the other @@ -3132,6 +4276,9 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_regular_ke_invalid_multi_ke, 0, 2); tcase_add_test(tc, test_regular_responder_ignore_soft_expire); tcase_add_test(tc, test_regular_responder_handle_hard_expire); + tcase_add_test(tc, test_regular_responder_delete); + tcase_add_test(tc, test_regular_responder_lost_sa); + tcase_add_test(tc, test_regular_responder_incorrect_delete); suite_add_tcase(s, tc); tc = tcase_create("collisions rekey"); @@ -3139,12 +4286,15 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_collision_multi_ke, 0, 4); tcase_add_loop_test(tc, test_collision_mixed, 0, 4); tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_response_delete, 0, 4); tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4); - tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request, 0, 6); tcase_add_loop_test(tc, test_collision_delayed_request_more, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_more_delete, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_request_multi_ke, 0, 6); tcase_add_loop_test(tc, test_collision_ke_invalid, 0, 4); tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3); + tcase_add_test(tc, test_collision_responder_incorrect_delete); suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); @@ -3152,6 +4302,7 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_collision_delete_multi_ke, 0, 2); tcase_add_loop_test(tc, test_collision_delete_drop_delete, 0, 2); tcase_add_loop_test(tc, test_collision_delete_drop_rekey, 0, 2); + tcase_add_loop_test(tc, test_collision_delete_delayed_response, 0, 2); suite_add_tcase(s, tc); tc = tcase_create("collisions ike rekey"); From ddb9b274c2f253bd96ee9176570a3dfe2079c2a9 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 9 Sep 2022 18:34:29 +0200 Subject: [PATCH 45/46] unit-tests: Ensure listeners can track SAs via ike/child_updown/rekey() Previously, it could happen that child_rekey() was triggered twice for the same "old" SA. For listeners that would mean they'd loose track as they'd be tracking a new SA that wasn't relevant anymore and for which no updown event would ever get triggered (it was the redundant SA in a collision). This new assert ensures that events are triggered in a predictable way and listeners can track SAs properly. --- src/libcharon/tests/suites/test_child_rekey.c | 89 +++++++++++++ src/libcharon/tests/suites/test_ike_rekey.c | 54 ++++++++ .../tests/utils/exchange_test_asserts.c | 123 ++++++++++++++++++ .../tests/utils/exchange_test_asserts.h | 96 +++++++++++++- 4 files changed, 361 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 4b10e9205..b61f31c7c 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -60,6 +60,8 @@ START_TEST(test_regular) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -127,6 +129,7 @@ START_TEST(test_regular) /* child_updown */ assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -154,6 +157,8 @@ START_TEST(test_regular_multi_ke) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -239,6 +244,7 @@ START_TEST(test_regular_multi_ke) /* child_updown */ assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -263,6 +269,8 @@ START_TEST(test_regular_ke_invalid) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -399,6 +407,7 @@ START_TEST(test_regular_ke_invalid) /* child_updown */ assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -423,6 +432,8 @@ START_TEST(test_regular_ke_invalid_multi_ke) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -596,6 +607,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) /* child_updown */ assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -610,6 +622,8 @@ START_TEST(test_regular_responder_ignore_soft_expire) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); initiate_rekey(a, 1); @@ -676,6 +690,7 @@ START_TEST(test_regular_responder_ignore_soft_expire) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -691,6 +706,8 @@ START_TEST(test_regular_responder_handle_hard_expire) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); initiate_rekey(a, 1); @@ -755,6 +772,7 @@ START_TEST(test_regular_responder_handle_hard_expire) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -770,6 +788,8 @@ START_TEST(test_regular_responder_delete) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); initiate_rekey(a, 1); @@ -869,6 +889,7 @@ START_TEST(test_regular_responder_delete) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 0); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -883,6 +904,8 @@ START_TEST(test_regular_responder_lost_sa) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); initiate_rekey(a, 1); @@ -925,6 +948,9 @@ START_TEST(test_regular_responder_lost_sa) /* child_rekey */ assert_hook(); + /* the additional CHILD_SA here is the one we destroyed on b without + * triggering an event */ + assert_track_sas(2, 3); assert_sa_idle(a); assert_sa_idle(b); @@ -985,6 +1011,8 @@ START_TEST(test_regular_responder_incorrect_delete) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); initiate_rekey(a, 1); @@ -1054,6 +1082,7 @@ START_TEST(test_regular_responder_incorrect_delete) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -1068,6 +1097,8 @@ START_TEST(test_collision) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1279,6 +1310,7 @@ START_TEST(test_collision) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -1293,6 +1325,8 @@ START_TEST(test_collision_multi_ke) { ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -1481,6 +1515,7 @@ START_TEST(test_collision_multi_ke) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -1504,6 +1539,8 @@ START_TEST(test_collision_mixed) }; ike_sa_t *a, *b; + assert_track_sas_start(); + /* let's accept what the peer proposes first */ lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); @@ -1742,6 +1779,7 @@ START_TEST(test_collision_mixed) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -1769,6 +1807,8 @@ START_TEST(test_collision_delayed_response) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1980,6 +2020,7 @@ START_TEST(test_collision_delayed_response) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -2007,6 +2048,8 @@ START_TEST(test_collision_delayed_response_delete) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -2258,6 +2301,7 @@ START_TEST(test_collision_delayed_response_delete) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 0); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -2296,6 +2340,8 @@ START_TEST(test_collision_delayed_response_multi_ke) _i %= 2; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -2435,6 +2481,7 @@ START_TEST(test_collision_delayed_response_multi_ke) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -2473,6 +2520,8 @@ START_TEST(test_collision_delayed_request) _i %= 3; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -2610,6 +2659,7 @@ START_TEST(test_collision_delayed_request) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -2638,6 +2688,8 @@ START_TEST(test_collision_delayed_request_more) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -2737,6 +2789,7 @@ START_TEST(test_collision_delayed_request_more) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -2767,6 +2820,8 @@ START_TEST(test_collision_delayed_request_more_delete) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -2902,6 +2957,7 @@ START_TEST(test_collision_delayed_request_more_delete) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -2938,6 +2994,8 @@ START_TEST(test_collision_delayed_request_multi_ke) _i %= 3; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -3075,6 +3133,7 @@ START_TEST(test_collision_delayed_request_multi_ke) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -3101,6 +3160,8 @@ START_TEST(test_collision_ke_invalid) }; ike_sa_t *a, *b; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &conf); @@ -3312,6 +3373,7 @@ START_TEST(test_collision_ke_invalid) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -3338,6 +3400,8 @@ START_TEST(test_collision_ke_invalid_delayed_retry) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &conf); @@ -3468,6 +3532,7 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -3489,6 +3554,8 @@ START_TEST(test_collision_responder_incorrect_delete) ike_sa_t *a, *b; message_t *msg; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -3612,6 +3679,7 @@ START_TEST(test_collision_responder_incorrect_delete) /* child_rekey/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -3634,6 +3702,8 @@ START_TEST(test_collision_delete) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -3696,6 +3766,7 @@ START_TEST(test_collision_delete) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -3722,6 +3793,8 @@ START_TEST(test_collision_delete_multi_ke) ike_sa_t *a, *b; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -3786,6 +3859,7 @@ START_TEST(test_collision_delete_multi_ke) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -3814,6 +3888,8 @@ START_TEST(test_collision_delete_drop_delete) message_t *msg; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -3872,6 +3948,7 @@ START_TEST(test_collision_delete_drop_delete) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -3899,6 +3976,8 @@ START_TEST(test_collision_delete_drop_rekey) message_t *msg; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -3962,6 +4041,7 @@ START_TEST(test_collision_delete_drop_rekey) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -3989,6 +4069,8 @@ START_TEST(test_collision_delete_delayed_response) message_t *msg; uint32_t spi_a = _i+1, spi_b = 2-_i; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -4091,6 +4173,7 @@ START_TEST(test_collision_delete_delayed_response) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -4116,6 +4199,8 @@ START_TEST(test_collision_ike_rekey) ike_sa_t *a, *b; uint32_t spi_a = _i+1; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -4172,6 +4257,7 @@ START_TEST(test_collision_ike_rekey) /* ike_rekey/child_rekey */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -4199,6 +4285,8 @@ START_TEST(test_collision_ike_delete) message_t *msg; status_t s; + assert_track_sas_start(); + if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -4259,6 +4347,7 @@ START_TEST(test_collision_ike_delete) /* child_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index 98c588b1e..c6691acf4 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -40,6 +40,8 @@ START_TEST(test_regular) 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, @@ -98,6 +100,7 @@ START_TEST(test_regular) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -124,6 +127,8 @@ START_TEST(test_regular_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, @@ -202,6 +207,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -225,6 +231,8 @@ START_TEST(test_regular_ke_invalid) ike_sa_t *a, *b, *sa; status_t s; + assert_track_sas_start(); + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); if (_i) @@ -303,6 +311,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -326,6 +335,8 @@ START_TEST(test_regular_ke_invalid_multi_ke) ike_sa_t *a, *b, *sa; status_t s; + assert_track_sas_start(); + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); if (_i) @@ -423,6 +434,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -437,6 +449,8 @@ START_TEST(test_collision) ike_sa_t *a, *b, *sa; status_t status; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -578,6 +592,7 @@ START_TEST(test_collision) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -593,6 +608,8 @@ START_TEST(test_collision_multi_ke) ike_sa_t *a, *b, *sa; status_t status; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -729,6 +746,7 @@ START_TEST(test_collision_multi_ke) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -752,6 +770,8 @@ START_TEST(test_collision_mixed) ike_sa_t *a, *b, *sa; status_t status; + assert_track_sas_start(); + /* let's accept what the peer proposes first */ lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); @@ -953,6 +973,7 @@ START_TEST(test_collision_mixed) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -976,6 +997,8 @@ START_TEST(test_collision_ke_invalid) ike_sa_t *a, *b, *sa; status_t status; + assert_track_sas_start(); + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); @@ -1165,6 +1188,7 @@ START_TEST(test_collision_ke_invalid) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1187,6 +1211,8 @@ START_TEST(test_collision_ke_invalid_delayed_retry) message_t *msg; status_t s; + assert_track_sas_start(); + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", FALSE, lib->ns); @@ -1325,6 +1351,7 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1366,6 +1393,8 @@ START_TEST(test_collision_delayed_response) message_t *msg, *d; status_t s; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1545,6 +1574,7 @@ START_TEST(test_collision_delayed_response) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1584,6 +1614,8 @@ START_TEST(test_collision_delayed_response_multi_ke) _i %= 2; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -1746,6 +1778,7 @@ START_TEST(test_collision_delayed_response_multi_ke) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1769,6 +1802,8 @@ START_TEST(test_collision_dropped_request) message_t *msg; status_t s; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1848,6 +1883,7 @@ START_TEST(test_collision_dropped_request) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1874,6 +1910,8 @@ START_TEST(test_collision_delayed_request) message_t *msg; status_t s; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1963,6 +2001,7 @@ START_TEST(test_collision_delayed_request) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1989,6 +2028,8 @@ START_TEST(test_collision_delayed_request_and_delete) message_t *msg; status_t s; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -2083,6 +2124,7 @@ START_TEST(test_collision_delayed_request_and_delete) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -2117,6 +2159,8 @@ START_TEST(test_collision_delayed_request_multi_ke) _i %= 3; + assert_track_sas_start(); + exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, &multi_ke_conf); @@ -2256,6 +2300,7 @@ START_TEST(test_collision_delayed_request_multi_ke) /* ike_updown/child_updown */ assert_hook(); assert_hook(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -2278,6 +2323,8 @@ START_TEST(test_collision_delete) message_t *msg; status_t s; + assert_track_sas_start(); + if (_i) { /* responder rekeys the IKE_SA */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -2340,6 +2387,7 @@ START_TEST(test_collision_delete) /* ike_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST @@ -2361,6 +2409,8 @@ START_TEST(test_collision_delete_multi_ke) message_t *msg; status_t s; + assert_track_sas_start(); + if (_i) { /* responder rekeys the IKE_SA */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -2425,6 +2475,7 @@ START_TEST(test_collision_delete_multi_ke) /* ike_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST @@ -2445,6 +2496,8 @@ START_TEST(test_collision_delete_drop_delete) message_t *msg; status_t s; + assert_track_sas_start(); + if (_i) { /* responder rekeys the IKE_SA */ exchange_test_helper->establish_sa(exchange_test_helper, @@ -2511,6 +2564,7 @@ START_TEST(test_collision_delete_drop_delete) /* ike_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST diff --git a/src/libcharon/tests/utils/exchange_test_asserts.c b/src/libcharon/tests/utils/exchange_test_asserts.c index 1a4fdda83..1db0215df 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.c +++ b/src/libcharon/tests/utils/exchange_test_asserts.c @@ -105,6 +105,129 @@ bool exchange_test_asserts_child_rekey(listener_t *listener, ike_sa_t *ike_sa, return TRUE; } +/** + * Track SAs via updown event. + */ +static void track_sa_updown(listener_track_sas_assert_t *this, char *event, + array_t *sas, uint32_t id, bool up) +{ + uint32_t existing; + bool found = FALSE; + int i; + + if (up) + { + for (i = 0; i < array_count(sas); i++) + { + array_get(sas, i, &existing); + assert_listener_msg(id != existing, this, "duplicate %s(up) event " + "for SA %u", event, id); + } + array_insert(sas, ARRAY_TAIL, &id); + } + else + { + for (i = 0; i < array_count(sas); i++) + { + array_get(sas, i, &existing); + if (id == existing) + { + array_remove(sas, i, NULL); + found = TRUE; + break; + } + } + assert_listener_msg(found, this, "%s(down) event for unknown SA %u", + event, id); + } +} + +/** + * Track SAs via a rekey event. + */ +static void track_sa_rekey(listener_track_sas_assert_t *this, char *event, + array_t *sas, uint32_t old_id, uint32_t new_id) +{ + uint32_t existing; + bool found = FALSE; + int i; + + for (i = 0; i < array_count(sas); i++) + { + array_get(sas, i, &existing); + if (old_id == existing) + { + array_remove(sas, i, NULL); + found = TRUE; + break; + } + } + assert_listener_msg(found, this, "%s() event for unknown old SA %u", event, + old_id); + + for (i = 0; i < array_count(sas); i++) + { + array_get(sas, i, &existing); + assert_listener_msg(new_id != existing, this, "%s() event for " + "already up new SA %u", event, new_id); + } + array_insert(sas, ARRAY_TAIL, &new_id); +} + +/* + * Described in header + */ +bool exchange_test_asserts_track_ike_updown(listener_t *listener, + ike_sa_t *ike_sa, bool up) +{ + listener_track_sas_assert_t *this = (listener_track_sas_assert_t*)listener; + + track_sa_updown(this, "ike_updown", this->ike_sas, + ike_sa->get_unique_id(ike_sa), up); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_track_child_updown(listener_t *listener, + ike_sa_t *ike_sa, + child_sa_t *child_sa, bool up) +{ + listener_track_sas_assert_t *this = (listener_track_sas_assert_t*)listener; + + track_sa_updown(this, "child_updown", this->child_sas, + child_sa->get_unique_id(child_sa), up); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_track_ike_rekey(listener_t *listener, ike_sa_t *old, + ike_sa_t *new) +{ + listener_track_sas_assert_t *this = (listener_track_sas_assert_t*)listener; + + track_sa_rekey(this, "ike_rekey", this->ike_sas, old->get_unique_id(old), + new->get_unique_id(new)); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_track_child_rekey(listener_t *listener, + ike_sa_t *ike_sa, child_sa_t *old, + child_sa_t *new) +{ + listener_track_sas_assert_t *this = (listener_track_sas_assert_t*)listener; + + track_sa_rekey(this, "child_rekey", this->child_sas, old->get_unique_id(old), + new->get_unique_id(new)); + return TRUE; +} + /** * Assert a given message rule */ diff --git a/src/libcharon/tests/utils/exchange_test_asserts.h b/src/libcharon/tests/utils/exchange_test_asserts.h index e4ce81040..c37f1c3bb 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.h +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Tobias Brunner + * Copyright (C) 2016-2022 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -27,6 +27,7 @@ #include typedef struct listener_hook_assert_t listener_hook_assert_t; +typedef struct listener_track_sas_assert_t listener_track_sas_assert_t; typedef struct listener_message_assert_t listener_message_assert_t; typedef struct listener_message_rule_t listener_message_rule_t; typedef struct ipsec_sas_assert_t ipsec_sas_assert_t; @@ -209,6 +210,99 @@ do { \ } \ } while(FALSE) +/** + * Track SAs by following events. + */ +struct listener_track_sas_assert_t { + + /** + * Implemented interface + */ + listener_t listener; + + /** + * Original source file + */ + const char *file; + + /** + * Source line + */ + int line; + + /** + * Tracked IKE_SAs. + */ + array_t *ike_sas; + + /** + * Tracked CHILD_SAs. + */ + array_t *child_sas; +}; + + +/** + * Implementation of listener_t::ike_updown. + */ +bool exchange_test_asserts_track_ike_updown(listener_t *this, ike_sa_t *ike_sa, + bool up); + +/** + * Implementation of listener_t::child_updown. + */ +bool exchange_test_asserts_track_child_updown(listener_t *this, ike_sa_t *ike_sa, + child_sa_t *child_sa, bool up); + +/** + * Implementation of listener_t::ike_rekey. + */ +bool exchange_test_asserts_track_ike_rekey(listener_t *this, ike_sa_t *old, + ike_sa_t *new); + +/** + * Implementation of listener_t::child_rekey. + */ +bool exchange_test_asserts_track_child_rekey(listener_t *this, ike_sa_t *ike_sa, + child_sa_t *old, child_sa_t *new); + +/** + * Start tracking SAs via their hooks. + */ +#define assert_track_sas_start() \ +do { \ + listener_track_sas_assert_t _track_sas_listener = { \ + .listener = { \ + .ike_updown = exchange_test_asserts_track_ike_updown, \ + .ike_rekey = exchange_test_asserts_track_ike_rekey, \ + .child_updown = exchange_test_asserts_track_child_updown, \ + .child_rekey = exchange_test_asserts_track_child_rekey, \ + }, \ + .file = __FILE__, \ + .line = __LINE__, \ + .ike_sas = array_create(sizeof(uint32_t), 8), \ + .child_sas = array_create(sizeof(uint32_t), 8), \ + }; \ + exchange_test_helper->add_listener(exchange_test_helper, &_track_sas_listener.listener) + +/** + * Check if there are the right number of SAs still up. + * + * @param ike the expected number of IKE_SAs + * @param child the expected number of CHILD_SAs + */ +#define assert_track_sas(ike, child) \ + charon->bus->remove_listener(charon->bus, &_track_sas_listener.listener); \ + u_int _up_ike = array_count(_track_sas_listener.ike_sas); \ + u_int _up_child = array_count(_track_sas_listener.child_sas); \ + array_destroy(_track_sas_listener.ike_sas); \ + array_destroy(_track_sas_listener.child_sas); \ + assert_listener_msg(_up_ike == (ike), &_track_sas_listener, \ + "%d IKE_SAs without matching down event", _up_ike); \ + assert_listener_msg(_up_child == (child), &_track_sas_listener, \ + "%d CHILD_SAs without matching down event", _up_child); \ +} while(FALSE) + /** * Rules regarding payloads/notifies to expect/not expect in a message */ From f3c7e5227cccc010f21a750810c4c300d9cf8e13 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 6 Jun 2024 17:48:38 +0200 Subject: [PATCH 46/46] testing: Add ikev2/rw-cert-multi-ke scenario --- .../ikev2/rw-cert-multi-ke/description.txt | 12 +++++++ .../tests/ikev2/rw-cert-multi-ke/evaltest.dat | 32 +++++++++++++++++ .../hosts/carol/etc/strongswan.conf | 9 +++++ .../hosts/carol/etc/swanctl/swanctl.conf | 35 +++++++++++++++++++ .../hosts/dave/etc/strongswan.conf | 9 +++++ .../hosts/dave/etc/swanctl/swanctl.conf | 27 ++++++++++++++ .../hosts/moon/etc/strongswan.conf | 9 +++++ .../hosts/moon/etc/swanctl/swanctl.conf | 25 +++++++++++++ .../tests/ikev2/rw-cert-multi-ke/posttest.dat | 8 +++++ .../tests/ikev2/rw-cert-multi-ke/pretest.dat | 11 ++++++ .../tests/ikev2/rw-cert-multi-ke/test.conf | 25 +++++++++++++ 11 files changed, 202 insertions(+) create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/description.txt create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/evaltest.dat create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/strongswan.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/swanctl/swanctl.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/strongswan.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/swanctl/swanctl.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/strongswan.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/swanctl/swanctl.conf create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/posttest.dat create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/pretest.dat create mode 100755 testing/tests/ikev2/rw-cert-multi-ke/test.conf diff --git a/testing/tests/ikev2/rw-cert-multi-ke/description.txt b/testing/tests/ikev2/rw-cert-multi-ke/description.txt new file mode 100755 index 000000000..3532dfe2b --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/description.txt @@ -0,0 +1,12 @@ +The roadwarriors carol and dave set up a connection each +to gateway moon. The authentication is based on X.509 certificates. +To test multiple key exchanges (RFC 9370) and IKE_INTERMEDIATE exchanges (RFC 9242), +carol proposes MODP_2048 for the key exchange and CURVE_25519 for the +additional key exchange whereas dave proposes MODP_3072 and ECP_384, +respectively. The IKE and ESP SAs are then rekeyed using the same proposals. +One set of SAs is rekeyed from carol and another from moon. +

+Upon the successful establishment of the IPsec tunnels, the updown script +automatically inserts iptables-based firewall rules that let pass the tunneled traffic. +In order to test both tunnel and firewall, both carol and dave ping +the client alice behind the gateway moon. diff --git a/testing/tests/ikev2/rw-cert-multi-ke/evaltest.dat b/testing/tests/ikev2/rw-cert-multi-ke/evaltest.dat new file mode 100755 index 000000000..5fdda3e1f --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/evaltest.dat @@ -0,0 +1,32 @@ +carol::swanctl --list-sas --raw 2> /dev/null::home.*version=2 state=ESTABLISHED local-host=192.168.0.100 local-port=4500 local-id=carol@strongswan.org remote-host=192.168.0.1 remote-port=4500 remote-id=moon.strongswan.org initiator=yes.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_2048 ake1=CURVE_25519.*child-sas.*home.*state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128.*local-ts=\[192.168.0.100/32] remote-ts=\[10.1.0.0/16]::YES +dave:: swanctl --list-sas --raw 2> /dev/null::home.*version=2 state=ESTABLISHED local-host=192.168.0.200 local-port=4500 local-id=dave@strongswan.org remote-host=192.168.0.1 remote-port=4500 remote-id=moon.strongswan.org initiator=yes.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_3072 ake1=ECP_384.*child-sas.*home.*state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128.*local-ts=\[192.168.0.200/32] remote-ts=\[10.1.0.0/16]::YES +moon:: swanctl --list-sas --ike-id 1 --raw 2> /dev/null::rw.*version=2 state=ESTABLISHED local-host=192.168.0.1 local-port=4500 local-id=moon.strongswan.org remote-host=192.168.0.100 remote-port=4500 remote-id=carol@strongswan.org.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_2048 ake1=CURVE_25519.*child-sas.*net.*reqid=1 state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128.*local-ts=\[10.1.0.0/16] remote-ts=\[192.168.0.100/32]::YES +moon:: swanctl --list-sas --ike-id 2 --raw 2> /dev/null::rw.*version=2 state=ESTABLISHED local-host=192.168.0.1 local-port=4500 local-id=moon.strongswan.org remote-host=192.168.0.200 remote-port=4500 remote-id=dave@strongswan.org.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_3072 ake1=ECP_384.*child-sas.*net.*reqid=2 state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128.*local-ts=\[10.1.0.0/16] remote-ts=\[192.168.0.200/32]::YES +alice::ping -c 1 192.168.0.100::64 bytes from 192.168.0.100: icmp_.eq=1::YES +alice::ping -c 1 192.168.0.200::64 bytes from 192.168.0.200: icmp_.eq=1::YES +# rekey with carol from moon (original responder), first the IKE_SA then the CHILD_SA using the new IKE_SA +moon::swanctl --rekey --ike-id 1 +moon::sleep 1 +alice::ping -c 1 192.168.0.100::64 bytes from 192.168.0.100: icmp_.eq=1::YES +moon::swanctl --rekey --child-id 1 +moon::sleep 1 +alice::ping -c 1 192.168.0.100::64 bytes from 192.168.0.100: icmp_.eq=1::YES +# rekey from dave (original initiator) +dave::swanctl --rekey --ike home +dave::sleep 1 +alice::ping -c 1 192.168.0.200::64 bytes from 192.168.0.200: icmp_.eq=1::YES +dave::swanctl --rekey --child home +dave::sleep 1 +alice::ping -c 1 192.168.0.200::64 bytes from 192.168.0.200: icmp_.eq=1::YES +moon::tcpdump::IP moon.strongswan.org > carol.strongswan.org: ESP::YES +moon::tcpdump::IP carol.strongswan.org > moon.strongswan.org: ESP::YES +moon::tcpdump::IP moon.strongswan.org > dave.strongswan.org: ESP::YES +moon::tcpdump::IP dave.strongswan.org > moon.strongswan.org: ESP::YES +moon:: swanctl --list-sas --ike-id 3 --raw 2> /dev/null::rw.*version=2 state=ESTABLISHED local-host=192.168.0.1 local-port=4500 local-id=moon.strongswan.org remote-host=192.168.0.100 remote-port=4500 remote-id=carol@strongswan.org.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_2048 ake1=CURVE_25519.*child-sas.*net.*reqid=1 state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128 dh-group=MODP_2048 ake1=CURVE_25519.*local-ts=\[10.1.0.0/16] remote-ts=\[192.168.0.100/32]::YES +moon:: swanctl --list-sas --ike-id 4 --raw 2> /dev/null::rw.*version=2 state=ESTABLISHED local-host=192.168.0.1 local-port=4500 local-id=moon.strongswan.org remote-host=192.168.0.200 remote-port=4500 remote-id=dave@strongswan.org.*encr-alg=AES_CBC encr-keysize=128 integ-alg=HMAC_SHA2_256_128 prf-alg=PRF_HMAC_SHA2_256 dh-group=MODP_3072 ake1=ECP_384.*child-sas.*net.*reqid=2 state=INSTALLED mode=TUNNEL.*ESP.*encr-alg=AES_GCM_16 encr-keysize=128 dh-group=MODP_3072 ake1=ECP_384.*local-ts=\[10.1.0.0/16] remote-ts=\[192.168.0.200/32]::YES +# from the rekeyings with carol +moon::cat /var/log/daemon.log::parsed IKE_INTERMEDIATE request 1 \[ KE \]::2 +moon::cat /var/log/daemon.log::parsed IKE_FOLLOWUP_KE response 1 \[ KE \]::2 +# from the rekeyings with dave +moon::cat /var/log/daemon.log::parsed IKE_FOLLOWUP_KE request 4 \[ KE N(ADD_KE) \]::1 +moon::cat /var/log/daemon.log::parsed IKE_FOLLOWUP_KE request 1 \[ KE N(ADD_KE) \]::1 \ No newline at end of file diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/strongswan.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/strongswan.conf new file mode 100755 index 000000000..83612c913 --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/strongswan.conf @@ -0,0 +1,9 @@ +# /etc/strongswan.conf - strongSwan configuration file + +swanctl { + load = pem pkcs1 x509 revocation constraints pubkey openssl random +} + +charon-systemd { + load = random nonce openssl pem pkcs1 x509 revocation constraints curl kernel-netlink socket-default updown vici +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/swanctl/swanctl.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/swanctl/swanctl.conf new file mode 100755 index 000000000..2c0d9641f --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/carol/etc/swanctl/swanctl.conf @@ -0,0 +1,35 @@ +connections { + + home { + local_addrs = 192.168.0.100 + remote_addrs = 192.168.0.1 + + local { + auth = pubkey + certs = carolCert.pem + id = carol@strongswan.org + } + remote { + auth = pubkey + id = moon.strongswan.org + } + children { + home { + remote_ts = 10.1.0.0/16 + + updown = /usr/local/libexec/ipsec/_updown iptables + esp_proposals = aes128gcm128-modp2048-ke1_x25519 + } + } + version = 2 + proposals = aes128-sha256-modp2048-ke1_x25519 + } +} + +secrets { + + rsa-carol { + file = carolKey.pem + secret = "nH5ZQEWtku0RJEZ6" + } +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/strongswan.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/strongswan.conf new file mode 100755 index 000000000..83612c913 --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/strongswan.conf @@ -0,0 +1,9 @@ +# /etc/strongswan.conf - strongSwan configuration file + +swanctl { + load = pem pkcs1 x509 revocation constraints pubkey openssl random +} + +charon-systemd { + load = random nonce openssl pem pkcs1 x509 revocation constraints curl kernel-netlink socket-default updown vici +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/swanctl/swanctl.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/swanctl/swanctl.conf new file mode 100755 index 000000000..695d7074f --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/dave/etc/swanctl/swanctl.conf @@ -0,0 +1,27 @@ +connections { + + home { + local_addrs = 192.168.0.200 + remote_addrs = 192.168.0.1 + + local { + auth = pubkey + certs = daveCert.pem + id = dave@strongswan.org + } + remote { + auth = pubkey + id = moon.strongswan.org + } + children { + home { + remote_ts = 10.1.0.0/16 + + updown = /usr/local/libexec/ipsec/_updown iptables + esp_proposals = aes128gcm128-modp3072-ke1_ecp384 + } + } + version = 2 + proposals = aes128-sha256-modp3072-ke1_ecp384 + } +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/strongswan.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/strongswan.conf new file mode 100755 index 000000000..83612c913 --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/strongswan.conf @@ -0,0 +1,9 @@ +# /etc/strongswan.conf - strongSwan configuration file + +swanctl { + load = pem pkcs1 x509 revocation constraints pubkey openssl random +} + +charon-systemd { + load = random nonce openssl pem pkcs1 x509 revocation constraints curl kernel-netlink socket-default updown vici +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/swanctl/swanctl.conf b/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/swanctl/swanctl.conf new file mode 100755 index 000000000..bbd05a4bb --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/hosts/moon/etc/swanctl/swanctl.conf @@ -0,0 +1,25 @@ +connections { + + rw { + local_addrs = 192.168.0.1 + + local { + auth = pubkey + certs = moonCert.pem + id = moon.strongswan.org + } + remote { + auth = pubkey + } + children { + net { + local_ts = 10.1.0.0/16 + + updown = /usr/local/libexec/ipsec/_updown iptables + esp_proposals = aes128gcm128-modp2048-modp3072-ke1_x25519-ke1_ecp384 + } + } + version = 2 + proposals = aes128-sha256-modp2048-modp3072-ke1_x25519-ke1_ecp384 + } +} diff --git a/testing/tests/ikev2/rw-cert-multi-ke/posttest.dat b/testing/tests/ikev2/rw-cert-multi-ke/posttest.dat new file mode 100755 index 000000000..eb2100856 --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/posttest.dat @@ -0,0 +1,8 @@ +carol::swanctl --terminate --ike home +dave::swanctl --terminate --ike home +carol::systemctl stop strongswan +dave::systemctl stop strongswan +moon::systemctl stop strongswan +moon::iptables-restore < /etc/iptables.flush +carol::iptables-restore < /etc/iptables.flush +dave::iptables-restore < /etc/iptables.flush diff --git a/testing/tests/ikev2/rw-cert-multi-ke/pretest.dat b/testing/tests/ikev2/rw-cert-multi-ke/pretest.dat new file mode 100755 index 000000000..3d37a86e5 --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/pretest.dat @@ -0,0 +1,11 @@ +moon::iptables-restore < /etc/iptables.rules +carol::iptables-restore < /etc/iptables.rules +dave::iptables-restore < /etc/iptables.rules +moon::systemctl start strongswan +carol::systemctl start strongswan +dave::systemctl start strongswan +moon::expect-connection rw +carol::expect-connection home +carol::swanctl --initiate --child home 2> /dev/null +dave::expect-connection home +dave::swanctl --initiate --child home 2> /dev/null diff --git a/testing/tests/ikev2/rw-cert-multi-ke/test.conf b/testing/tests/ikev2/rw-cert-multi-ke/test.conf new file mode 100755 index 000000000..1227b9d1c --- /dev/null +++ b/testing/tests/ikev2/rw-cert-multi-ke/test.conf @@ -0,0 +1,25 @@ +#!/bin/bash +# +# This configuration file provides information on the +# guest instances used for this test + +# All guest instances that are required for this test +# +VIRTHOSTS="alice moon carol winnetou dave" + +# Corresponding block diagram +# +DIAGRAM="a-m-c-w-d.png" + +# Guest instances on which tcpdump is to be started +# +TCPDUMPHOSTS="moon" + +# Guest instances on which IPsec is started +# Used for IPsec logging purposes +# +IPSECHOSTS="moon carol dave" + +# charon controlled by swanctl +# +SWANCTL=1