
/*
 * Copyright (C) 2010 Martin Willi
 *
 * 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 "tls_eap.h"

#include "tls.h"

#include <utils/debug.h>
#include <library.h>

/**
 * Size limit for a TLS message allowing for worst-case protection overhead
 * according to section 6.2.3. "Payload Protection" of RFC 5246 TLS 1.2
 */
#define TLS_MAX_MESSAGE_LEN		4 * (TLS_MAX_FRAGMENT_LEN + 2048)

typedef struct private_tls_eap_t private_tls_eap_t;

/**
 * Private data of an tls_eap_t object.
 */
struct private_tls_eap_t {

	/**
	 * Public tls_eap_t interface.
	 */
	tls_eap_t public;

	/**
	 * Type of EAP method, EAP-TLS, EAP-TTLS, or EAP-TNC
	 */
	eap_type_t type;

	/**
	 * Current value of EAP identifier
	 */
	uint8_t identifier;

	/**
	 * TLS stack
	 */
	tls_t *tls;

	/**
	 * Role
	 */
	bool is_server;

	/**
	 * Supported version of the EAP tunnel protocol
	 */
	uint8_t supported_version;

	/**
	 * If FALSE include the total length of an EAP message
	 * in the first fragment of fragmented messages only.
	 * If TRUE also include the length in non-fragmented messages.
	 */
	bool include_length;

	/**
	 * First fragment of a multi-fragment record?
	 */
	bool first_fragment;

	/**
	 * Can we accept acknowledgements as server?
	 */
	bool conversation_started;

	/**
	 * Maximum size of an outgoing EAP-TLS fragment
	 */
	size_t frag_size;

	/**
	 * Number of EAP messages/fragments processed so far
	 */
	int processed;

	/**
	 * Maximum number of processed EAP messages/fragments
	 */
	int max_msg_count;
};

/**
 * Flags of an EAP-TLS/TTLS/TNC message
 */
typedef enum {
	EAP_TLS_LENGTH = (1<<7),		/* shared with EAP-TTLS/TNC/PEAP */
	EAP_TLS_MORE_FRAGS = (1<<6),	/* shared with EAP-TTLS/TNC/PEAP */
	EAP_TLS_START = (1<<5),			/* shared with EAP-TTLS/TNC/PEAP */
	EAP_TTLS_VERSION = (0x07),		/* shared with EAP-TNC/PEAP/PT-EAP */
	EAP_PT_START = (1<<7)			/* PT-EAP only */
} eap_tls_flags_t;

#define EAP_TTLS_SUPPORTED_VERSION		0
#define EAP_TNC_SUPPORTED_VERSION		1
#define EAP_PEAP_SUPPORTED_VERSION		0
#define EAP_PT_EAP_SUPPORTED_VERSION	1

/**
 * EAP-TLS/TTLS packet format
 */
typedef struct __attribute__((packed)) {
	uint8_t code;
	uint8_t identifier;
	uint16_t length;
	uint8_t type;
	uint8_t flags;
} eap_tls_packet_t;

METHOD(tls_eap_t, initiate, status_t,
	private_tls_eap_t *this, chunk_t *out)
{
	if (this->is_server)
	{
		eap_tls_packet_t pkt = {
			.type = this->type,
			.code = EAP_REQUEST,
			.flags = this->supported_version
		};
		switch (this->type)
		{
			case EAP_TLS:
			case EAP_TTLS:
			case EAP_TNC:
			case EAP_PEAP:
				pkt.flags |= EAP_TLS_START;
				break;
			case EAP_PT_EAP:
				pkt.flags |= EAP_PT_START;
				break;
			default:
				break;
		}
		htoun16(&pkt.length, sizeof(eap_tls_packet_t));
		pkt.identifier = this->identifier;

		*out = chunk_clone(chunk_from_thing(pkt));
		DBG2(DBG_TLS, "sending %N start packet (%u bytes)",
			 eap_type_names, this->type, sizeof(eap_tls_packet_t));
		DBG3(DBG_TLS, "%B", out);
		return NEED_MORE;
	}
	return FAILED;
}

/**
 * Process a received packet
 */
static status_t process_pkt(private_tls_eap_t *this, eap_tls_packet_t *pkt)
{
	uint8_t version;
	uint16_t pkt_len;
	uint32_t msg_len;
	size_t msg_len_offset = 0;

	/* EAP-TLS doesn't have a version field */
	if (this->type != EAP_TLS)
	{
		version = pkt->flags & EAP_TTLS_VERSION;
		if (version != this->supported_version)
		{
			DBG1(DBG_TLS, "received %N packet with unsupported version v%u",
			eap_type_names, this->type, version);
			return FAILED;
		}
	}
	pkt_len = untoh16(&pkt->length);

	if (this->type != EAP_PT_EAP && (pkt->flags & EAP_TLS_LENGTH))
	{
		if (pkt_len < sizeof(eap_tls_packet_t) + sizeof(msg_len))
		{
			DBG1(DBG_TLS, "%N packet too short", eap_type_names, this->type);
			return FAILED;
		}
		msg_len = untoh32(pkt + 1);
		if (msg_len < pkt_len - sizeof(eap_tls_packet_t) - sizeof(msg_len) ||
			msg_len > TLS_MAX_MESSAGE_LEN)
		{
			DBG1(DBG_TLS, "invalid %N packet length (%u bytes)", eap_type_names,
				 this->type, msg_len);
			return FAILED;
		}
		msg_len_offset = sizeof(msg_len);
	}

	return this->tls->process(this->tls, (char*)(pkt + 1) + msg_len_offset,
					   pkt_len - sizeof(eap_tls_packet_t) - msg_len_offset);
}

/**
 * Build a packet to send
 */
static status_t build_pkt(private_tls_eap_t *this, chunk_t *out)
{
	char buf[this->frag_size];
	eap_tls_packet_t *pkt;
	size_t len, reclen, msg_len_offset;
	status_t status;
	char *kind DBG_UNUSED;

	if (this->is_server)
	{
		this->identifier++;
	}
	pkt = (eap_tls_packet_t*)buf;
	pkt->code = this->is_server ? EAP_REQUEST : EAP_RESPONSE;
	pkt->identifier = this->identifier;
	pkt->type = this->type;
	pkt->flags = this->supported_version;

	if (this->first_fragment)
	{
		len = sizeof(buf) - sizeof(eap_tls_packet_t) - sizeof(uint32_t);
		msg_len_offset = sizeof(uint32_t);
	}
	else
	{
		len = sizeof(buf) - sizeof(eap_tls_packet_t);
		msg_len_offset = 0;
	}
	status = this->tls->build(this->tls, buf + sizeof(eap_tls_packet_t) +
										 msg_len_offset, &len, &reclen);

	switch (status)
	{
		case NEED_MORE:
			pkt->flags |= EAP_TLS_MORE_FRAGS;
			kind = "further fragment";
			if (this->first_fragment)
			{
				pkt->flags |= EAP_TLS_LENGTH;
				this->first_fragment = FALSE;
				kind = "first fragment";
			}
			break;
		case ALREADY_DONE:
			if (this->first_fragment)
			{
				if (this->include_length)
				{
					pkt->flags |= EAP_TLS_LENGTH;
				}
				kind = "packet";
			}
			else if (this->type != EAP_TNC && this->type != EAP_PT_EAP)
			{
				this->first_fragment = TRUE;
				kind = "final fragment";
			}
			else
			{
				kind = "packet";
			}
			break;
		default:
			return status;
	}
	if (reclen)
	{
		if (pkt->flags & EAP_TLS_LENGTH)
		{
			htoun32(pkt + 1, reclen);
			len += sizeof(uint32_t);
			pkt->flags |= EAP_TLS_LENGTH;
		}
		else
		{
			/* get rid of the reserved length field */
			memmove(buf + sizeof(eap_tls_packet_t),
					buf + sizeof(eap_tls_packet_t) + sizeof(uint32_t), len);
		}
	}
	len += sizeof(eap_tls_packet_t);
	htoun16(&pkt->length, len);
	*out = chunk_clone(chunk_create(buf, len));
	DBG2(DBG_TLS, "sending %N %s (%u bytes)",
		 eap_type_names, this->type, kind, len);
	DBG3(DBG_TLS, "%B", out);
	return NEED_MORE;
}

/**
 * Send an ack to request next fragment
 */
static chunk_t create_ack(private_tls_eap_t *this)
{
	eap_tls_packet_t pkt = {
		.code = this->is_server ? EAP_REQUEST : EAP_RESPONSE,
		.type = this->type,
	};

	if (this->is_server)
	{
		this->identifier++;
	}
	pkt.identifier = this->identifier;
	htoun16(&pkt.length, sizeof(pkt));

	switch (this->type)
	{
		case EAP_TTLS:
			pkt.flags |= EAP_TTLS_SUPPORTED_VERSION;
			break;
		case EAP_TNC:
			pkt.flags |= EAP_TNC_SUPPORTED_VERSION;
			break;
		case EAP_PEAP:
			pkt.flags |= EAP_PEAP_SUPPORTED_VERSION;
			break;
		default:
			break;
	}
	DBG2(DBG_TLS, "sending %N acknowledgment packet",
		 eap_type_names, this->type);
	return chunk_clone(chunk_from_thing(pkt));
}

METHOD(tls_eap_t, process, status_t,
	private_tls_eap_t *this, chunk_t in, chunk_t *out)
{
	eap_tls_packet_t *pkt;
	status_t status;

	if (this->max_msg_count && ++this->processed > this->max_msg_count)
	{
		DBG1(DBG_TLS, "%N packet count exceeded (%d > %d)",
			 eap_type_names, this->type,
			 this->processed, this->max_msg_count);
		return FAILED;
	}

	pkt = (eap_tls_packet_t*)in.ptr;
	if (in.len < sizeof(eap_tls_packet_t) || untoh16(&pkt->length) != in.len)
	{
		DBG1(DBG_TLS, "invalid %N packet length", eap_type_names, this->type);
		return FAILED;
	}

	/* update EAP identifier */
	if (!this->is_server)
	{
		this->identifier = pkt->identifier;
	}
	DBG3(DBG_TLS, "%N payload %B", eap_type_names, this->type, &in);

	if ((this->type == EAP_PT_EAP && (pkt->flags & EAP_PT_START)) ||
        (pkt->flags & EAP_TLS_START))
	{
		if (this->is_server)
		{
			DBG1(DBG_TLS, "received unexpected %N start from client",
				 eap_type_names, this->type);
			return FAILED;
		}
		if (this->type == EAP_TTLS || this->type == EAP_TNC ||
			this->type == EAP_PEAP || this->type == EAP_PT_EAP)
		{
			DBG1(DBG_TLS, "%N version is v%u", eap_type_names, this->type,
				 pkt->flags & EAP_TTLS_VERSION);
		}
	}
	else
	{
		/* note that with TLS 1.3 the client sends an empty EAP packet after the
		 * server sent the "protected success indication" over the TLS
		 * connection, which is interpreted here as an ACK packet */
		if (in.len == sizeof(eap_tls_packet_t))
		{
			if (this->is_server && !this->conversation_started)
			{
				DBG1(DBG_TLS, "received %N acknowledgment packet before EAP "
					 "conversation started", eap_type_names, this->type);
				return FAILED;
			}
			DBG2(DBG_TLS, "received %N acknowledgment packet",
				 eap_type_names, this->type);
			status = build_pkt(this, out);
			if (status == INVALID_STATE && this->tls->is_complete(this->tls))
			{
				return SUCCESS;
			}
			return status;
		}
		status = process_pkt(this, pkt);
		switch (status)
		{
			case NEED_MORE:
				if (this->is_server)
				{
					this->conversation_started = TRUE;
				}
				break;
			case SUCCESS:
				return this->tls->is_complete(this->tls) ? SUCCESS : FAILED;
			default:
				return status;
		}
	}
	status = build_pkt(this, out);
	switch (status)
	{
		case INVALID_STATE:
			if (this->is_server && this->tls->is_complete(this->tls))
			{
				return SUCCESS;
			}
			*out = create_ack(this);
			return NEED_MORE;
		case FAILED:
			if (!this->is_server)
			{
				*out = create_ack(this);
				return NEED_MORE;
			}
			return FAILED;
		default:
			return status;
	}
}

METHOD(tls_eap_t, get_msk, chunk_t,
	private_tls_eap_t *this)
{
	return this->tls->get_eap_msk(this->tls);
}

METHOD(tls_eap_t, get_identifier, uint8_t,
	private_tls_eap_t *this)
{
	return this->identifier;
}

METHOD(tls_eap_t, set_identifier, void,
	private_tls_eap_t *this, uint8_t identifier)
{
	this->identifier = identifier;
}

METHOD(tls_eap_t, get_auth, auth_cfg_t*,
	private_tls_eap_t *this)
{
	return this->tls->get_auth(this->tls);
}

METHOD(tls_eap_t, destroy, void,
	private_tls_eap_t *this)
{
	this->tls->destroy(this->tls);
	free(this);
}

/**
 * See header
 */
tls_eap_t *tls_eap_create(eap_type_t type, tls_t *tls, size_t frag_size,
						  int max_msg_count, bool include_length)
{
	private_tls_eap_t *this;

	if (!tls)
	{
		return NULL;
	}

	INIT(this,
		.public = {
			.initiate = _initiate,
			.process = _process,
			.get_msk = _get_msk,
			.get_identifier = _get_identifier,
			.set_identifier = _set_identifier,
			.get_auth = _get_auth,
			.destroy = _destroy,
		},
		.type = type,
		.is_server = tls->is_server(tls),
		.first_fragment = (type != EAP_TNC && type != EAP_PT_EAP),
		.frag_size = frag_size,
		.max_msg_count = max_msg_count,
		.include_length = include_length,
		.tls = tls,
	);

	switch (type)
	{
		case EAP_TTLS:
			this->supported_version = EAP_TTLS_SUPPORTED_VERSION;
			break;
		case EAP_TNC:
			this->supported_version = EAP_TNC_SUPPORTED_VERSION;
			break;
		case EAP_PEAP:
			this->supported_version = EAP_PEAP_SUPPORTED_VERSION;
			break;
		case EAP_PT_EAP:
			this->supported_version = EAP_PT_EAP_SUPPORTED_VERSION;
			break;
		default:
			break;
	}

	if (this->is_server)
	{
		do
		{	/* start with non-zero random identifier */
			this->identifier = random();
		}
		while (!this->identifier);
	}

	return &this->public;
}
