From 2f3af8759fe6827a768e2aa532df587334121bb7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 6 Apr 2020 17:41:15 +0200 Subject: [PATCH] ike-rekey: Support IKE_SA rekeying with multiple key exchanges --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 497 +++++++++++++++++++---- 1 file changed, 415 insertions(+), 82 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index c5b277767..9ea11085e 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 * HSR Hochschule fuer Technik Rapperswil @@ -25,7 +25,6 @@ #include #include - typedef struct private_ike_rekey_t private_ike_rekey_t; /** @@ -64,14 +63,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; }; /** @@ -160,10 +194,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) @@ -187,9 +235,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; } /** @@ -230,19 +278,110 @@ 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 (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; } @@ -262,11 +401,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 */ @@ -275,17 +419,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*) @@ -296,7 +466,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 @@ -304,18 +474,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) { @@ -329,78 +655,66 @@ 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 DH group or QSKE mechanism, 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, later we just respond with TEMPORARY_FAILURE and ignore + * the passive task */ + return collision_lost(this, TRUE) ? SUCCESS : NEED_MORE; default: + charon->bus->set_sa(charon->bus, this->ike_sa); break; } - if (this->collision) + /* there won't be a collision here if this task is for a multi-KE rekeying, + * as a collision during CREATE_CHILD_SA was cleaned up above */ + 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; @@ -430,7 +744,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: { @@ -440,11 +754,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; } - DESTROY_IF(&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 */ @@ -471,7 +796,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); - DESTROY_IF(&this->collision->public.task); + /* 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, @@ -483,6 +815,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,