/*
 * Copyright (C) 2008-2019 Tobias Brunner
 * Copyright (C) 2005-2008 Martin Willi
 * Copyright (C) 2005 Jan Hutter
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#include "ike_init.h"

#include <string.h>

#include <daemon.h>
#include <bio/bio_reader.h>
#include <bio/bio_writer.h>
#include <sa/ikev2/keymat_v2.h>
#include <crypto/key_exchange.h>
#include <crypto/hashers/hash_algorithm_set.h>
#include <encoding/payloads/sa_payload.h>
#include <encoding/payloads/ke_payload.h>
#include <encoding/payloads/nonce_payload.h>

/** maximum retries to do with cookies/other dh groups */
#define MAX_RETRIES 5

typedef struct private_ike_init_t private_ike_init_t;

/**
 * Private members of a ike_init_t task.
 */
struct private_ike_init_t {

	/**
	 * Public methods and task_t interface.
	 */
	ike_init_t public;

	/**
	 * Assigned IKE_SA.
	 */
	ike_sa_t *ike_sa;

	/**
	 * Are we the initiator?
	 */
	bool initiator;

	/**
	 * Whether the key exchange is done
	 */
	bool ke_done;

	/**
	 * Whether keys have already been derived
	 */
	bool ke_derived;

	/**
	 * diffie hellman group to use
	 */
	key_exchange_method_t dh_group;

	/**
	 * diffie hellman key exchange
	 */
	key_exchange_t *dh;

	/**
	 * Applying DH public value failed?
	 */
	bool dh_failed;

	/**
	 * Keymat derivation (from IKE_SA)
	 */
	keymat_v2_t *keymat;

	/**
	 * nonce chosen by us
	 */
	chunk_t my_nonce;

	/**
	 * nonce chosen by peer
	 */
	chunk_t other_nonce;

	/**
	 * nonce generator
	 */
	nonce_gen_t *nonceg;

	/**
	 * Negotiated proposal used for IKE_SA
	 */
	proposal_t *proposal;

	/**
	 * Old IKE_SA which gets rekeyed
	 */
	ike_sa_t *old_sa;

	/**
	 * cookie received from responder
	 */
	chunk_t cookie;

	/**
	 * retries done so far after failure (cookie or bad dh group)
	 */
	u_int retry;

	/**
	 * Whether to use Signature Authentication as per RFC 7427
	 */
	bool signature_authentication;

	/**
	 * Whether to follow IKEv2 redirects as per RFC 5685
	 */
	bool follow_redirects;
};

/**
 * Allocate our own nonce value
 */
static bool generate_nonce(private_ike_init_t *this)
{
	if (!this->nonceg)
	{
		DBG1(DBG_IKE, "no nonce generator found to create nonce");
		return FALSE;
	}
	if (!this->nonceg->allocate_nonce(this->nonceg, NONCE_SIZE,
									  &this->my_nonce))
	{
		DBG1(DBG_IKE, "nonce allocation failed");
		return FALSE;
	}
	return TRUE;
}

/**
 * Notify the peer about the hash algorithms we support or expect,
 * as per RFC 7427
 */
static void send_supported_hash_algorithms(private_ike_init_t *this,
										   message_t *message)
{
	hash_algorithm_set_t *algos;
	enumerator_t *enumerator, *rounds;
	bio_writer_t *writer;
	hash_algorithm_t hash;
	peer_cfg_t *peer;
	auth_cfg_t *auth;
	auth_rule_t rule;
	signature_params_t *config;
	int written;
	size_t len = BUF_LEN;
	char buf[len];
	char *pos = buf;
	char *plugin_name;

	algos = hash_algorithm_set_create();
	peer = this->ike_sa->get_peer_cfg(this->ike_sa);
	if (peer)
	{
		rounds = peer->create_auth_cfg_enumerator(peer, FALSE);
		while (rounds->enumerate(rounds, &auth))
		{
			enumerator = auth->create_enumerator(auth);
			while (enumerator->enumerate(enumerator, &rule, &config))
			{
				if (rule == AUTH_RULE_IKE_SIGNATURE_SCHEME)
				{
					hash = hasher_from_signature_scheme(config->scheme,
														config->params);
					if (hasher_algorithm_for_ikev2(hash))
					{
						algos->add(algos, hash);
					}
				}
			}
			enumerator->destroy(enumerator);
		}
		rounds->destroy(rounds);
	}

	if (!algos->count(algos))
	{
		enumerator = lib->crypto->create_hasher_enumerator(lib->crypto);
		while (enumerator->enumerate(enumerator, &hash, &plugin_name))
		{
			if (hasher_algorithm_for_ikev2(hash))
			{
				algos->add(algos, hash);
			}
		}
		enumerator->destroy(enumerator);
	}

	if (algos->count(algos))
	{
		writer = bio_writer_create(0);
		enumerator = algos->create_enumerator(algos);
		while (enumerator->enumerate(enumerator, &hash))
		{
			writer->write_uint16(writer, hash);

			/* generate debug output */
			written = snprintf(pos, len, " %N", hash_algorithm_short_names,
							   hash);
			if (written > 0 && written < len)
			{
				pos += written;
				len -= written;
			}
		}
		enumerator->destroy(enumerator);
		message->add_notify(message, FALSE, SIGNATURE_HASH_ALGORITHMS,
							writer->get_buf(writer));
		writer->destroy(writer);

		*pos = '\0';
		DBG2(DBG_CFG, "sending supported signature hash algorithms:%s", buf);
	}
	algos->destroy(algos);
}

/**
 * Store algorithms supported by other peer
 */
static void handle_supported_hash_algorithms(private_ike_init_t *this,
											 notify_payload_t *notify)
{
	bio_reader_t *reader;
	uint16_t algo;
	int written;
	size_t len = BUF_LEN;
	char buf[len];
	char *pos = buf;
	bool added = FALSE;

	reader = bio_reader_create(notify->get_notification_data(notify));
	while (reader->remaining(reader) >= 2 && reader->read_uint16(reader, &algo))
	{
		if (hasher_algorithm_for_ikev2(algo))
		{
			this->keymat->add_hash_algorithm(this->keymat, algo);
			added = TRUE;

			/* generate debug output */
			written = snprintf(pos, len, " %N", hash_algorithm_short_names,
							   algo);
			if (written > 0 && written < len)
			{
				pos += written;
				len -= written;
			}
		}
	}
	reader->destroy(reader);

	*pos = '\0';
	DBG2(DBG_CFG, "received supported signature hash algorithms:%s", buf);

	if (added)
	{
		this->ike_sa->enable_extension(this->ike_sa, EXT_SIGNATURE_AUTH);
	}
}

/**
 * Check whether to send a USE_PPK notify
 */
static bool send_use_ppk(private_ike_init_t *this)
{
	peer_cfg_t *peer;
	enumerator_t *keys;
	shared_key_t *key;
	bool use_ppk = FALSE;

	if (this->initiator)
	{
		peer = this->ike_sa->get_peer_cfg(this->ike_sa);
		if (peer->get_ppk_id(peer))
		{
			use_ppk = TRUE;
		}
	}
	else if (this->ike_sa->supports_extension(this->ike_sa, EXT_PPK))
	{
		/* check if we have at least one PPK available */
		keys = lib->credmgr->create_shared_enumerator(lib->credmgr, SHARED_PPK,
													  NULL, NULL);
		if (keys->enumerate(keys, &key, NULL, NULL))
		{
			use_ppk = TRUE;
		}
		keys->destroy(keys);
	}
	return use_ppk;
}

/**
 * build the payloads for the message
 */
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;
	ike_sa_id_t *id;
	proposal_t *proposal;
	enumerator_t *enumerator;
	ike_cfg_t *ike_cfg;

	id = this->ike_sa->get_id(this->ike_sa);

	ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa);

	if (this->initiator)
	{
		proposal_list = ike_cfg->get_proposals(ike_cfg);
		other_dh_groups = linked_list_create();
		enumerator = proposal_list->create_enumerator(proposal_list);
		while (enumerator->enumerate(enumerator, (void**)&proposal))
		{
			/* include SPI of new IKE_SA when we are rekeying */
			if (this->old_sa)
			{
				proposal->set_spi(proposal, id->get_initiator_spi(id));
			}
			/* move the selected DH group to the front of the proposal */
			if (!proposal->promote_transform(proposal, KEY_EXCHANGE_METHOD,
											 this->dh_group))
			{	/* 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);
			}
		}
		enumerator->destroy(enumerator);
		/* add proposals that don't contain the selected group */
		enumerator = other_dh_groups->create_enumerator(other_dh_groups);
		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);

		sa_payload = sa_payload_create_from_proposals_v2(proposal_list);
		proposal_list->destroy_offset(proposal_list, offsetof(proposal_t, destroy));
	}
	else
	{
		if (this->old_sa)
		{
			/* include SPI of new IKE_SA when we are rekeying */
			this->proposal->set_spi(this->proposal, id->get_responder_spi(id));
		}
		sa_payload = sa_payload_create_from_proposal_v2(this->proposal);
	}
	message->add_payload(message, (payload_t*)sa_payload);

	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;
	}
	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);
	}

	/* negotiate fragmentation if we are not rekeying */
	if (!this->old_sa &&
		 ike_cfg->fragmentation(ike_cfg) != FRAGMENTATION_NO)
	{
		if (this->initiator ||
			this->ike_sa->supports_extension(this->ike_sa,
											 EXT_IKE_FRAGMENTATION))
		{
			message->add_notify(message, FALSE, FRAGMENTATION_SUPPORTED,
								chunk_empty);
		}
	}
	/* submit supported hash algorithms for signature authentication */
	if (!this->old_sa && this->signature_authentication)
	{
		if (this->initiator ||
			this->ike_sa->supports_extension(this->ike_sa,
											 EXT_SIGNATURE_AUTH))
		{
			send_supported_hash_algorithms(this, message);
		}
	}
	/* notify other peer if we support redirection */
	if (!this->old_sa && this->initiator && this->follow_redirects)
	{
		identification_t *gateway;
		host_t *from;
		chunk_t data;

		from = this->ike_sa->get_redirected_from(this->ike_sa);
		if (from)
		{
			gateway = identification_create_from_sockaddr(
													from->get_sockaddr(from));
			data = redirect_data_create(gateway, chunk_empty);
			message->add_notify(message, FALSE, REDIRECTED_FROM, data);
			chunk_free(&data);
			gateway->destroy(gateway);
		}
		else
		{
			message->add_notify(message, FALSE, REDIRECT_SUPPORTED,
								chunk_empty);
		}
	}
	/* notify the peer if we want to use/support PPK */
	if (!this->old_sa && send_use_ppk(this))
	{
		message->add_notify(message, FALSE, USE_PPK, chunk_empty);
	}
	/* notify the peer if we accept childless IKE_SAs */
	if (!this->old_sa && !this->initiator &&
		 ike_cfg->childless(ike_cfg) != CHILDLESS_NEVER)
	{
		message->add_notify(message, FALSE, CHILDLESS_IKEV2_SUPPORTED,
							chunk_empty);
	}
	return TRUE;
}

/**
 * Process the SA payload and select a proposal
 */
static void process_sa_payload(private_ike_init_t *this, message_t *message,
							   sa_payload_t *sa_payload)
{
	ike_cfg_t *ike_cfg, *cfg, *alt_cfg = NULL;
	enumerator_t *enumerator;
	linked_list_t *proposal_list;
	host_t *me, *other;
	proposal_selection_flag_t flags = 0;

	ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa);

	proposal_list = sa_payload->get_proposals(sa_payload);
	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 = ike_cfg->select_proposal(ike_cfg, proposal_list, flags);
	if (!this->proposal)
	{
		if (!this->initiator && !this->old_sa)
		{
			me = message->get_destination(message);
			other = message->get_source(message);
			enumerator = charon->backends->create_ike_cfg_enumerator(
											charon->backends, me, other, IKEV2);
			while (enumerator->enumerate(enumerator, &cfg))
			{
				if (ike_cfg == cfg)
				{	/* already tried and failed */
					continue;
				}
				DBG1(DBG_IKE, "no matching proposal found, trying alternative "
					 "config");
				this->proposal = cfg->select_proposal(cfg, proposal_list,
													  flags);
				if (this->proposal)
				{
					alt_cfg = cfg->get_ref(cfg);
					break;
				}
			}
			enumerator->destroy(enumerator);
		}
		if (alt_cfg)
		{
			this->ike_sa->set_ike_cfg(this->ike_sa, alt_cfg);
			alt_cfg->destroy(alt_cfg);
		}
		else
		{
			charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_IKE,
							   proposal_list);
		}
	}
	proposal_list->destroy_offset(proposal_list,
								  offsetof(proposal_t, destroy));
}

/**
 * Read payloads from message
 */
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;

	enumerator = message->create_payload_enumerator(message);
	while (enumerator->enumerate(enumerator, &payload))
	{
		switch (payload->get_type(payload))
		{
			case PLV2_SECURITY_ASSOCIATION:
			{
				process_sa_payload(this, message, (sa_payload_t*)payload);
				break;
			}
			case PLV2_KEY_EXCHANGE:
			{
				ke_payload = (ke_payload_t*)payload;

				this->dh_group = ke_payload->get_key_exchange_method(ke_payload);
				break;
			}
			case PLV2_NONCE:
			{
				nonce_payload_t *nonce_payload = (nonce_payload_t*)payload;

				this->other_nonce = nonce_payload->get_nonce(nonce_payload);
				break;
			}
			case PLV2_NOTIFY:
			{
				notify_payload_t *notify = (notify_payload_t*)payload;

				switch (notify->get_notify_type(notify))
				{
					case FRAGMENTATION_SUPPORTED:
						this->ike_sa->enable_extension(this->ike_sa,
													   EXT_IKE_FRAGMENTATION);
						break;
					case SIGNATURE_HASH_ALGORITHMS:
						if (this->signature_authentication)
						{
							handle_supported_hash_algorithms(this, notify);
						}
						break;
					case USE_PPK:
						if (!this->old_sa)
						{
							this->ike_sa->enable_extension(this->ike_sa,
														   EXT_PPK);
						}
						break;
					case REDIRECTED_FROM:
					{
						identification_t *gateway;
						chunk_t data;

						data = notify->get_notification_data(notify);
						gateway = redirect_data_parse(data, NULL);
						if (!gateway)
						{
							DBG1(DBG_IKE, "received invalid REDIRECTED_FROM "
								 "notify, ignored");
							break;
						}
						DBG1(DBG_IKE, "client got redirected from %Y", gateway);
						gateway->destroy(gateway);
						/* fall-through */
					}
					case REDIRECT_SUPPORTED:
						if (!this->old_sa)
						{
							this->ike_sa->enable_extension(this->ike_sa,
														   EXT_IKE_REDIRECTION);
						}
						break;
					case CHILDLESS_IKEV2_SUPPORTED:
						if (this->initiator && !this->old_sa)
						{
							this->ike_sa->enable_extension(this->ike_sa,
														   EXT_IKE_CHILDLESS);
						}
						break;
					default:
						/* other notifies are handled elsewhere */
						break;
				}

			}
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (this->proposal)
	{
		this->ike_sa->set_proposal(this->ike_sa, this->proposal);

		if (this->old_sa)
		{	/* retrieve SPI of new IKE_SA when rekeying */
			id = this->ike_sa->get_id(this->ike_sa);
			if (this->initiator)
			{
				id->set_responder_spi(id,
									  this->proposal->get_spi(this->proposal));
			}
			else
			{
				id->set_initiator_spi(id,
									  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)
		{
			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));
		}
	}
}

METHOD(task_t, build_i, status_t,
	private_ike_init_t *this, message_t *message)
{
	ike_cfg_t *ike_cfg;

	ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa);

	DBG0(DBG_IKE, "initiating IKE_SA %s[%d] to %H",
		 this->ike_sa->get_name(this->ike_sa),
		 this->ike_sa->get_unique_id(this->ike_sa),
		 this->ike_sa->get_other_host(this->ike_sa));
	this->ike_sa->set_state(this->ike_sa, IKE_CONNECTING);

	if (this->retry >= MAX_RETRIES)
	{
		DBG1(DBG_IKE, "giving up after %d retries", MAX_RETRIES);
		return FAILED;
	}

	/* if we are retrying after an INVALID_KE_PAYLOAD we already have one */
	if (!this->dh)
	{
		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 */
			proposal_t *proposal;
			uint16_t dh_group;

			proposal = this->old_sa->get_proposal(this->old_sa);
			if (proposal->get_algorithm(proposal, KEY_EXCHANGE_METHOD,
										&dh_group, NULL))
			{
				this->dh_group = dh_group;
			}
			else
			{	/* this shouldn't happen, but let's be safe */
				this->dh_group = ike_cfg->get_algorithm(ike_cfg,
														KEY_EXCHANGE_METHOD);
			}
		}
		else
		{
			this->dh_group = 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)
		{
			DBG1(DBG_IKE, "configured DH group %N not supported",
				key_exchange_method_names, this->dh_group);
			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)
		{
			DBG1(DBG_IKE, "requested DH group %N not supported",
				 key_exchange_method_names, this->dh_group);
			return FAILED;
		}
	}

	/* generate nonce only when we are trying the first time */
	if (this->my_nonce.ptr == NULL)
	{
		if (!generate_nonce(this))
		{
			return FAILED;
		}
	}

	if (this->cookie.ptr)
	{
		message->add_notify(message, FALSE, COOKIE, this->cookie);
	}

	if (!build_payloads(this, message))
	{
		return FAILED;
	}

#ifdef ME
	{
		chunk_t connect_id = this->ike_sa->get_connect_id(this->ike_sa);
		if (connect_id.ptr)
		{
			message->add_notify(message, FALSE, ME_CONNECTID, connect_id);
		}
	}
#endif /* ME */

	return NEED_MORE;
}

METHOD(task_t, process_r,  status_t,
	private_ike_init_t *this, message_t *message)
{
	DBG0(DBG_IKE, "%H is initiating an IKE_SA", message->get_source(message));
	this->ike_sa->set_state(this->ike_sa, IKE_CONNECTING);

	if (!generate_nonce(this))
	{
		return FAILED;
	}

#ifdef ME
	{
		notify_payload_t *notify = message->get_notify(message, ME_CONNECTID);
		if (notify)
		{
			chunk_t connect_id = notify->get_notification_data(notify);
			DBG2(DBG_IKE, "received ME_CONNECTID %#B", &connect_id);
			charon->connect_manager->stop_checks(charon->connect_manager,
												 connect_id);
		}
	}
#endif /* ME */

	process_payloads(this, message);

	return NEED_MORE;
}

/**
 * Derive the keymat for the IKE_SA
 */
static bool derive_keys_internal(private_ike_init_t *this, chunk_t nonce_i,
								 chunk_t nonce_r)
{
	keymat_v2_t *old_keymat;
	pseudo_random_function_t prf_alg = PRF_UNDEFINED;
	chunk_t skd = chunk_empty;
	ike_sa_id_t *id;

	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 (!this->keymat->derive_ike_keys(this->keymat, this->proposal, this->dh,
									   nonce_i, nonce_r, id, prf_alg, skd))
	{
		return FALSE;
	}
	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;
}

METHOD(ike_init_t, derive_keys, status_t,
	private_ike_init_t *this)
{
	bool success;

	if (!this->ke_done || this->ke_derived)
	{
		return NEED_MORE;
	}

	if (this->initiator)
	{
		success = derive_keys_internal(this, this->my_nonce, this->other_nonce);
	}
	else
	{
		success = derive_keys_internal(this, this->other_nonce, this->my_nonce);
	}

	this->ke_derived = TRUE;

	if (!success)
	{
		DBG1(DBG_IKE, "key derivation failed");
		return FAILED;
	}
	return SUCCESS;
}

METHOD(task_t, build_r, status_t,
	private_ike_init_t *this, message_t *message)
{
	identification_t *gateway;

	/* check if we have everything we need */
	if (this->proposal == NULL ||
		this->other_nonce.len == 0 || this->my_nonce.len == 0)
	{
		DBG1(DBG_IKE, "received proposals unacceptable");
		message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty);
		return FAILED;
	}

	/* check if we'd have to redirect the client */
	if (!this->old_sa &&
		this->ike_sa->supports_extension(this->ike_sa, EXT_IKE_REDIRECTION) &&
		charon->redirect->redirect_on_init(charon->redirect, this->ike_sa,
										   &gateway))
	{
		chunk_t data;

		DBG1(DBG_IKE, "redirecting peer to %Y", gateway);
		data = redirect_data_create(gateway, this->other_nonce);
		message->add_notify(message, TRUE, REDIRECT, data);
		gateway->destroy(gateway);
		chunk_free(&data);
		return FAILED;
	}

	if (this->dh == NULL ||
		!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;
			group = htons(group);
			message->add_notify(message, FALSE, INVALID_KE_PAYLOAD,
								chunk_from_thing(group));
		}
		else
		{
			DBG1(DBG_IKE, "no acceptable proposal found");
			message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty);
		}
		return FAILED;
	}

	if (this->dh_failed)
	{
		DBG1(DBG_IKE, "applying DH public value failed");
		message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty);
		return FAILED;
	}

	if (!build_payloads(this, message))
	{
		message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty);
		return FAILED;
	}
	this->ke_done = TRUE;

	if (this->old_sa)
	{
		/* during rekeying, we derive keys here directly */
		if (derive_keys(this) != SUCCESS)
		{
			message->add_notify(message, FALSE, NO_PROPOSAL_CHOSEN, chunk_empty);
			return FAILED;
		}
		return SUCCESS;
	}
	/* key derivation is done before the next request is processed */
	return NEED_MORE;
}

/**
 * Raise alerts for received notify errors
 */
static void raise_alerts(private_ike_init_t *this, notify_type_t type)
{
	ike_cfg_t *ike_cfg;
	linked_list_t *list;

	switch (type)
	{
		case NO_PROPOSAL_CHOSEN:
			ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa);
			list = ike_cfg->get_proposals(ike_cfg);
			charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_IKE, list);
			list->destroy_offset(list, offsetof(proposal_t, destroy));
			break;
		default:
			break;
	}
}

METHOD(task_t, pre_process_i, status_t,
	private_ike_init_t *this, message_t *message)
{
	enumerator_t *enumerator;
	payload_t *payload;

	/* check for erroneous notifies */
	enumerator = message->create_payload_enumerator(message);
	while (enumerator->enumerate(enumerator, &payload))
	{
		if (payload->get_type(payload) == PLV2_NOTIFY)
		{
			notify_payload_t *notify = (notify_payload_t*)payload;
			notify_type_t type = notify->get_notify_type(notify);

			switch (type)
			{
				case COOKIE:
				{
					chunk_t cookie;

					cookie = notify->get_notification_data(notify);
					if (chunk_equals(cookie, this->cookie))
					{
						DBG1(DBG_IKE, "ignore response with duplicate COOKIE "
							 "notify");
						enumerator->destroy(enumerator);
						return FAILED;
					}
					break;
				}
				case REDIRECT:
				{
					identification_t *gateway;
					chunk_t data, nonce = chunk_empty;
					status_t status = SUCCESS;

					if (this->old_sa)
					{
						break;
					}
					data = notify->get_notification_data(notify);
					gateway = redirect_data_parse(data, &nonce);
					if (!gateway || !chunk_equals(nonce, this->my_nonce))
					{
						DBG1(DBG_IKE, "received invalid REDIRECT notify");
						status = FAILED;
					}
					DESTROY_IF(gateway);
					chunk_free(&nonce);
					enumerator->destroy(enumerator);
					return status;
				}
				default:
					break;
			}
		}
	}
	enumerator->destroy(enumerator);
	return SUCCESS;
}

METHOD(task_t, process_i, status_t,
	private_ike_init_t *this, message_t *message)
{
	enumerator_t *enumerator;
	payload_t *payload;

	/* check for erroneous notifies */
	enumerator = message->create_payload_enumerator(message);
	while (enumerator->enumerate(enumerator, &payload))
	{
		if (payload->get_type(payload) == PLV2_NOTIFY)
		{
			notify_payload_t *notify = (notify_payload_t*)payload;
			notify_type_t type = notify->get_notify_type(notify);

			switch (type)
			{
				case INVALID_KE_PAYLOAD:
				{
					chunk_t data;
					key_exchange_method_t bad_group;

					bad_group = this->dh_group;
					data = notify->get_notification_data(notify);
					this->dh_group = ntohs(*((uint16_t*)data.ptr));
					DBG1(DBG_IKE, "peer didn't accept DH group %N, "
						 "it requested %N", key_exchange_method_names,
						 bad_group, key_exchange_method_names, this->dh_group);

					if (this->old_sa == NULL)
					{	/* reset the IKE_SA if we are not rekeying */
						this->ike_sa->reset(this->ike_sa, FALSE);
					}

					enumerator->destroy(enumerator);
					this->retry++;
					return NEED_MORE;
				}
				case NAT_DETECTION_SOURCE_IP:
				case NAT_DETECTION_DESTINATION_IP:
					/* skip, handled in ike_natd_t */
					break;
				case MULTIPLE_AUTH_SUPPORTED:
					/* handled in ike_auth_t */
					break;
				case COOKIE:
				{
					if (this->old_sa)
					{
						DBG1(DBG_IKE, "received COOKIE notify during rekeying"
						     ", ignored");
						break;
					}
					chunk_free(&this->cookie);
					this->cookie = chunk_clone(notify->get_notification_data(notify));
					this->ike_sa->reset(this->ike_sa, FALSE);
					enumerator->destroy(enumerator);
					DBG2(DBG_IKE, "received %N notify", notify_type_names, type);
					this->retry++;
					return NEED_MORE;
				}
				case REDIRECT:
				{
					identification_t *gateway;
					chunk_t data, nonce = chunk_empty;
					status_t status = FAILED;

					if (this->old_sa)
					{
						DBG1(DBG_IKE, "received REDIRECT notify during rekeying"
						     ", ignored");
						break;
					}
					data = notify->get_notification_data(notify);
					gateway = redirect_data_parse(data, &nonce);
					if (this->ike_sa->handle_redirect(this->ike_sa, gateway))
					{
						status = NEED_MORE;
					}
					DESTROY_IF(gateway);
					chunk_free(&nonce);
					enumerator->destroy(enumerator);
					return status;
				}
				default:
				{
					if (type <= 16383)
					{
						DBG1(DBG_IKE, "received %N notify error",
							 notify_type_names, type);
						enumerator->destroy(enumerator);
						raise_alerts(this, type);
						return FAILED;
					}
					DBG2(DBG_IKE, "received %N notify",
						notify_type_names, type);
					break;
				}
			}
		}
	}
	enumerator->destroy(enumerator);

	process_payloads(this, message);

	/* check if we have everything */
	if (this->proposal == NULL ||
		this->other_nonce.len == 0 || this->my_nonce.len == 0)
	{
		DBG1(DBG_IKE, "peers proposal selection invalid");
		return FAILED;
	}

	if (this->dh == NULL ||
		!this->proposal->has_transform(this->proposal, KEY_EXCHANGE_METHOD,
									   this->dh_group))
	{
		DBG1(DBG_IKE, "peer DH group selection invalid");
		return FAILED;
	}

	if (this->dh_failed)
	{
		DBG1(DBG_IKE, "applying DH public value failed");
		return FAILED;
	}
	this->ke_done = TRUE;

	if (this->old_sa)
	{
		/* during rekeying, we derive keys here directly */
		return derive_keys(this);
	}
	/* key derivation is done before we send the next message */
	return NEED_MORE;
}

METHOD(task_t, get_type, task_type_t,
	private_ike_init_t *this)
{
	return TASK_IKE_INIT;
}

METHOD(task_t, migrate, void,
	private_ike_init_t *this, ike_sa_t *ike_sa)
{
	DESTROY_IF(this->proposal);
	chunk_free(&this->other_nonce);
	this->ke_done = FALSE;
	this->ke_derived = FALSE;

	this->ike_sa = ike_sa;
	this->keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa);
	this->proposal = NULL;
	this->dh_failed = FALSE;
}

METHOD(task_t, destroy, void,
	private_ike_init_t *this)
{
	DESTROY_IF(this->dh);
	DESTROY_IF(this->proposal);
	DESTROY_IF(this->nonceg);
	chunk_free(&this->my_nonce);
	chunk_free(&this->other_nonce);
	chunk_free(&this->cookie);
	free(this);
}

METHOD(ike_init_t, get_lower_nonce, chunk_t,
	private_ike_init_t *this)
{
	if (memcmp(this->my_nonce.ptr, this->other_nonce.ptr,
			   min(this->my_nonce.len, this->other_nonce.len)) < 0)
	{
		return this->my_nonce;
	}
	else
	{
		return this->other_nonce;
	}
}

/*
 * Described in header.
 */
ike_init_t *ike_init_create(ike_sa_t *ike_sa, bool initiator, ike_sa_t *old_sa)
{
	private_ike_init_t *this;

	INIT(this,
		.public = {
			.task = {
				.get_type = _get_type,
				.migrate = _migrate,
				.destroy = _destroy,
			},
			.derive_keys = _derive_keys,
			.get_lower_nonce = _get_lower_nonce,
		},
		.ike_sa = ike_sa,
		.initiator = initiator,
		.dh_group = KE_NONE,
		.keymat = (keymat_v2_t*)ike_sa->get_keymat(ike_sa),
		.old_sa = old_sa,
		.signature_authentication = lib->settings->get_bool(lib->settings,
								"%s.signature_authentication", TRUE, lib->ns),
		.follow_redirects = lib->settings->get_bool(lib->settings,
								"%s.follow_redirects", TRUE, lib->ns),
	);
	this->nonceg = this->keymat->keymat.create_nonce_gen(&this->keymat->keymat);

	if (initiator)
	{
		this->public.task.build = _build_i;
		this->public.task.process = _process_i;
		this->public.task.pre_process = _pre_process_i;
	}
	else
	{
		this->public.task.build = _build_r;
		this->public.task.process = _process_r;
	}
	return &this->public;
}
