/*
 *  Created by Gavriloaie Eugen-Andrei (shiretu@gmail.com)
 *
 * The logical steps:
 * 1. initialize the SSL library
 * 2. creates an X509 key and cert
 * 3. creates an DTLS server SSL context
 * 4. Setup 2 memory BIO instances on the SSL context
 * 5. Feed the input BIO with a hardcoded "Client Hello" packet
 * 6. Call SSL_accept
 *
 * Wanted:
 * The output BIO should contain a packet ("Server Hello") to be sent over the wire
 *
 * Observed on OpenSSL 1.0.1k:
 * The output BIO is empty, the handshake never succeeds
 */

#include <openssl/conf.h>
#include <openssl/x509.h>
#include <openssl/ssl.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/engine.h>
#include <assert.h>

#define X509_KEY_SIZE 1024

void InitSSL();
void CleanupSSL();
EVP_PKEY * CreateCertificateKey();
X509 * CreateCertificate(EVP_PKEY *pKey);

int main(void) {
	//first, init the OpenSSL library
	InitSSL();

	//define a "Client Hello"
	unsigned char buffer[] = {
		0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00,
		0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x61, 0xfe, 0xff, 0xa1, 0xe1, 0xcb, 0x7f, 0x76,
		0xb9, 0x42, 0x7c, 0x97, 0xaa, 0x1e, 0x9f, 0x5e,
		0x18, 0x62, 0xe1, 0xe0, 0x29, 0x7b, 0xdc, 0xf3,
		0x57, 0x02, 0x89, 0xab, 0x82, 0x80, 0x91, 0x63,
		0x33, 0x98, 0xce, 0x00, 0x00, 0x00, 0x18, 0xc0,
		0x14, 0xc0, 0x0a, 0x00, 0x39, 0x00, 0x35, 0xc0,
		0x19, 0xc0, 0x13, 0xc0, 0x09, 0x00, 0x33, 0x00,
		0x2f, 0xc0, 0x18, 0x00, 0x0a, 0x00, 0xff, 0x01,
		0x00, 0x00, 0x1f, 0x00, 0x23, 0x00, 0x00, 0x00,
		0x0e, 0x00, 0x05, 0x00, 0x02, 0x00, 0x01, 0x00,
		0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0a,
		0x00, 0x08, 0x00, 0x06, 0x00, 0x17, 0x00, 0x18,
		0x00, 0x19
	};

	X509 *pX509 = NULL;
	EVP_PKEY *pX509Key = NULL;
	SSL_CTX *pSslContext = NULL;
	SSL *pSSL = NULL;
	BIO *pNetToSSLBIO = NULL;
	BIO *pSSLToNetBIO = NULL;
	BUF_MEM *pSSLBuffer = NULL;

	//initialize the X509 key and cert
	pX509Key = CreateCertificateKey();
	assert(pX509Key != NULL);
	pX509 = CreateCertificate(pX509Key);
	assert(pX509 != NULL);

	//create a SSL context blueprint and setup the X509 key and cert into it
	pSslContext = SSL_CTX_new(DTLSv1_server_method());
	assert(pSslContext != NULL);
	assert(SSL_CTX_use_certificate(pSslContext, pX509) == 1);
	assert(SSL_CTX_use_PrivateKey(pSslContext, pX509Key) == 1);
	assert(SSL_CTX_check_private_key(pSslContext) == 1);

	//create a SSL context using the blueprints above
	pSSL = SSL_new(pSslContext);
	assert(pSSL != NULL);

	//create 2 memory BIO to simulate the I/O

	//used to feed data FROM network TO SSL
	pNetToSSLBIO = BIO_new(BIO_s_mem());
	assert(pNetToSSLBIO != NULL);

	//used by SSL context to store data that needs to be eventually sent out to the network
	pSSLToNetBIO = BIO_new(BIO_s_mem());
	assert(pSSLToNetBIO != NULL);

	//set the BIOs into the SSL context
	SSL_set_bio(pSSL, pNetToSSLBIO, pSSLToNetBIO);

	//simulate network input by appending the data to the input BIO
	assert(BIO_write(pNetToSSLBIO, buffer, sizeof (buffer)) == sizeof (buffer));
	BIO_get_mem_ptr(pNetToSSLBIO, &pSSLBuffer);
	assert((pSSLBuffer != NULL)&&(pSSLBuffer->length == sizeof (buffer)));

	//call SSL_accept
	BIO_get_mem_ptr(pSSLToNetBIO, &pSSLBuffer);
	assert(pSSLBuffer != NULL);
	assert(pSSLBuffer->length == 0);
	SSL_accept(pSSL);

	//the output BIO MUST contain some data at this point
	BIO_get_mem_ptr(pSSLToNetBIO, &pSSLBuffer);
	assert(pSSLBuffer != NULL);
	assert(pSSLBuffer->length != 0);

	//cleanup
	SSL_free(pSSL);
	SSL_CTX_free(pSslContext);
	X509_free(pX509);
	EVP_PKEY_free(pX509Key);
	CleanupSSL();

	return 0;
}

void InitSSL() {
	//init the random numbers generator
	int length = 16;
	int *pBuffer = (int *) malloc(length * sizeof (int));
	while (RAND_status() == 0) {
		for (int i = 0; i < length; i++) {
			pBuffer[i] = rand();
		}

		RAND_seed(pBuffer, length * sizeof (int));
	}
	free(pBuffer);

	//init the SSL library
	SSL_library_init();

	//load SSL resources
	SSL_load_error_strings();
	ERR_load_SSL_strings();
	ERR_load_CRYPTO_strings();
	ERR_load_crypto_strings();
	OpenSSL_add_all_algorithms();
	OpenSSL_add_all_ciphers();
	OpenSSL_add_all_digests();
}

void CleanupSSL() {
	ERR_remove_state(0);
	ENGINE_cleanup();
	CONF_modules_unload(1);
	ERR_free_strings();
	EVP_cleanup();
	CRYPTO_cleanup_all_ex_data();
}

EVP_PKEY * CreateCertificateKey() {
	//create the keys container
	EVP_PKEY *pKey = EVP_PKEY_new();
	if (pKey == NULL)
		return NULL;

	//create the key itself which will be RSA
	RSA *pRSA = RSA_generate_key(
			X509_KEY_SIZE, /* number of bits for the key - 2048 is a sensible value */
			RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */
			NULL, /* callback - can be NULL if we aren't displaying progress */
			NULL /* callback argument - not needed in this case */
			);
	if (pRSA == NULL) {
		EVP_PKEY_free(pKey);
		return NULL;
	}

	//assign the key to the keys container
	if (EVP_PKEY_assign_RSA(pKey, pRSA) != 1) {
		EVP_PKEY_free(pKey);
		RSA_free(pRSA);
		return NULL;
	}

	//done
	return pKey;
}

X509 * CreateCertificate(EVP_PKEY *pKey) {
	//see if we have a key
	if (pKey == NULL)
		return NULL;

	//create the certificate
	X509 *pX509 = X509_new();
	if (pX509 == NULL)
		return NULL;

	X509_NAME *pSubjectProperties = NULL;
	if (
			//set the public key
			(X509_set_pubkey(pX509, pKey) != 1)

			//set the serial number
			|| (ASN1_INTEGER_set(X509_get_serialNumber(pX509), 1) != 1)

			//set validity period
			|| (X509_gmtime_adj(X509_get_notBefore(pX509), -1 * 24 * 3600) == NULL)
			|| (X509_gmtime_adj(X509_get_notAfter(pX509), 31536000L) == NULL)

			//get the subject for the x509 cert
			|| ((pSubjectProperties = X509_get_subject_name(pX509)) == NULL)

			//set the relevant properties on the subject
			|| (X509_NAME_add_entry_by_txt(pSubjectProperties, "C", MBSTRING_ASC, (unsigned char *) "CA", -1, -1, 0) != 1)
			|| (X509_NAME_add_entry_by_txt(pSubjectProperties, "O", MBSTRING_ASC, (unsigned char *) "Some org", -1, -1, 0) != 1)
			|| (X509_NAME_add_entry_by_txt(pSubjectProperties, "CN", MBSTRING_ASC, (unsigned char *) "example.com", -1, -1, 0) != 1)

			//this will be self signed. So issuer == subject
			|| (X509_set_issuer_name(pX509, pSubjectProperties) != 1)

			//sign the certificate
			|| (X509_sign(pX509, pKey, EVP_sha1()) == 0)
			) {
		X509_free(pX509);
		return NULL;
	}

	//done
	return pX509;
}
