ike-rekey: Support IKE_SA rekeying with multiple key exchanges

This commit is contained in:
Tobias Brunner 2020-04-06 17:41:15 +02:00
parent 273b4258f1
commit 2f3af8759f
1 changed files with 415 additions and 82 deletions

View File

@ -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 <processing/jobs/rekey_ike_sa_job.h>
#include <processing/jobs/initiate_tasks_job.h>
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,