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 6e79c639e..a435e29de 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-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,21 +206,22 @@ 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); } 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, @@ -221,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)); @@ -246,10 +255,18 @@ 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], - 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; @@ -279,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, @@ -291,8 +314,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; } @@ -388,6 +411,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/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 e5d6280e7..9cd6d5e02 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,17 +95,19 @@ 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"); 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)"); @@ -119,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/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/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/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 ba0c17433..5d05e2091 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,86 @@ 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}, +}; + +/** + * 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. @@ -767,6 +847,22 @@ 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, + }, + {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, @@ -1888,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 */ @@ -1994,7 +2166,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); @@ -2027,7 +2198,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); } @@ -2083,10 +2254,10 @@ 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); + this->payloads->insert_last(this->payloads, encrypted); } aead = keymat->get_aead(keymat, FALSE); /* overhead for the encrypted fragment payload */ @@ -3036,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(). * diff --git a/src/libcharon/encoding/payloads/ike_header.c b/src/libcharon/encoding/payloads/ike_header.c index 2aee4496d..bdbb6463a 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,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_FOLLOWUP_KE, + IKE_SESSION_RESUME, + "IKE_INTERMEDIATE", + "IKE_FOLLOWUP_KE"); #ifdef ME -ENUM_NEXT(exchange_type_names, ME_CONNECT, ME_CONNECT, IKE_SESSION_RESUME, +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, +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_FOLLOWUP_KE, "EXCHANGE_TYPE_UNDEFINED"); #endif /* ME */ ENUM_END(exchange_type_names, EXCHANGE_TYPE_UNDEFINED); @@ -218,10 +222,12 @@ METHOD(payload_t, verify, status_t, } break; case IKE_SA_INIT: + case IKE_INTERMEDIATE: case IKE_AUTH: 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 5a38ad658..61625c4ae 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,12 @@ enum exchange_type_t{ */ IKE_SESSION_RESUME = 38, + /* IKE_INTERMEDIATE (RFC 9242) */ + IKE_INTERMEDIATE = 43, + + /* IKE_FOLLOWUP_KE (RFC 9370) */ + IKE_FOLLOWUP_KE = 44, + #ifdef ME /** * ME_CONNECT diff --git a/src/libcharon/encoding/payloads/notify_payload.c b/src/libcharon/encoding/payloads/notify_payload.c index edb3c489a..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"); @@ -114,11 +116,14 @@ 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, 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", @@ -175,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"); @@ -228,11 +235,14 @@ 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, 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 a1596bb05..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,19 +157,29 @@ 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 */ PPK_IDENTITY = 16436, /* No Postquantum Preshared Key Auth, RFC 8784 */ 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 */ 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); } /** 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_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/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/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))) 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/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. * 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/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/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/authenticators/eap_authenticator.c b/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c index 5f60af7ca..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, 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, 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,8 +561,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, this->int_auth, + this->msk, chunk_empty, my_id, this->reserved, + &auth_data)) { DBG1(DBG_IKE, "failed adding NO_PPK_AUTH notify"); return FALSE; @@ -767,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) { @@ -793,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, }, @@ -824,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, }, diff --git a/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c b/src/libcharon/sa/ikev2/authenticators/psk_authenticator.c index 99c7fdb5e..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,8 +91,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)) + this->int_auth, key->get_key(key), this->ppk, + my_id, this->reserved, &auth_data)) { key->destroy(key); return FAILED; @@ -103,8 +108,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)) + this->int_auth, 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 +165,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)) + 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, }, diff --git a/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c b/src/libcharon/sa/ikev2/authenticators/pubkey_authenticator.c index 4f83dad10..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,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)) + this->int_auth, this->ppk, id, this->reserved, + &octets, schemes)) { enumerator = array_create_enumerator(schemes); while (enumerator->enumerate(enumerator, ¶ms)) @@ -347,8 +353,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, this->int_auth, + 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 +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, - ppk, id, this->reserved, octets, + this->int_auth, ppk, id, this->reserved, octets, schemes) && array_remove(schemes, 0, scheme)) { @@ -696,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) { @@ -717,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, }, @@ -745,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, }, diff --git a/src/libcharon/sa/ikev2/keymat_v2.c b/src/libcharon/sa/ikev2/keymat_v2.c index c8a9c7117..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 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; @@ -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) { @@ -279,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, @@ -299,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) @@ -339,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; @@ -362,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)) @@ -377,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); @@ -411,6 +419,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); @@ -432,6 +441,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; @@ -523,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, @@ -595,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); @@ -656,10 +669,31 @@ 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], - 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; @@ -690,8 +724,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; } @@ -702,9 +737,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; @@ -722,8 +757,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; } @@ -749,7 +784,6 @@ failure: chunk_free(&octets); chunk_free(&key); return success; - } METHOD(keymat_v2_t, hash_algorithm_supported, bool, @@ -805,6 +839,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..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-2015 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,10 +87,11 @@ 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); + /** * 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). * @@ -107,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. * @@ -130,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 @@ -140,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. diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 8c07cf272..30dba22db 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. @@ -892,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); @@ -911,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); @@ -1676,6 +1720,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 +2078,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 +2123,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)); } diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 3e80a7d2d..c3a12780f 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,106 +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; - 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); } @@ -620,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; @@ -642,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, @@ -672,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,7 +676,7 @@ static status_t select_and_install(private_child_create_t *this, } status_i = status_o = FAILED; if (this->keymat->derive_child_keys(this->keymat, this->proposal, - this->dh, 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) { @@ -706,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) { @@ -763,6 +749,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, this->kes, nonce_i, nonce_r); } } chunk_clear(&integ_i); @@ -775,9 +763,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; @@ -808,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 */ @@ -815,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); @@ -838,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 */ @@ -957,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 */ @@ -965,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 */ @@ -981,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; @@ -1144,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) { @@ -1151,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)) { @@ -1162,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)) @@ -1248,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); @@ -1284,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)) @@ -1334,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) { @@ -1528,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)) { @@ -1546,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)) @@ -1596,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; @@ -1636,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); @@ -1650,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)) @@ -1663,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; @@ -1671,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); @@ -1685,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; } @@ -1733,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; } /** @@ -1744,18 +2195,70 @@ 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); + this->child_sa = NULL; return NEED_MORE; } 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) +{ + 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) + { + 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)) { @@ -1763,11 +2266,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) @@ -1811,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 */ @@ -1823,28 +2325,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); @@ -1873,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" @@ -1894,31 +2404,45 @@ METHOD(task_t, process_i, status_t, return delete_failed_sa(this); } - if (this->dh_failed) + if (!check_ke_method(this, NULL)) { - DBG1(DBG_IKE, "applying DH public value failed"); 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; } @@ -1958,10 +2482,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*, @@ -1970,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) { @@ -2002,6 +2532,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)); @@ -2021,15 +2552,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); @@ -2037,7 +2569,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; @@ -2046,6 +2578,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, @@ -2053,6 +2586,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)); @@ -2080,7 +2614,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)); @@ -2103,13 +2638,14 @@ 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, .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, @@ -2120,7 +2656,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..eae39e61c 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. @@ -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 2e2668bbe..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,76 +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; - } - 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) - { - 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) { @@ -247,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" @@ -263,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); @@ -308,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); } @@ -530,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); } @@ -540,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) { @@ -584,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 3ef175891..6c73d0671 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 * @@ -19,6 +19,7 @@ #include "child_rekey.h" #include +#include #include #include #include @@ -79,18 +80,57 @@ struct private_child_rekey_t { child_sa_t *child_sa; /** - * colliding task, may be delete or rekey + * Colliding passive rekey task */ 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. + * SPIs of SAs the peer deleted and we haven't found while this task was + * active */ - bool other_child_destroyed; + array_t *deleted_spis; + + /** + * State flags + */ + 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 the passive rekey task is completed and we adopted it, + * otherwise (i.e. for multi-KE rekeyings) we just reference it. + */ + CHILD_REKEY_PASSIVE_INSTALLED = (1<<1), + + /** + * 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_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; }; /** @@ -111,27 +151,176 @@ 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 build_delete_old_sa(private_child_rekey_t *this, message_t *message) +{ + delete_payload_t *del; + protocol_id_t protocol; + uint32_t spi; + + 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) + { + 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, + private_child_rekey_t *this, message_t *message) +{ + notify_payload_t *notify; + + this->child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, + this->spi, TRUE); + if (!this->child_sa) + { /* check if it is an outbound CHILD_SA */ + this->child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, + this->spi, FALSE); + if (this->child_sa) + { + /* we work only with the inbound SPI */ + this->spi = this->child_sa->get_spi(this->child_sa, TRUE); + } + } + if (!this->child_sa || + (!this->child_create && + this->child_sa->get_state(this->child_sa) != CHILD_INSTALLED) || + (this->child_create && + this->child_sa->get_state(this->child_sa) != CHILD_REKEYING)) + { + /* CHILD_SA is gone or in the wrong state, unable to rekey */ + message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); + return SUCCESS; + } + + /* 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); + + proposal = this->child_sa->get_proposal(this->child_sa); + if (proposal->get_algorithm(proposal, KEY_EXCHANGE_METHOD, + &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); + 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)); + } + + if (this->child_create->task.build(&this->child_create->task, + message) != NEED_MORE) + { + schedule_delayed_rekey(this); + message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); + return SUCCESS; + } + if (message->get_exchange_type(message) == CREATE_CHILD_SA) + { + /* don't add the notify if the CHILD_CREATE task changed the exchange */ + notify = notify_payload_create_from_protocol_and_type(PLV2_NOTIFY, + this->protocol, REKEY_SA); + notify->set_spi(notify, this->spi); + message->add_payload(message, (payload_t*)notify); + } + this->child_sa->set_state(this->child_sa, CHILD_REKEYING); + + return NEED_MORE; +} + +/** + * Find a CHILD_SA using the REKEY_SA notify */ static void find_child(private_child_rekey_t *this, message_t *message) { @@ -165,89 +354,6 @@ static void find_child(private_child_rekey_t *this, message_t *message) } } -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); - if (!this->child_sa) - { /* check if it is an outbound CHILD_SA */ - this->child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, - this->spi, FALSE); - if (this->child_sa) - { - /* we work only with the inbound SPI */ - this->spi = this->child_sa->get_spi(this->child_sa, TRUE); - } - } - if (!this->child_sa || - (!this->child_create && - this->child_sa->get_state(this->child_sa) != CHILD_INSTALLED) || - (this->child_create && - this->child_sa->get_state(this->child_sa) != CHILD_REKEYING)) - { - /* CHILD_SA is gone or in the wrong state, unable to rekey */ - 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) - { - proposal_t *proposal; - uint16_t dh_group; - - 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); - } - } - 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)); - - if (this->child_create->task.build(&this->child_create->task, - message) != NEED_MORE) - { - schedule_delayed_rekey(this); - message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); - return SUCCESS; - } - if (message->get_exchange_type(message) == CREATE_CHILD_SA) - { - /* don't add the notify if the CHILD_CREATE task changed the exchange */ - notify = notify_payload_create_from_protocol_and_type(PLV2_NOTIFY, - this->protocol, REKEY_SA); - notify->set_spi(notify, this->spi); - message->add_payload(message, (payload_t*)notify); - } - this->child_sa->set_state(this->child_sa, CHILD_REKEYING); - - return NEED_MORE; -} - METHOD(task_t, process_r, status_t, private_child_rekey_t *this, message_t *message) { @@ -259,14 +365,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 = FALSE; if (!this->child_sa) { @@ -280,123 +420,480 @@ 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; + } + if (actively_rekeying(this, &followup_sent) && followup_sent) + { + 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; } - /* let the CHILD_CREATE task build the response */ - reqid = this->child_sa->get_reqid_ref(this->child_sa); - if (reqid) + if (message->get_exchange_type(message) == CREATE_CHILD_SA) { - 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)); - 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); + 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); + } - 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); - return SUCCESS; + 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); - 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)); - - /* invoke rekey hook */ - charon->bus->child_rekey(charon->bus, this->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); + /* 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 */ + this->child_sa->set_state(this->child_sa, state); + } return SUCCESS; } /** - * Handle a rekey collision + * Check if the peer deleted the replacement SA we created while we waited for + * its completion. */ -static child_sa_t *handle_collision(private_child_rekey_t *this, - child_sa_t **to_install) +static bool is_our_replacement_deleted(private_child_rekey_t *this) { - child_sa_t *to_delete; + uint32_t spi, peer_spi; + int i; - if (this->collision->get_type(this->collision) == TASK_CHILD_REKEY) + if (!this->deleted_spis) { - chunk_t this_nonce, other_nonce; - private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + return FALSE; + } - this_nonce = this->child_create->get_lower_nonce(this->child_create); - other_nonce = other->child_create->get_lower_nonce(other->child_create); + peer_spi = this->child_create->get_other_spi(this->child_create); + if (!peer_spi) + { + return FALSE; + } - /* 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) + for (i = 0; i < array_count(this->deleted_spis); i++) + { + array_get(this->deleted_spis, i, &spi); + if (spi == peer_spi) { - child_sa_t *child_sa; + return TRUE; + } + } + return FALSE; +} - *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) +/** + * Remove the passive rekey task that's waiting for IKE_FOLLOWUP_KE requests + * that will never come if we won the collision. + */ +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); +} + +/** + * 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 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; + + 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); + + 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); + 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{%u}", (*to_delete)->get_name(*to_delete), + (*to_delete)->get_unique_id(*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) { - /* disable close action and updown event for redundant child */ - child_sa = other->child_create->get_child(other->child_create); - if (child_sa) + /* 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) { - 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); - } + 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; + } + + *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_PASSIVE_INSTALLED) + { + 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 { - 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)); + DBG1(DBG_IKE, "CHILD_SA rekey collision won, deleting old child " + "%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); } 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) + { + /* 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) { - 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)); + 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 { - 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)); + DBG1(DBG_IKE, "CHILD_SA rekey collision won, remove passive %N " + "task", task_type_names, TASK_CHILD_REKEY); } + remove_passive_rekey_task(this); } - return to_delete; + this->collision = NULL; + 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, @@ -404,7 +901,8 @@ 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, *winning_sa = NULL; + bool collision_won; if (message->get_notify(message, NO_ADDITIONAL_SAS)) { @@ -418,187 +916,248 @@ 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) { - /* bad DH group while rekeying, retry, or failure requiring deletion */ + 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, so handle + * it now */ + if (!handle_collision(this, &to_delete, &winning_sa, TRUE)) + { + /* we lost the collision. since the SA is not complete yet, we just + * abort the task */ + return SUCCESS; + } return 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 */ - if (!this->collision || - this->collision->get_type(this->collision) != TASK_CHILD_DELETE) + /* 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; } - /* check for rekey collisions */ - if (this->collision) - { - to_delete = handle_collision(this, &to_install); - } - 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; + /* 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 */ + 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 || - other_child->get_state(other_child) != CHILD_INSTALLED) - { - 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->other_child_destroyed = TRUE; - 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)); - DESTROY_IF(this->collision); + + other_child = rekey->child_create->get_child(rekey->child_create); + if (!other_child) + { + /* ignore passive tasks that did not successfully create a CHILD_SA */ + return FALSE; + } + 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; } @@ -615,10 +1174,15 @@ 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_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, @@ -632,7 +1196,11 @@ METHOD(task_t, destroy, void, { this->child_delete->task.destroy(&this->child_delete->task); } - DESTROY_IF(this->collision); + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) + { + DESTROY_IF(this->collision); + } + array_destroy(this->deleted_spis); chunk_free(&this->spi_data); free(this); } @@ -652,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/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); diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index b84c2832a..44a89f9eb 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,11 +343,12 @@ 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; ike_cfg_t *ike_cfg; + bool additional_ke = FALSE; id = this->ike_sa->get_id(this->ike_sa); @@ -333,7 +357,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 +366,25 @@ 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); } + additional_ke = additional_ke || + proposal_has_additional_ke(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)); @@ -371,29 +397,22 @@ 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); 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 && @@ -452,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; } @@ -523,6 +552,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 +660,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 +674,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: @@ -611,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; @@ -641,29 +777,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 +848,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 +928,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,26 +992,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; } - if (!this->keymat->derive_ike_keys(this->keymat, this->proposal, this->dh, - 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) { - return FALSE; + charon->bus->ike_keys(charon->bus, this->ike_sa, kes, chunk_empty, + nonce_i, nonce_r, skd.len ? old_sa : NULL, NULL, + AUTH_NONE); } - 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; + if (kes != this->kes) + { + array_destroy(kes); + } + return success; } METHOD(ike_init_t, derive_keys, status_t, @@ -825,7 +1039,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; } @@ -839,14 +1053,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, @@ -879,19 +1143,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)); @@ -904,9 +1169,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; } @@ -916,9 +1181,22 @@ 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) + { + 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; + } + else if (this->old_sa) { /* during rekeying, we derive keys here directly */ if (derive_keys(this) != SUCCESS) @@ -1015,6 +1293,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) { @@ -1035,16 +1333,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); } @@ -1121,29 +1419,40 @@ 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) + { + 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; + } + else if (this->old_sa) { /* during rekeying, we derive keys here directly */ return derive_keys(this); @@ -1163,24 +1472,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); } @@ -1217,7 +1528,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, 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, 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; } diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 6629cb378..b61f31c7c 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 @@ -59,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, @@ -76,7 +79,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 +88,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 +97,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); @@ -126,6 +129,122 @@ START_TEST(test_regular) /* child_updown */ assert_hook(); + assert_track_sas(2, 2); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +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; + + assert_track_sas_start(); + + 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_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, 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_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); + 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_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); + 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(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -150,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, @@ -185,107 +306,308 @@ 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); - 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 } */ - 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); - 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 } --> */ - 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); - 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); /* 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, 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_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, 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(); + assert_track_sas(2, 2); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +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; + + assert_track_sas_start(); + + 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_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, 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_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); + 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_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); + 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_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, 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_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(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -300,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); @@ -309,7 +633,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); @@ -318,7 +642,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); @@ -326,14 +650,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); @@ -342,6 +665,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); @@ -363,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); @@ -378,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); @@ -387,7 +717,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); @@ -396,7 +726,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); @@ -404,17 +734,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); @@ -440,6 +772,317 @@ 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); +} +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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 0); + + 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; + + assert_track_sas_start(); + + 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(); + /* 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); + + 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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -454,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); @@ -495,7 +1140,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); @@ -503,7 +1148,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); @@ -512,17 +1157,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 @@ -534,10 +1176,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) { @@ -545,11 +1187,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 @@ -561,19 +1201,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, @@ -587,13 +1236,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, @@ -607,7 +1266,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); @@ -648,6 +1310,476 @@ 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); +} +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; + + assert_track_sas_start(); + + 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_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, 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_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); + 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(); + } + + 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); + 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(); + 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); + 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(); + 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); + 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(); + 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); + 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(); + 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); + assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + assert_track_sas(2, 2); + + 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; + + 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); + + 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_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(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ + 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_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); + + 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_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_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_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); + 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(); + 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_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(); + } + + 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); + 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(); + 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); + 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(); + assert_hook(); + + 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, 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); + + 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); + 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(); + assert_hook(); + + 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, 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_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); + assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -675,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); @@ -712,7 +1846,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); @@ -720,7 +1854,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); @@ -735,56 +1869,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); @@ -804,35 +1941,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, @@ -843,6 +1990,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); @@ -852,9 +2002,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 */ @@ -870,6 +2020,468 @@ 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); +} +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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 0); + + 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: + * 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; + + assert_track_sas_start(); + + 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_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(); + + /* 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(); + + 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_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); + 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) */ + 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 */ + 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(); + assert_track_sas(2, 2); call_ikesa(a, destroy); call_ikesa(b, destroy); @@ -886,14 +2498,29 @@ 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; + + assert_track_sas_start(); exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -929,7 +2556,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); @@ -943,32 +2570,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); @@ -990,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); @@ -1018,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); @@ -1052,7 +2724,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); @@ -1066,17 +2738,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); @@ -1093,8 +2768,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); @@ -1113,6 +2789,351 @@ 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); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +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; + + assert_track_sas_start(); + + 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_track_sas(2, 0); + + 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: + * + * 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, similar to the non-multi-KE scenario above. + */ +START_TEST(test_collision_delayed_request_multi_ke) +{ + ike_sa_t *a, *b; + message_t *msg; + bool after_delete = _i >= 3; + + _i %= 3; + + assert_track_sas_start(); + + 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_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); + 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(); + + /* <-- INFORMATIONAL { D } */ + 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); + 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_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); + 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_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -1139,20 +3160,22 @@ 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); /* 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 +3188,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 */ @@ -1179,127 +3202,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, 9); 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_hook(); + assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); /* <-- 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); 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); + + /* 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); @@ -1336,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); @@ -1362,19 +3400,21 @@ 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); /* 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 @@ -1398,98 +3438,101 @@ 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, 9); 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); + + /* child_rekey */ 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(); + + /* 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_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); - /* 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); - assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); - assert_child_sa_state(b, 8, 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_count(a, 2); - assert_scheduler(); - - /* <-- 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, 9, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - 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, 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(); assert_hook(); + assert_track_sas(2, 2); assert_sa_idle(a); assert_sa_idle(b); @@ -1499,6 +3542,150 @@ 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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); + + 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. @@ -1515,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, @@ -1551,18 +3740,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(); @@ -1574,6 +3766,100 @@ START_TEST(test_collision_delete) /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); + + 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 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; + + assert_track_sas_start(); + + 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_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_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_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(); + + /* 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_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -1602,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, @@ -1660,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); @@ -1681,12 +3970,14 @@ 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; 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, @@ -1714,11 +4005,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 } --> */ @@ -1739,15 +4031,17 @@ 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(); /* child_rekey */ assert_hook(); + assert_track_sas(2, 0); assert_sa_idle(a); assert_sa_idle(b); @@ -1758,26 +4052,136 @@ 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; + assert_track_sas_start(); + + 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_track_sas(2, 0); + + 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 @@ -1795,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, @@ -1851,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); @@ -1878,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, @@ -1938,6 +4347,7 @@ START_TEST(test_collision_ike_delete) /* child_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST @@ -1950,24 +4360,38 @@ 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); + 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"); 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_request, 0, 3); + 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, 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"); 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); + tcase_add_loop_test(tc, test_collision_delete_delayed_response, 0, 2); suite_add_tcase(s, tc); tc = tcase_create("collisions ike rekey"); diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index efc27542a..c6691acf4 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 * @@ -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,114 @@ START_TEST(test_regular) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +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; + + assert_track_sas_start(); + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &multi_ke_conf); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + } + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { 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(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -121,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) @@ -199,6 +311,130 @@ 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); +} +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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -213,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); @@ -277,11 +515,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 +534,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); @@ -354,6 +592,388 @@ START_TEST(test_collision) assert_hook(); assert_hook(); assert_hook(); + assert_track_sas(2, 2); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * Both peers initiate the IKE_SA rekeying 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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); + + 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; + + 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); + + 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(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -377,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); @@ -566,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); } @@ -588,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); @@ -726,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); } @@ -767,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); @@ -946,6 +1574,211 @@ 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); +} +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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -969,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); @@ -1048,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); } @@ -1074,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); @@ -1163,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); } @@ -1189,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); @@ -1283,6 +2124,183 @@ 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); +} +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; + + assert_track_sas_start(); + + 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(); + assert_track_sas(2, 2); charon->ike_sa_manager->flush(charon->ike_sa_manager); } @@ -1305,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, @@ -1367,6 +2387,95 @@ START_TEST(test_collision_delete) /* ike_rekey */ assert_hook(); + assert_track_sas(0, 0); +} +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; + + assert_track_sas_start(); + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &multi_ke_conf); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &multi_ke_conf); + } + /* 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(); + assert_track_sas(0, 0); } END_TEST @@ -1387,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, @@ -1453,6 +2564,7 @@ START_TEST(test_collision_delete_drop_delete) /* ike_rekey */ assert_hook(); + assert_track_sas(0, 0); } END_TEST @@ -1465,21 +2577,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_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 */ diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 8c57c8877..42530be44 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 { @@ -265,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); @@ -282,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 */ diff --git a/src/libstrongswan/crypto/key_exchange.c b/src/libstrongswan/crypto/key_exchange.c index d672ec7e8..b3fb9641f 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 * @@ -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); } /* @@ -619,3 +694,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..4aa4e264b 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. @@ -177,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. @@ -209,4 +215,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_ @}*/ diff --git a/src/libstrongswan/crypto/proposal/proposal.c b/src/libstrongswan/crypto/proposal/proposal.c index 1ad04db4e..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,15 +317,16 @@ 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; bool found = FALSE, optional = FALSE; - if (type == KEY_EXCHANGE_METHOD) + 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); @@ -358,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)) @@ -389,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. @@ -397,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++) { @@ -411,11 +437,12 @@ 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; } - 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 @@ -426,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)) { @@ -441,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; } @@ -604,7 +637,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; } @@ -849,7 +882,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 +893,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) { @@ -1420,3 +1458,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/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..6e6594fc2 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, @@ -57,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. */ 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; } diff --git a/src/libstrongswan/tests/suites/test_proposal.c b/src/libstrongswan/tests/suites/test_proposal.c index c42f9e4a6..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) @@ -474,6 +496,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 +611,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); 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"); 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