/*
 * Copyright (C) 2007 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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 any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

/**
 * @file
 *
 * Transport Layer Security Protocol
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <byteswap.h>
#include <gpxe/hmac.h>
#include <gpxe/md5.h>
#include <gpxe/sha1.h>
#include <gpxe/aes.h>
#include <gpxe/rsa.h>
#include <gpxe/xfer.h>
#include <gpxe/open.h>
#include <gpxe/filter.h>
#include <gpxe/asn1.h>
#include <gpxe/x509.h>
#include <gpxe/tls.h>

static int tls_send_plaintext ( struct tls_session *tls, unsigned int type,
				const void *data, size_t len );
static void tls_clear_cipher ( struct tls_session *tls,
			       struct tls_cipherspec *cipherspec );

/******************************************************************************
 *
 * Utility functions
 *
 ******************************************************************************
 */

/**
 * Extract 24-bit field value
 *
 * @v field24		24-bit field
 * @ret value		Field value
 *
 * TLS uses 24-bit integers in several places, which are awkward to
 * parse in C.
 */
static unsigned long tls_uint24 ( uint8_t field24[3] ) {
	return ( ( field24[0] << 16 ) + ( field24[1] << 8 ) + field24[2] );
}

/******************************************************************************
 *
 * Cleanup functions
 *
 ******************************************************************************
 */

/**
 * Free TLS session
 *
 * @v refcnt		Reference counter
 */
static void free_tls ( struct refcnt *refcnt ) {
	struct tls_session *tls =
		container_of ( refcnt, struct tls_session, refcnt );

	/* Free dynamically-allocated resources */
	tls_clear_cipher ( tls, &tls->tx_cipherspec );
	tls_clear_cipher ( tls, &tls->tx_cipherspec_pending );
	tls_clear_cipher ( tls, &tls->rx_cipherspec );
	tls_clear_cipher ( tls, &tls->rx_cipherspec_pending );
	x509_free_rsa_public_key ( &tls->rsa );
	free ( tls->rx_data );

	/* Free TLS structure itself */
	free ( tls );	
}

/**
 * Finish with TLS session
 *
 * @v tls		TLS session
 * @v rc		Status code
 */
static void tls_close ( struct tls_session *tls, int rc ) {

	/* Remove process */
	process_del ( &tls->process );
	
	/* Close ciphertext and plaintext streams */
	xfer_nullify ( &tls->cipherstream.xfer );
	xfer_close ( &tls->cipherstream.xfer, rc );
	xfer_nullify ( &tls->plainstream.xfer );
	xfer_close ( &tls->plainstream.xfer, rc );
}

/******************************************************************************
 *
 * Random number generation
 *
 ******************************************************************************
 */

/**
 * Generate random data
 *
 * @v data		Buffer to fill
 * @v len		Length of buffer
 */
static void tls_generate_random ( void *data, size_t len ) {
	/* FIXME: Some real random data source would be nice... */
	memset ( data, 0x01, len );
}

/**
 * Update HMAC with a list of ( data, len ) pairs
 *
 * @v digest		Hash function to use
 * @v digest_ctx	Digest context
 * @v args		( data, len ) pairs of data, terminated by NULL
 */
static void tls_hmac_update_va ( struct digest_algorithm *digest,
				 void *digest_ctx, va_list args ) {
	void *data;
	size_t len;

	while ( ( data = va_arg ( args, void * ) ) ) {
		len = va_arg ( args, size_t );
		hmac_update ( digest, digest_ctx, data, len );
	}
}

/**
 * Generate secure pseudo-random data using a single hash function
 *
 * @v tls		TLS session
 * @v digest		Hash function to use
 * @v secret		Secret
 * @v secret_len	Length of secret
 * @v out		Output buffer
 * @v out_len		Length of output buffer
 * @v seeds		( data, len ) pairs of seed data, terminated by NULL
 */
static void tls_p_hash_va ( struct tls_session *tls,
			    struct digest_algorithm *digest,
			    void *secret, size_t secret_len,
			    void *out, size_t out_len,
			    va_list seeds ) {
	uint8_t secret_copy[secret_len];
	uint8_t digest_ctx[digest->ctxsize];
	uint8_t digest_ctx_partial[digest->ctxsize];
	uint8_t a[digest->digestsize];
	uint8_t out_tmp[digest->digestsize];
	size_t frag_len = digest->digestsize;
	va_list tmp;

	/* Copy the secret, in case HMAC modifies it */
	memcpy ( secret_copy, secret, secret_len );
	secret = secret_copy;
	DBGC2 ( tls, "TLS %p %s secret:\n", tls, digest->name );
	DBGC2_HD ( tls, secret, secret_len );

	/* Calculate A(1) */
	hmac_init ( digest, digest_ctx, secret, &secret_len );
	va_copy ( tmp, seeds );
	tls_hmac_update_va ( digest, digest_ctx, tmp );
	va_end ( tmp );
	hmac_final ( digest, digest_ctx, secret, &secret_len, a );
	DBGC2 ( tls, "TLS %p %s A(1):\n", tls, digest->name );
	DBGC2_HD ( tls, &a, sizeof ( a ) );

	/* Generate as much data as required */
	while ( out_len ) {
		/* Calculate output portion */
		hmac_init ( digest, digest_ctx, secret, &secret_len );
		hmac_update ( digest, digest_ctx, a, sizeof ( a ) );
		memcpy ( digest_ctx_partial, digest_ctx, digest->ctxsize );
		va_copy ( tmp, seeds );
		tls_hmac_update_va ( digest, digest_ctx, tmp );
		va_end ( tmp );
		hmac_final ( digest, digest_ctx,
			     secret, &secret_len, out_tmp );

		/* Copy output */
		if ( frag_len > out_len )
			frag_len = out_len;
		memcpy ( out, out_tmp, frag_len );
		DBGC2 ( tls, "TLS %p %s output:\n", tls, digest->name );
		DBGC2_HD ( tls, out, frag_len );

		/* Calculate A(i) */
		hmac_final ( digest, digest_ctx_partial,
			     secret, &secret_len, a );
		DBGC2 ( tls, "TLS %p %s A(n):\n", tls, digest->name );
		DBGC2_HD ( tls, &a, sizeof ( a ) );

		out += frag_len;
		out_len -= frag_len;
	}
}

/**
 * Generate secure pseudo-random data
 *
 * @v tls		TLS session
 * @v secret		Secret
 * @v secret_len	Length of secret
 * @v out		Output buffer
 * @v out_len		Length of output buffer
 * @v ...		( data, len ) pairs of seed data, terminated by NULL
 */
static void tls_prf ( struct tls_session *tls, void *secret, size_t secret_len,
		      void *out, size_t out_len, ... ) {
	va_list seeds;
	va_list tmp;
	size_t subsecret_len;
	void *md5_secret;
	void *sha1_secret;
	uint8_t out_md5[out_len];
	uint8_t out_sha1[out_len];
	unsigned int i;

	va_start ( seeds, out_len );

	/* Split secret into two, with an overlap of up to one byte */
	subsecret_len = ( ( secret_len + 1 ) / 2 );
	md5_secret = secret;
	sha1_secret = ( secret + secret_len - subsecret_len );

	/* Calculate MD5 portion */
	va_copy ( tmp, seeds );
	tls_p_hash_va ( tls, &md5_algorithm, md5_secret, subsecret_len,
			out_md5, out_len, seeds );
	va_end ( tmp );

	/* Calculate SHA1 portion */
	va_copy ( tmp, seeds );
	tls_p_hash_va ( tls, &sha1_algorithm, sha1_secret, subsecret_len,
			out_sha1, out_len, seeds );
	va_end ( tmp );

	/* XOR the two portions together into the final output buffer */
	for ( i = 0 ; i < out_len ; i++ ) {
		*( ( uint8_t * ) out + i ) = ( out_md5[i] ^ out_sha1[i] );
	}

	va_end ( seeds );
}

/**
 * Generate secure pseudo-random data
 *
 * @v secret		Secret
 * @v secret_len	Length of secret
 * @v out		Output buffer
 * @v out_len		Length of output buffer
 * @v label		String literal label
 * @v ...		( data, len ) pairs of seed data
 */
#define tls_prf_label( tls, secret, secret_len, out, out_len, label, ... ) \
	tls_prf ( (tls), (secret), (secret_len), (out), (out_len),	   \
		  label, ( sizeof ( label ) - 1 ), __VA_ARGS__, NULL )

/******************************************************************************
 *
 * Secret management
 *
 ******************************************************************************
 */

/**
 * Generate master secret
 *
 * @v tls		TLS session
 *
 * The pre-master secret and the client and server random values must
 * already be known.
 */
static void tls_generate_master_secret ( struct tls_session *tls ) {
	DBGC ( tls, "TLS %p pre-master-secret:\n", tls );
	DBGC_HD ( tls, &tls->pre_master_secret,
		  sizeof ( tls->pre_master_secret ) );
	DBGC ( tls, "TLS %p client random bytes:\n", tls );
	DBGC_HD ( tls, &tls->client_random, sizeof ( tls->client_random ) );
	DBGC ( tls, "TLS %p server random bytes:\n", tls );
	DBGC_HD ( tls, &tls->server_random, sizeof ( tls->server_random ) );

	tls_prf_label ( tls, &tls->pre_master_secret,
			sizeof ( tls->pre_master_secret ),
			&tls->master_secret, sizeof ( tls->master_secret ),
			"master secret",
			&tls->client_random, sizeof ( tls->client_random ),
			&tls->server_random, sizeof ( tls->server_random ) );

	DBGC ( tls, "TLS %p generated master secret:\n", tls );
	DBGC_HD ( tls, &tls->master_secret, sizeof ( tls->master_secret ) );
}

/**
 * Generate key material
 *
 * @v tls		TLS session
 *
 * The master secret must already be known.
 */
static int tls_generate_keys ( struct tls_session *tls ) {
	struct tls_cipherspec *tx_cipherspec = &tls->tx_cipherspec_pending;
	struct tls_cipherspec *rx_cipherspec = &tls->rx_cipherspec_pending;
	size_t hash_size = tx_cipherspec->digest->digestsize;
	size_t key_size = tx_cipherspec->key_len;
	size_t iv_size = tx_cipherspec->cipher->blocksize;
	size_t total = ( 2 * ( hash_size + key_size + iv_size ) );
	uint8_t key_block[total];
	uint8_t *key;
	int rc;

	/* Generate key block */
	tls_prf_label ( tls, &tls->master_secret, sizeof ( tls->master_secret ),
			key_block, sizeof ( key_block ), "key expansion",
			&tls->server_random, sizeof ( tls->server_random ),
			&tls->client_random, sizeof ( tls->client_random ) );

	/* Split key block into portions */
	key = key_block;

	/* TX MAC secret */
	memcpy ( tx_cipherspec->mac_secret, key, hash_size );
	DBGC ( tls, "TLS %p TX MAC secret:\n", tls );
	DBGC_HD ( tls, key, hash_size );
	key += hash_size;

	/* RX MAC secret */
	memcpy ( rx_cipherspec->mac_secret, key, hash_size );
	DBGC ( tls, "TLS %p RX MAC secret:\n", tls );
	DBGC_HD ( tls, key, hash_size );
	key += hash_size;

	/* TX key */
	if ( ( rc = cipher_setkey ( tx_cipherspec->cipher,
				    tx_cipherspec->cipher_ctx,
				    key, key_size ) ) != 0 ) {
		DBGC ( tls, "TLS %p could not set TX key: %s\n",
		       tls, strerror ( rc ) );
		return rc;
	}
	DBGC ( tls, "TLS %p TX key:\n", tls );
	DBGC_HD ( tls, key, key_size );
	key += key_size;

	/* RX key */
	if ( ( rc = cipher_setkey ( rx_cipherspec->cipher,
				    rx_cipherspec->cipher_ctx,
				    key, key_size ) ) != 0 ) {
		DBGC ( tls, "TLS %p could not set TX key: %s\n",
		       tls, strerror ( rc ) );
		return rc;
	}
	DBGC ( tls, "TLS %p RX key:\n", tls );
	DBGC_HD ( tls, key, key_size );
	key += key_size;

	/* TX initialisation vector */
	cipher_setiv ( tx_cipherspec->cipher, tx_cipherspec->cipher_ctx, key );
	DBGC ( tls, "TLS %p TX IV:\n", tls );
	DBGC_HD ( tls, key, iv_size );
	key += iv_size;

	/* RX initialisation vector */
	cipher_setiv ( rx_cipherspec->cipher, rx_cipherspec->cipher_ctx, key );
	DBGC ( tls, "TLS %p RX IV:\n", tls );
	DBGC_HD ( tls, key, iv_size );
	key += iv_size;

	assert ( ( key_block + total ) == key );

	return 0;
}

/******************************************************************************
 *
 * Cipher suite management
 *
 ******************************************************************************
 */

/**
 * Clear cipher suite
 *
 * @v cipherspec	TLS cipher specification
 */
static void tls_clear_cipher ( struct tls_session *tls __unused,
			       struct tls_cipherspec *cipherspec ) {
	free ( cipherspec->dynamic );
	memset ( cipherspec, 0, sizeof ( cipherspec ) );
	cipherspec->pubkey = &pubkey_null;
	cipherspec->cipher = &cipher_null;
	cipherspec->digest = &digest_null;
}

/**
 * Set cipher suite
 *
 * @v tls		TLS session
 * @v cipherspec	TLS cipher specification
 * @v pubkey		Public-key encryption elgorithm
 * @v cipher		Bulk encryption cipher algorithm
 * @v digest		MAC digest algorithm
 * @v key_len		Key length
 * @ret rc		Return status code
 */
static int tls_set_cipher ( struct tls_session *tls,
			    struct tls_cipherspec *cipherspec,
			    struct pubkey_algorithm *pubkey,
			    struct cipher_algorithm *cipher,
			    struct digest_algorithm *digest,
			    size_t key_len ) {
	size_t total;
	void *dynamic;

	/* Clear out old cipher contents, if any */
	tls_clear_cipher ( tls, cipherspec );
	
	/* Allocate dynamic storage */
	total = ( pubkey->ctxsize + 2 * cipher->ctxsize + digest->digestsize );
	dynamic = malloc ( total );
	if ( ! dynamic ) {
		DBGC ( tls, "TLS %p could not allocate %zd bytes for crypto "
		       "context\n", tls, total );
		return -ENOMEM;
	}
	memset ( dynamic, 0, total );

	/* Assign storage */
	cipherspec->dynamic = dynamic;
	cipherspec->pubkey_ctx = dynamic;	dynamic += pubkey->ctxsize;
	cipherspec->cipher_ctx = dynamic;	dynamic += cipher->ctxsize;
	cipherspec->cipher_next_ctx = dynamic;	dynamic += cipher->ctxsize;
	cipherspec->mac_secret = dynamic;	dynamic += digest->digestsize;
	assert ( ( cipherspec->dynamic + total ) == dynamic );

	/* Store parameters */
	cipherspec->pubkey = pubkey;
	cipherspec->cipher = cipher;
	cipherspec->digest = digest;
	cipherspec->key_len = key_len;

	return 0;
}

/**
 * Select next cipher suite
 *
 * @v tls		TLS session
 * @v cipher_suite	Cipher suite specification
 * @ret rc		Return status code
 */
static int tls_select_cipher ( struct tls_session *tls,
			       unsigned int cipher_suite ) {
	struct pubkey_algorithm *pubkey = &pubkey_null;
	struct cipher_algorithm *cipher = &cipher_null;
	struct digest_algorithm *digest = &digest_null;
	unsigned int key_len = 0;
	int rc;

	switch ( cipher_suite ) {
	case htons ( TLS_RSA_WITH_AES_128_CBC_SHA ):
		key_len = ( 128 / 8 );
		cipher = &aes_cbc_algorithm;
		digest = &sha1_algorithm;
		break;
	case htons ( TLS_RSA_WITH_AES_256_CBC_SHA ):
		key_len = ( 256 / 8 );
		cipher = &aes_cbc_algorithm;
		digest = &sha1_algorithm;
		break;
	default:
		DBGC ( tls, "TLS %p does not support cipher %04x\n",
		       tls, ntohs ( cipher_suite ) );
		return -ENOTSUP;
	}

	/* Set ciphers */
	if ( ( rc = tls_set_cipher ( tls, &tls->tx_cipherspec_pending, pubkey,
				     cipher, digest, key_len ) ) != 0 )
		return rc;
	if ( ( rc = tls_set_cipher ( tls, &tls->rx_cipherspec_pending, pubkey,
				     cipher, digest, key_len ) ) != 0 )
		return rc;

	DBGC ( tls, "TLS %p selected %s-%s-%d-%s\n", tls,
	       pubkey->name, cipher->name, ( key_len * 8 ), digest->name );

	return 0;
}

/**
 * Activate next cipher suite
 *
 * @v tls		TLS session
 * @v pending		Pending cipher specification
 * @v active		Active cipher specification to replace
 * @ret rc		Return status code
 */
static int tls_change_cipher ( struct tls_session *tls,
			       struct tls_cipherspec *pending,
			       struct tls_cipherspec *active ) {

	/* Sanity check */
	if ( /* FIXME (when pubkey is not hard-coded to RSA):
	      * ( pending->pubkey == &pubkey_null ) || */
	     ( pending->cipher == &cipher_null ) ||
	     ( pending->digest == &digest_null ) ) {
		DBGC ( tls, "TLS %p refusing to use null cipher\n", tls );
		return -ENOTSUP;
	}

	tls_clear_cipher ( tls, active );
	memswap ( active, pending, sizeof ( *active ) );
	return 0;
}

/******************************************************************************
 *
 * Handshake verification
 *
 ******************************************************************************
 */

/**
 * Add handshake record to verification hash
 *
 * @v tls		TLS session
 * @v data		Handshake record
 * @v len		Length of handshake record
 */
static void tls_add_handshake ( struct tls_session *tls,
				const void *data, size_t len ) {

	digest_update ( &md5_algorithm, tls->handshake_md5_ctx, data, len );
	digest_update ( &sha1_algorithm, tls->handshake_sha1_ctx, data, len );
}

/**
 * Calculate handshake verification hash
 *
 * @v tls		TLS session
 * @v out		Output buffer
 *
 * Calculates the MD5+SHA1 digest over all handshake messages seen so
 * far.
 */
static void tls_verify_handshake ( struct tls_session *tls, void *out ) {
	struct digest_algorithm *md5 = &md5_algorithm;
	struct digest_algorithm *sha1 = &sha1_algorithm;
	uint8_t md5_ctx[md5->ctxsize];
	uint8_t sha1_ctx[sha1->ctxsize];
	void *md5_digest = out;
	void *sha1_digest = ( out + md5->digestsize );

	memcpy ( md5_ctx, tls->handshake_md5_ctx, sizeof ( md5_ctx ) );
	memcpy ( sha1_ctx, tls->handshake_sha1_ctx, sizeof ( sha1_ctx ) );
	digest_final ( md5, md5_ctx, md5_digest );
	digest_final ( sha1, sha1_ctx, sha1_digest );
}

/******************************************************************************
 *
 * Record handling
 *
 ******************************************************************************
 */

/**
 * Transmit Handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_send_handshake ( struct tls_session *tls,
				void *data, size_t len ) {

	/* Add to handshake digest */
	tls_add_handshake ( tls, data, len );

	/* Send record */
	return tls_send_plaintext ( tls, TLS_TYPE_HANDSHAKE, data, len );
}

/**
 * Transmit Client Hello record
 *
 * @v tls		TLS session
 * @ret rc		Return status code
 */
static int tls_send_client_hello ( struct tls_session *tls ) {
	struct {
		uint32_t type_length;
		uint16_t version;
		uint8_t random[32];
		uint8_t session_id_len;
		uint16_t cipher_suite_len;
		uint16_t cipher_suites[2];
		uint8_t compression_methods_len;
		uint8_t compression_methods[1];
	} __attribute__ (( packed )) hello;

	memset ( &hello, 0, sizeof ( hello ) );
	hello.type_length = ( cpu_to_le32 ( TLS_CLIENT_HELLO ) |
			      htonl ( sizeof ( hello ) -
				      sizeof ( hello.type_length ) ) );
	hello.version = htons ( TLS_VERSION_TLS_1_0 );
	memcpy ( &hello.random, &tls->client_random, sizeof ( hello.random ) );
	hello.cipher_suite_len = htons ( sizeof ( hello.cipher_suites ) );
	hello.cipher_suites[0] = htons ( TLS_RSA_WITH_AES_128_CBC_SHA );
	hello.cipher_suites[1] = htons ( TLS_RSA_WITH_AES_256_CBC_SHA );
	hello.compression_methods_len = sizeof ( hello.compression_methods );

	return tls_send_handshake ( tls, &hello, sizeof ( hello ) );
}

/**
 * Transmit Client Key Exchange record
 *
 * @v tls		TLS session
 * @ret rc		Return status code
 */
static int tls_send_client_key_exchange ( struct tls_session *tls ) {
	/* FIXME: Hack alert */
	RSA_CTX *rsa_ctx;
	RSA_pub_key_new ( &rsa_ctx, tls->rsa.modulus, tls->rsa.modulus_len,
			  tls->rsa.exponent, tls->rsa.exponent_len );
	struct {
		uint32_t type_length;
		uint16_t encrypted_pre_master_secret_len;
		uint8_t encrypted_pre_master_secret[rsa_ctx->num_octets];
	} __attribute__ (( packed )) key_xchg;

	memset ( &key_xchg, 0, sizeof ( key_xchg ) );
	key_xchg.type_length = ( cpu_to_le32 ( TLS_CLIENT_KEY_EXCHANGE ) |
				 htonl ( sizeof ( key_xchg ) -
					 sizeof ( key_xchg.type_length ) ) );
	key_xchg.encrypted_pre_master_secret_len
		= htons ( sizeof ( key_xchg.encrypted_pre_master_secret ) );

	/* FIXME: Hack alert */
	DBGC ( tls, "RSA encrypting plaintext, modulus, exponent:\n" );
	DBGC_HD ( tls, &tls->pre_master_secret,
		  sizeof ( tls->pre_master_secret ) );
	DBGC_HD ( tls, tls->rsa.modulus, tls->rsa.modulus_len );
	DBGC_HD ( tls, tls->rsa.exponent, tls->rsa.exponent_len );
	RSA_encrypt ( rsa_ctx, ( const uint8_t * ) &tls->pre_master_secret,
		      sizeof ( tls->pre_master_secret ),
		      key_xchg.encrypted_pre_master_secret, 0 );
	DBGC ( tls, "RSA encrypt done.  Ciphertext:\n" );
	DBGC_HD ( tls, &key_xchg.encrypted_pre_master_secret,
		  sizeof ( key_xchg.encrypted_pre_master_secret ) );
	RSA_free ( rsa_ctx );


	return tls_send_handshake ( tls, &key_xchg, sizeof ( key_xchg ) );
}

/**
 * Transmit Change Cipher record
 *
 * @v tls		TLS session
 * @ret rc		Return status code
 */
static int tls_send_change_cipher ( struct tls_session *tls ) {
	static const uint8_t change_cipher[1] = { 1 };
	return tls_send_plaintext ( tls, TLS_TYPE_CHANGE_CIPHER,
				    change_cipher, sizeof ( change_cipher ) );
}

/**
 * Transmit Finished record
 *
 * @v tls		TLS session
 * @ret rc		Return status code
 */
static int tls_send_finished ( struct tls_session *tls ) {
	struct {
		uint32_t type_length;
		uint8_t verify_data[12];
	} __attribute__ (( packed )) finished;
	uint8_t digest[MD5_DIGEST_SIZE + SHA1_DIGEST_SIZE];

	memset ( &finished, 0, sizeof ( finished ) );
	finished.type_length = ( cpu_to_le32 ( TLS_FINISHED ) |
				 htonl ( sizeof ( finished ) -
					 sizeof ( finished.type_length ) ) );
	tls_verify_handshake ( tls, digest );
	tls_prf_label ( tls, &tls->master_secret, sizeof ( tls->master_secret ),
			finished.verify_data, sizeof ( finished.verify_data ),
			"client finished", digest, sizeof ( digest ) );

	return tls_send_handshake ( tls, &finished, sizeof ( finished ) );
}

/**
 * Receive new Change Cipher record
 *
 * @v tls		TLS session
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_new_change_cipher ( struct tls_session *tls,
				   void *data, size_t len ) {
	int rc;

	if ( ( len != 1 ) || ( *( ( uint8_t * ) data ) != 1 ) ) {
		DBGC ( tls, "TLS %p received invalid Change Cipher\n", tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	if ( ( rc = tls_change_cipher ( tls, &tls->rx_cipherspec_pending,
					&tls->rx_cipherspec ) ) != 0 ) {
		DBGC ( tls, "TLS %p could not activate RX cipher: %s\n",
		       tls, strerror ( rc ) );
		return rc;
	}
	tls->rx_seq = ~( ( uint64_t ) 0 );

	return 0;
}

/**
 * Receive new Alert record
 *
 * @v tls		TLS session
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_new_alert ( struct tls_session *tls, void *data, size_t len ) {
	struct {
		uint8_t level;
		uint8_t description;
		char next[0];
	} __attribute__ (( packed )) *alert = data;
	void *end = alert->next;

	/* Sanity check */
	if ( end != ( data + len ) ) {
		DBGC ( tls, "TLS %p received overlength Alert\n", tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	switch ( alert->level ) {
	case TLS_ALERT_WARNING:
		DBGC ( tls, "TLS %p received warning alert %d\n",
		       tls, alert->description );
		return 0;
	case TLS_ALERT_FATAL:
		DBGC ( tls, "TLS %p received fatal alert %d\n",
		       tls, alert->description );
		return -EPERM;
	default:
		DBGC ( tls, "TLS %p received unknown alert level %d"
		       "(alert %d)\n", tls, alert->level, alert->description );
		return -EIO;
	}
}

/**
 * Receive new Server Hello handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext handshake record
 * @v len		Length of plaintext handshake record
 * @ret rc		Return status code
 */
static int tls_new_server_hello ( struct tls_session *tls,
				  void *data, size_t len ) {
	struct {
		uint16_t version;
		uint8_t random[32];
		uint8_t session_id_len;
		char next[0];
	} __attribute__ (( packed )) *hello_a = data;
	struct {
		uint8_t session_id[hello_a->session_id_len];
		uint16_t cipher_suite;
		uint8_t compression_method;
		char next[0];
	} __attribute__ (( packed )) *hello_b = ( void * ) &hello_a->next;
	void *end = hello_b->next;
	int rc;

	/* Sanity check */
	if ( end != ( data + len ) ) {
		DBGC ( tls, "TLS %p received overlength Server Hello\n", tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	/* Check protocol version */
	if ( ntohs ( hello_a->version ) < TLS_VERSION_TLS_1_0 ) {
		DBGC ( tls, "TLS %p does not support protocol version %d.%d\n",
		       tls, ( ntohs ( hello_a->version ) >> 8 ),
		       ( ntohs ( hello_a->version ) & 0xff ) );
		return -ENOTSUP;
	}

	/* Copy out server random bytes */
	memcpy ( &tls->server_random, &hello_a->random,
		 sizeof ( tls->server_random ) );

	/* Select cipher suite */
	if ( ( rc = tls_select_cipher ( tls, hello_b->cipher_suite ) ) != 0 )
		return rc;

	/* Generate secrets */
	tls_generate_master_secret ( tls );
	if ( ( rc = tls_generate_keys ( tls ) ) != 0 )
		return rc;

	return 0;
}

/**
 * Receive new Certificate handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext handshake record
 * @v len		Length of plaintext handshake record
 * @ret rc		Return status code
 */
static int tls_new_certificate ( struct tls_session *tls,
				 void *data, size_t len ) {
	struct {
		uint8_t length[3];
		uint8_t certificates[0];
	} __attribute__ (( packed )) *certificate = data;
	struct {
		uint8_t length[3];
		uint8_t certificate[0];
	} __attribute__ (( packed )) *element =
		  ( ( void * ) certificate->certificates );
	size_t elements_len = tls_uint24 ( certificate->length );
	void *end = ( certificate->certificates + elements_len );
	struct asn1_cursor cursor;
	int rc;

	/* Sanity check */
	if ( end != ( data + len ) ) {
		DBGC ( tls, "TLS %p received overlength Server Certificate\n",
		       tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	/* Traverse certificate chain */
	do {
		cursor.data = element->certificate;
		cursor.len = tls_uint24 ( element->length );
		if ( ( cursor.data + cursor.len ) > end ) {
			DBGC ( tls, "TLS %p received corrupt Server "
			       "Certificate\n", tls );
			DBGC_HD ( tls, data, len );
			return -EINVAL;
		}

		// HACK
		if ( ( rc = x509_rsa_public_key ( &cursor,
						  &tls->rsa ) ) != 0 ) {
			DBGC ( tls, "TLS %p cannot determine RSA public key: "
			       "%s\n", tls, strerror ( rc ) );
			return rc;
		}
		return 0;

		element = ( cursor.data + cursor.len );
	} while ( element != end );

	return -EINVAL;
}

/**
 * Receive new Server Hello Done handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext handshake record
 * @v len		Length of plaintext handshake record
 * @ret rc		Return status code
 */
static int tls_new_server_hello_done ( struct tls_session *tls,
				       void *data, size_t len ) {
	struct {
		char next[0];
	} __attribute__ (( packed )) *hello_done = data;
	void *end = hello_done->next;

	/* Sanity check */
	if ( end != ( data + len ) ) {
		DBGC ( tls, "TLS %p received overlength Server Hello Done\n",
		       tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	/* Check that we are ready to send the Client Key Exchange */
	if ( tls->tx_state != TLS_TX_NONE ) {
		DBGC ( tls, "TLS %p received Server Hello Done while in "
		       "TX state %d\n", tls, tls->tx_state );
		return -EIO;
	}

	/* Start sending the Client Key Exchange */
	tls->tx_state = TLS_TX_CLIENT_KEY_EXCHANGE;

	return 0;
}

/**
 * Receive new Finished handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext handshake record
 * @v len		Length of plaintext handshake record
 * @ret rc		Return status code
 */
static int tls_new_finished ( struct tls_session *tls,
			      void *data, size_t len ) {

	/* FIXME: Handle this properly */
	tls->tx_state = TLS_TX_DATA;
	( void ) data;
	( void ) len;
	return 0;
}

/**
 * Receive new Handshake record
 *
 * @v tls		TLS session
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_new_handshake ( struct tls_session *tls,
			       void *data, size_t len ) {
	struct {
		uint8_t type;
		uint8_t length[3];
		uint8_t payload[0];
	} __attribute__ (( packed )) *handshake = data;
	void *payload = &handshake->payload;
	size_t payload_len = tls_uint24 ( handshake->length );
	void *end = ( payload + payload_len );
	int rc;

	/* Sanity check */
	if ( end != ( data + len ) ) {
		DBGC ( tls, "TLS %p received overlength Handshake\n", tls );
		DBGC_HD ( tls, data, len );
		return -EINVAL;
	}

	switch ( handshake->type ) {
	case TLS_SERVER_HELLO:
		rc = tls_new_server_hello ( tls, payload, payload_len );
		break;
	case TLS_CERTIFICATE:
		rc = tls_new_certificate ( tls, payload, payload_len );
		break;
	case TLS_SERVER_HELLO_DONE:
		rc = tls_new_server_hello_done ( tls, payload, payload_len );
		break;
	case TLS_FINISHED:
		rc = tls_new_finished ( tls, payload, payload_len );
		break;
	default:
		DBGC ( tls, "TLS %p ignoring handshake type %d\n",
		       tls, handshake->type );
		rc = 0;
		break;
	}

	/* Add to handshake digest (except for Hello Requests, which
	 * are explicitly excluded).
	 */
	if ( handshake->type != TLS_HELLO_REQUEST )
		tls_add_handshake ( tls, data, len );

	return rc;
}

/**
 * Receive new record
 *
 * @v tls		TLS session
 * @v type		Record type
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_new_record ( struct tls_session *tls,
			    unsigned int type, void *data, size_t len ) {

	switch ( type ) {
	case TLS_TYPE_CHANGE_CIPHER:
		return tls_new_change_cipher ( tls, data, len );
	case TLS_TYPE_ALERT:
		return tls_new_alert ( tls, data, len );
	case TLS_TYPE_HANDSHAKE:
		return tls_new_handshake ( tls, data, len );
	case TLS_TYPE_DATA:
		return xfer_deliver_raw ( &tls->plainstream.xfer, data, len );
	default:
		/* RFC4346 says that we should just ignore unknown
		 * record types.
		 */
		DBGC ( tls, "TLS %p ignoring record type %d\n", tls, type );
		return 0;
	}
}

/******************************************************************************
 *
 * Record encryption/decryption
 *
 ******************************************************************************
 */

/**
 * Calculate HMAC
 *
 * @v tls		TLS session
 * @v cipherspec	Cipher specification
 * @v seq		Sequence number
 * @v tlshdr		TLS header
 * @v data		Data
 * @v len		Length of data
 * @v mac		HMAC to fill in
 */
static void tls_hmac ( struct tls_session *tls __unused,
		       struct tls_cipherspec *cipherspec,
		       uint64_t seq, struct tls_header *tlshdr,
		       const void *data, size_t len, void *hmac ) {
	struct digest_algorithm *digest = cipherspec->digest;
	uint8_t digest_ctx[digest->ctxsize];

	hmac_init ( digest, digest_ctx, cipherspec->mac_secret,
		    &digest->digestsize );
	seq = cpu_to_be64 ( seq );
	hmac_update ( digest, digest_ctx, &seq, sizeof ( seq ) );
	hmac_update ( digest, digest_ctx, tlshdr, sizeof ( *tlshdr ) );
	hmac_update ( digest, digest_ctx, data, len );
	hmac_final ( digest, digest_ctx, cipherspec->mac_secret,
		     &digest->digestsize, hmac );
}

/**
 * Allocate and assemble stream-ciphered record from data and MAC portions
 *
 * @v tls		TLS session
 * @ret data		Data
 * @ret len		Length of data
 * @ret digest		MAC digest
 * @ret plaintext_len	Length of plaintext record
 * @ret plaintext	Allocated plaintext record
 */
static void * __malloc tls_assemble_stream ( struct tls_session *tls,
				    const void *data, size_t len,
				    void *digest, size_t *plaintext_len ) {
	size_t mac_len = tls->tx_cipherspec.digest->digestsize;
	void *plaintext;
	void *content;
	void *mac;

	/* Calculate stream-ciphered struct length */
	*plaintext_len = ( len + mac_len );

	/* Allocate stream-ciphered struct */
	plaintext = malloc ( *plaintext_len );
	if ( ! plaintext )
		return NULL;
	content = plaintext;
	mac = ( content + len );

	/* Fill in stream-ciphered struct */
	memcpy ( content, data, len );
	memcpy ( mac, digest, mac_len );

	return plaintext;
}

/**
 * Allocate and assemble block-ciphered record from data and MAC portions
 *
 * @v tls		TLS session
 * @ret data		Data
 * @ret len		Length of data
 * @ret digest		MAC digest
 * @ret plaintext_len	Length of plaintext record
 * @ret plaintext	Allocated plaintext record
 */
static void * tls_assemble_block ( struct tls_session *tls,
				   const void *data, size_t len,
				   void *digest, size_t *plaintext_len ) {
	size_t blocksize = tls->tx_cipherspec.cipher->blocksize;
	size_t iv_len = blocksize;
	size_t mac_len = tls->tx_cipherspec.digest->digestsize;
	size_t padding_len;
	void *plaintext;
	void *iv;
	void *content;
	void *mac;
	void *padding;

	/* FIXME: TLSv1.1 has an explicit IV */
	iv_len = 0;

	/* Calculate block-ciphered struct length */
	padding_len = ( ( blocksize - 1 ) & -( iv_len + len + mac_len + 1 ) );
	*plaintext_len = ( iv_len + len + mac_len + padding_len + 1 );

	/* Allocate block-ciphered struct */
	plaintext = malloc ( *plaintext_len );
	if ( ! plaintext )
		return NULL;
	iv = plaintext;
	content = ( iv + iv_len );
	mac = ( content + len );
	padding = ( mac + mac_len );

	/* Fill in block-ciphered struct */
	memset ( iv, 0, iv_len );
	memcpy ( content, data, len );
	memcpy ( mac, digest, mac_len );
	memset ( padding, padding_len, ( padding_len + 1 ) );

	return plaintext;
}

/**
 * Send plaintext record
 *
 * @v tls		TLS session
 * @v type		Record type
 * @v data		Plaintext record
 * @v len		Length of plaintext record
 * @ret rc		Return status code
 */
static int tls_send_plaintext ( struct tls_session *tls, unsigned int type,
				const void *data, size_t len ) {
	struct tls_header plaintext_tlshdr;
	struct tls_header *tlshdr;
	struct tls_cipherspec *cipherspec = &tls->tx_cipherspec;
	void *plaintext = NULL;
	size_t plaintext_len;
	struct io_buffer *ciphertext = NULL;
	size_t ciphertext_len;
	size_t mac_len = cipherspec->digest->digestsize;
	uint8_t mac[mac_len];
	int rc;

	/* Construct header */
	plaintext_tlshdr.type = type;
	plaintext_tlshdr.version = htons ( TLS_VERSION_TLS_1_0 );
	plaintext_tlshdr.length = htons ( len );

	/* Calculate MAC */
	tls_hmac ( tls, cipherspec, tls->tx_seq, &plaintext_tlshdr,
		   data, len, mac );

	/* Allocate and assemble plaintext struct */
	if ( is_stream_cipher ( cipherspec->cipher ) ) {
		plaintext = tls_assemble_stream ( tls, data, len, mac,
						  &plaintext_len );
	} else {
		plaintext = tls_assemble_block ( tls, data, len, mac,
						 &plaintext_len );
	}
	if ( ! plaintext ) {
		DBGC ( tls, "TLS %p could not allocate %zd bytes for "
		       "plaintext\n", tls, plaintext_len );
		rc = -ENOMEM;
		goto done;
	}

	DBGC2 ( tls, "Sending plaintext data:\n" );
	DBGC2_HD ( tls, plaintext, plaintext_len );

	/* Allocate ciphertext */
	ciphertext_len = ( sizeof ( *tlshdr ) + plaintext_len );
	ciphertext = xfer_alloc_iob ( &tls->cipherstream.xfer,
				      ciphertext_len );
	if ( ! ciphertext ) {
		DBGC ( tls, "TLS %p could not allocate %zd bytes for "
		       "ciphertext\n", tls, ciphertext_len );
		rc = -ENOMEM;
		goto done;
	}

	/* Assemble ciphertext */
	tlshdr = iob_put ( ciphertext, sizeof ( *tlshdr ) );
	tlshdr->type = type;
	tlshdr->version = htons ( TLS_VERSION_TLS_1_0 );
	tlshdr->length = htons ( plaintext_len );
	memcpy ( cipherspec->cipher_next_ctx, cipherspec->cipher_ctx,
		 cipherspec->cipher->ctxsize );
	cipher_encrypt ( cipherspec->cipher, cipherspec->cipher_next_ctx,
			 plaintext, iob_put ( ciphertext, plaintext_len ),
			 plaintext_len );

	/* Free plaintext as soon as possible to conserve memory */
	free ( plaintext );
	plaintext = NULL;

	/* Send ciphertext */
	rc = xfer_deliver_iob ( &tls->cipherstream.xfer, ciphertext );
	ciphertext = NULL;
	if ( rc != 0 ) {
		DBGC ( tls, "TLS %p could not deliver ciphertext: %s\n",
		       tls, strerror ( rc ) );
		goto done;
	}

	/* Update TX state machine to next record */
	tls->tx_seq += 1;
	memcpy ( tls->tx_cipherspec.cipher_ctx,
		 tls->tx_cipherspec.cipher_next_ctx,
		 tls->tx_cipherspec.cipher->ctxsize );

 done:
	free ( plaintext );
	free_iob ( ciphertext );
	return rc;
}

/**
 * Split stream-ciphered record into data and MAC portions
 *
 * @v tls		TLS session
 * @v plaintext		Plaintext record
 * @v plaintext_len	Length of record
 * @ret data		Data
 * @ret len		Length of data
 * @ret digest		MAC digest
 * @ret rc		Return status code
 */
static int tls_split_stream ( struct tls_session *tls,
			      void *plaintext, size_t plaintext_len,
			      void **data, size_t *len, void **digest ) {
	void *content;
	size_t content_len;
	void *mac;
	size_t mac_len;

	/* Decompose stream-ciphered data */
	mac_len = tls->rx_cipherspec.digest->digestsize;
	if ( plaintext_len < mac_len ) {
		DBGC ( tls, "TLS %p received underlength record\n", tls );
		DBGC_HD ( tls, plaintext, plaintext_len );
		return -EINVAL;
	}
	content_len = ( plaintext_len - mac_len );
	content = plaintext;
	mac = ( content + content_len );

	/* Fill in return values */
	*data = content;
	*len = content_len;
	*digest = mac;

	return 0;
}

/**
 * Split block-ciphered record into data and MAC portions
 *
 * @v tls		TLS session
 * @v plaintext		Plaintext record
 * @v plaintext_len	Length of record
 * @ret data		Data
 * @ret len		Length of data
 * @ret digest		MAC digest
 * @ret rc		Return status code
 */
static int tls_split_block ( struct tls_session *tls,
			     void *plaintext, size_t plaintext_len,
			     void **data, size_t *len,
			     void **digest ) {
	void *iv;
	size_t iv_len;
	void *content;
	size_t content_len;
	void *mac;
	size_t mac_len;
	void *padding;
	size_t padding_len;
	unsigned int i;

	/* Decompose block-ciphered data */
	if ( plaintext_len < 1 ) {
		DBGC ( tls, "TLS %p received underlength record\n", tls );
		DBGC_HD ( tls, plaintext, plaintext_len );
		return -EINVAL;
	}
	iv_len = tls->rx_cipherspec.cipher->blocksize;

	/* FIXME: TLSv1.1 uses an explicit IV */
	iv_len = 0;

	mac_len = tls->rx_cipherspec.digest->digestsize;
	padding_len = *( ( uint8_t * ) ( plaintext + plaintext_len - 1 ) );
	if ( plaintext_len < ( iv_len + mac_len + padding_len + 1 ) ) {
		DBGC ( tls, "TLS %p received underlength record\n", tls );
		DBGC_HD ( tls, plaintext, plaintext_len );
		return -EINVAL;
	}
	content_len = ( plaintext_len - iv_len - mac_len - padding_len - 1 );
	iv = plaintext;
	content = ( iv + iv_len );
	mac = ( content + content_len );
	padding = ( mac + mac_len );

	/* Verify padding bytes */
	for ( i = 0 ; i < padding_len ; i++ ) {
		if ( *( ( uint8_t * ) ( padding + i ) ) != padding_len ) {
			DBGC ( tls, "TLS %p received bad padding\n", tls );
			DBGC_HD ( tls, plaintext, plaintext_len );
			return -EINVAL;
		}
	}

	/* Fill in return values */
	*data = content;
	*len = content_len;
	*digest = mac;

	return 0;
}

/**
 * Receive new ciphertext record
 *
 * @v tls		TLS session
 * @v tlshdr		Record header
 * @v ciphertext	Ciphertext record
 * @ret rc		Return status code
 */
static int tls_new_ciphertext ( struct tls_session *tls,
				struct tls_header *tlshdr, void *ciphertext ) {
	struct tls_header plaintext_tlshdr;
	struct tls_cipherspec *cipherspec = &tls->rx_cipherspec;
	size_t record_len = ntohs ( tlshdr->length );
	void *plaintext = NULL;
	void *data;
	size_t len;
	void *mac;
	size_t mac_len = cipherspec->digest->digestsize;
	uint8_t verify_mac[mac_len];
	int rc;

	/* Allocate buffer for plaintext */
	plaintext = malloc ( record_len );
	if ( ! plaintext ) {
		DBGC ( tls, "TLS %p could not allocate %zd bytes for "
		       "decryption buffer\n", tls, record_len );
		rc = -ENOMEM;
		goto done;
	}

	/* Decrypt the record */
	cipher_decrypt ( cipherspec->cipher, cipherspec->cipher_ctx,
			 ciphertext, plaintext, record_len );

	/* Split record into content and MAC */
	if ( is_stream_cipher ( cipherspec->cipher ) ) {
		if ( ( rc = tls_split_stream ( tls, plaintext, record_len,
					       &data, &len, &mac ) ) != 0 )
			goto done;
	} else {
		if ( ( rc = tls_split_block ( tls, plaintext, record_len,
					      &data, &len, &mac ) ) != 0 )
			goto done;
	}

	/* Verify MAC */
	plaintext_tlshdr.type = tlshdr->type;
	plaintext_tlshdr.version = tlshdr->version;
	plaintext_tlshdr.length = htons ( len );
	tls_hmac ( tls, cipherspec, tls->rx_seq, &plaintext_tlshdr,
		   data, len, verify_mac);
	if ( memcmp ( mac, verify_mac, mac_len ) != 0 ) {
		DBGC ( tls, "TLS %p failed MAC verification\n", tls );
		DBGC_HD ( tls, plaintext, record_len );
		goto done;
	}

	DBGC2 ( tls, "Received plaintext data:\n" );
	DBGC2_HD ( tls, data, len );

	/* Process plaintext record */
	if ( ( rc = tls_new_record ( tls, tlshdr->type, data, len ) ) != 0 )
		goto done;

	rc = 0;
 done:
	free ( plaintext );
	return rc;
}

/******************************************************************************
 *
 * Plaintext stream operations
 *
 ******************************************************************************
 */

/**
 * Close interface
 *
 * @v xfer		Plainstream data transfer interface
 * @v rc		Reason for close
 */
static void tls_plainstream_close ( struct xfer_interface *xfer, int rc ) {
	struct tls_session *tls =
		container_of ( xfer, struct tls_session, plainstream.xfer );

	tls_close ( tls, rc );
}

/**
 * Check flow control window
 *
 * @v xfer		Plainstream data transfer interface
 * @ret len		Length of window
 */
static size_t tls_plainstream_window ( struct xfer_interface *xfer ) {
	struct tls_session *tls =
		container_of ( xfer, struct tls_session, plainstream.xfer );

	/* Block window unless we are ready to accept data */
	if ( tls->tx_state != TLS_TX_DATA )
		return 0;

	return filter_window ( xfer );
}

/**
 * Deliver datagram as raw data
 *
 * @v xfer		Plainstream data transfer interface
 * @v data		Data buffer
 * @v len		Length of data buffer
 * @ret rc		Return status code
 */
static int tls_plainstream_deliver_raw ( struct xfer_interface *xfer,
					 const void *data, size_t len ) {
	struct tls_session *tls =
		container_of ( xfer, struct tls_session, plainstream.xfer );
	
	/* Refuse unless we are ready to accept data */
	if ( tls->tx_state != TLS_TX_DATA )
		return -ENOTCONN;

	return tls_send_plaintext ( tls, TLS_TYPE_DATA, data, len );
}

/** TLS plaintext stream operations */
static struct xfer_interface_operations tls_plainstream_operations = {
	.close		= tls_plainstream_close,
	.vredirect	= ignore_xfer_vredirect,
	.window		= tls_plainstream_window,
	.alloc_iob	= default_xfer_alloc_iob,
	.deliver_iob	= xfer_deliver_as_raw,
	.deliver_raw	= tls_plainstream_deliver_raw,
};

/******************************************************************************
 *
 * Ciphertext stream operations
 *
 ******************************************************************************
 */

/**
 * Close interface
 *
 * @v xfer		Plainstream data transfer interface
 * @v rc		Reason for close
 */
static void tls_cipherstream_close ( struct xfer_interface *xfer, int rc ) {
	struct tls_session *tls =
		container_of ( xfer, struct tls_session, cipherstream.xfer );

	tls_close ( tls, rc );
}

/**
 * Handle received TLS header
 *
 * @v tls		TLS session
 * @ret rc		Returned status code
 */
static int tls_newdata_process_header ( struct tls_session *tls ) {
	size_t data_len = ntohs ( tls->rx_header.length );

	/* Allocate data buffer now that we know the length */
	assert ( tls->rx_data == NULL );
	tls->rx_data = malloc ( data_len );
	if ( ! tls->rx_data ) {
		DBGC ( tls, "TLS %p could not allocate %zd bytes "
		       "for receive buffer\n", tls, data_len );
		return -ENOMEM;
	}

	/* Move to data state */
	tls->rx_state = TLS_RX_DATA;

	return 0;
}

/**
 * Handle received TLS data payload
 *
 * @v tls		TLS session
 * @ret rc		Returned status code
 */
static int tls_newdata_process_data ( struct tls_session *tls ) {
	int rc;

	/* Process record */
	if ( ( rc = tls_new_ciphertext ( tls, &tls->rx_header,
					 tls->rx_data ) ) != 0 )
		return rc;

	/* Increment RX sequence number */
	tls->rx_seq += 1;

	/* Free data buffer */
	free ( tls->rx_data );
	tls->rx_data = NULL;

	/* Return to header state */
	tls->rx_state = TLS_RX_HEADER;

	return 0;
}

/**
 * Receive new ciphertext
 *
 * @v app		Stream application
 * @v data		Data received
 * @v len		Length of received data
 * @ret rc		Return status code
 */
static int tls_cipherstream_deliver_raw ( struct xfer_interface *xfer,
					  const void *data, size_t len ) {
	struct tls_session *tls = 
		container_of ( xfer, struct tls_session, cipherstream.xfer );
	size_t frag_len;
	void *buf;
	size_t buf_len;
	int ( * process ) ( struct tls_session *tls );
	int rc;

	while ( len ) {
		/* Select buffer according to current state */
		switch ( tls->rx_state ) {
		case TLS_RX_HEADER:
			buf = &tls->rx_header;
			buf_len = sizeof ( tls->rx_header );
			process = tls_newdata_process_header;
			break;
		case TLS_RX_DATA:
			buf = tls->rx_data;
			buf_len = ntohs ( tls->rx_header.length );
			process = tls_newdata_process_data;
			break;
		default:
			assert ( 0 );
			return -EINVAL;
		}

		/* Copy data portion to buffer */
		frag_len = ( buf_len - tls->rx_rcvd );
		if ( frag_len > len )
			frag_len = len;
		memcpy ( ( buf + tls->rx_rcvd ), data, frag_len );
		tls->rx_rcvd += frag_len;
		data += frag_len;
		len -= frag_len;

		/* Process data if buffer is now full */
		if ( tls->rx_rcvd == buf_len ) {
			if ( ( rc = process ( tls ) ) != 0 ) {
				tls_close ( tls, rc );
				return rc;
			}
			tls->rx_rcvd = 0;
		}
	}

	return 0;
}

/** TLS ciphertext stream operations */
static struct xfer_interface_operations tls_cipherstream_operations = {
	.close		= tls_cipherstream_close,
	.vredirect	= xfer_vreopen,
	.window		= filter_window,
	.alloc_iob	= default_xfer_alloc_iob,
	.deliver_iob	= xfer_deliver_as_raw,
	.deliver_raw	= tls_cipherstream_deliver_raw,
};

/******************************************************************************
 *
 * Controlling process
 *
 ******************************************************************************
 */

/**
 * TLS TX state machine
 *
 * @v process		TLS process
 */
static void tls_step ( struct process *process ) {
	struct tls_session *tls =
		container_of ( process, struct tls_session, process );
	int rc;

	/* Wait for cipherstream to become ready */
	if ( ! xfer_window ( &tls->cipherstream.xfer ) )
		return;

	switch ( tls->tx_state ) {
	case TLS_TX_NONE:
		/* Nothing to do */
		break;
	case TLS_TX_CLIENT_HELLO:
		/* Send Client Hello */
		if ( ( rc = tls_send_client_hello ( tls ) ) != 0 ) {
			DBGC ( tls, "TLS %p could not send Client Hello: %s\n",
			       tls, strerror ( rc ) );
			goto err;
		}
		tls->tx_state = TLS_TX_NONE;
		break;
	case TLS_TX_CLIENT_KEY_EXCHANGE:
		/* Send Client Key Exchange */
		if ( ( rc = tls_send_client_key_exchange ( tls ) ) != 0 ) {
			DBGC ( tls, "TLS %p could send Client Key Exchange: "
			       "%s\n", tls, strerror ( rc ) );
			goto err;
		}
		tls->tx_state = TLS_TX_CHANGE_CIPHER;
		break;
	case TLS_TX_CHANGE_CIPHER:
		/* Send Change Cipher, and then change the cipher in use */
		if ( ( rc = tls_send_change_cipher ( tls ) ) != 0 ) {
			DBGC ( tls, "TLS %p could not send Change Cipher: "
			       "%s\n", tls, strerror ( rc ) );
			goto err;
		}
		if ( ( rc = tls_change_cipher ( tls,
						&tls->tx_cipherspec_pending,
						&tls->tx_cipherspec )) != 0 ){
			DBGC ( tls, "TLS %p could not activate TX cipher: "
			       "%s\n", tls, strerror ( rc ) );
			goto err;
		}
		tls->tx_seq = 0;
		tls->tx_state = TLS_TX_FINISHED;
		break;
	case TLS_TX_FINISHED:
		/* Send Finished */
		if ( ( rc = tls_send_finished ( tls ) ) != 0 ) {
			DBGC ( tls, "TLS %p could not send Finished: %s\n",
			       tls, strerror ( rc ) );
			goto err;
		}
		tls->tx_state = TLS_TX_NONE;
		break;
	case TLS_TX_DATA:
		/* Nothing to do */
		break;
	default:
		assert ( 0 );
	}

	return;

 err:
	tls_close ( tls, rc );
}

/******************************************************************************
 *
 * Instantiator
 *
 ******************************************************************************
 */

int add_tls ( struct xfer_interface *xfer, struct xfer_interface **next ) {
	struct tls_session *tls;

	/* Allocate and initialise TLS structure */
	tls = malloc ( sizeof ( *tls ) );
	if ( ! tls )
		return -ENOMEM;
	memset ( tls, 0, sizeof ( *tls ) );
	tls->refcnt.free = free_tls;
	filter_init ( &tls->plainstream, &tls_plainstream_operations,
		      &tls->cipherstream, &tls_cipherstream_operations,
		      &tls->refcnt );
	tls_clear_cipher ( tls, &tls->tx_cipherspec );
	tls_clear_cipher ( tls, &tls->tx_cipherspec_pending );
	tls_clear_cipher ( tls, &tls->rx_cipherspec );
	tls_clear_cipher ( tls, &tls->rx_cipherspec_pending );
	tls->client_random.gmt_unix_time = 0;
	tls_generate_random ( &tls->client_random.random,
			      ( sizeof ( tls->client_random.random ) ) );
	tls->pre_master_secret.version = htons ( TLS_VERSION_TLS_1_0 );
	tls_generate_random ( &tls->pre_master_secret.random,
			      ( sizeof ( tls->pre_master_secret.random ) ) );
	digest_init ( &md5_algorithm, tls->handshake_md5_ctx );
	digest_init ( &sha1_algorithm, tls->handshake_sha1_ctx );
	tls->tx_state = TLS_TX_CLIENT_HELLO;
	process_init ( &tls->process, tls_step, &tls->refcnt );

	/* Attach to parent interface, mortalise self, and return */
	xfer_plug_plug ( &tls->plainstream.xfer, xfer );
	*next = &tls->cipherstream.xfer;
	ref_put ( &tls->refcnt );
	return 0;
}