/*
 * tls_mbedtls.c
 *
 * TLS API for TCP/IP protocol stacks
 *
 * Copyright 2017 MZ Automation GmbH
 *
 * Implementation of the TLS abstraction layer for mbedtls
 *
 */

#include <string.h>

#include "tls_socket.h"
#include "hal_thread.h"
#include "lib_memory.h"
#include "linked_list.h"

#include "mbedtls/platform.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/certs.h"
#include "mbedtls/x509.h"
#include "mbedtls/ssl.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"

#if (CONFIG_DEBUG_TLS == 1)
#define DEBUG_PRINT(appId, fmt, ...) fprintf(stderr, "%s: " fmt, \
    appId, \
    __VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...) {do {} while(0);}
#endif


struct sTLSConfiguration {
    mbedtls_entropy_context entropy;
    mbedtls_ctr_drbg_context ctr_drbg;

    mbedtls_x509_crt ownCertificate;
    mbedtls_pk_context ownKey;

    mbedtls_x509_crt cacerts;

    mbedtls_ssl_config conf;
    LinkedList /* <mbedtls_x509_crt*> */ allowedCertificates;

    bool chainValidation;
    bool allowOnlyKnownCertificates;

    /* TLS session renegotioation time in milliseconds */
    int renegotiationTimeInMs;
};

struct sTLSSocket {
    mbedtls_ssl_context ssl;
    Socket socket;
    mbedtls_ssl_config conf;
    TLSConfiguration tlsConfig;
    bool storePeerCert;
    uint8_t* peerCert;
    int peerCertLength;
};

static bool
compareCertificates(mbedtls_x509_crt *crt1, mbedtls_x509_crt *crt2)
{
    if (crt1 != NULL && crt2 != NULL) {

        if (crt1->sig.len == crt2->sig.len) {
            if (memcmp(crt1->sig.p, crt2->sig.p, crt1->sig.len) == 0)
                return true;
        }

    }

    return false;
}

static int
verifyCertificate (void* parameter, mbedtls_x509_crt *crt, int certificate_depth, uint32_t *flags)
{
    TLSSocket self = (TLSSocket) parameter;

    DEBUG_PRINT("TLS", "Verify cert: depth %i\n", certificate_depth);

    DEBUG_PRINT("TLS", "   flags: %08x\n", *flags);

    char buffer[1024];

    mbedtls_x509_crt_info(buffer, 1023, "  ", crt);

    DEBUG_PRINT("TLS", "%s\n", buffer);

    if (self->tlsConfig->chainValidation == false) {
        if (certificate_depth != 0)
            *flags = 0;
    }

    if (certificate_depth == 0) {
        if (self->tlsConfig->allowOnlyKnownCertificates) {

            DEBUG_PRINT("TLS", "Check against list of allowed certs\n");

            bool certMatches = false;

            LinkedList certList = LinkedList_getNext(self->tlsConfig->allowedCertificates);

            while (certList) {
                mbedtls_x509_crt* allowedCert = (mbedtls_x509_crt*) LinkedList_getData(certList);

                DEBUG_PRINT("TLS", "Compare With:\n");
                mbedtls_x509_crt_info(buffer, 1023, "     ", allowedCert);
                DEBUG_PRINT("TLS", "%s\n", buffer);

                if (compareCertificates(allowedCert, crt)) {
                    certMatches = true;
                    break;
                }

                certList = LinkedList_getNext(certList);
            }

            if (certMatches)
                *flags = 0;
            else
                return 1;
        }

        if (self->storePeerCert) {

            if (*flags == 0) {

                self->peerCertLength = crt->raw.len;
                self->peerCert = (uint8_t*) GLOBAL_MALLOC(self->peerCertLength);
                memcpy(self->peerCert, crt->raw.p, self->peerCertLength);

            }

        }
    }

    return 0;
}



TLSConfiguration
TLSConfiguration_create()
{
    TLSConfiguration self = (TLSConfiguration) GLOBAL_MALLOC(sizeof(struct sTLSConfiguration));

    if (self != NULL) {
        mbedtls_ssl_config_init( &(self->conf) );
        mbedtls_x509_crt_init( &(self->ownCertificate) );
        mbedtls_x509_crt_init( &(self->cacerts));
        mbedtls_pk_init( &(self->ownKey) );
        mbedtls_entropy_init( &(self->entropy) );
        mbedtls_ctr_drbg_init( &(self->ctr_drbg) );

        /* WARINING is fixed to server! */
        mbedtls_ssl_config_defaults( &(self->conf),
                MBEDTLS_SSL_IS_SERVER,
                MBEDTLS_SSL_TRANSPORT_STREAM,
                MBEDTLS_SSL_PRESET_DEFAULT );

        mbedtls_ctr_drbg_seed( &(self->ctr_drbg), mbedtls_entropy_func, &(self->entropy), NULL, 0);
        mbedtls_ssl_conf_rng( &(self->conf), mbedtls_ctr_drbg_random, &(self->ctr_drbg) );

        mbedtls_ssl_conf_authmode(&(self->conf), MBEDTLS_SSL_VERIFY_REQUIRED);

        mbedtls_ssl_conf_renegotiation(&(self->conf), MBEDTLS_SSL_RENEGOTIATION_ENABLED);

        /* static int hashes[] = {3,4,5,6,7,8,0}; */
        /* mbedtls_ssl_conf_sig_hashes(&(self->conf), hashes); */

        self->allowedCertificates = LinkedList_create();

        /* default behavior is to allow all certificates that are signed by the CA */
        self->chainValidation = true;
        self->allowOnlyKnownCertificates = false;
    }

    return self;
}

void
TLSConfiguration_setClientMode(TLSConfiguration self)
{
    self->conf.endpoint = MBEDTLS_SSL_IS_CLIENT;
}

void
TLSConfiguration_setChainValidation(TLSConfiguration self, bool value)
{
    self->chainValidation = value;
}

void
TLSConfiguration_setAllowOnlyKnownCertificates(TLSConfiguration self, bool value)
{
    self->allowOnlyKnownCertificates = value;
}

bool
TLSConfiguration_setOwnCertificate(TLSConfiguration self, uint8_t* certificate, int certLen)
{
    int ret = mbedtls_x509_crt_parse(&(self->ownCertificate), certificate, certLen);

    if (ret != 0)
        DEBUG_PRINT("mbedtls_x509_crt_parse returned %d\n", ret);

    return (ret == 0);
}

bool
TLSConfiguration_setOwnCertificateFromFile(TLSConfiguration self, const char* filename)
{
    int ret =  mbedtls_x509_crt_parse_file(&(self->ownCertificate), filename);

    if (ret != 0)
        DEBUG_PRINT("TLS", "mbedtls_x509_crt_parse_file returned %d\n", ret);

    return (ret == 0);
}

bool
TLSConfiguration_setOwnKey(TLSConfiguration self, uint8_t* key, int keyLen, const char* keyPassword)
{
    int ret = mbedtls_pk_parse_key(&(self->ownKey), key, keyLen, (const uint8_t*) keyPassword, (keyPassword == NULL) ? 0 : strlen(keyPassword));

    if (ret != 0)
        DEBUG_PRINT("TLS", "mbedtls_pk_parse_key returned %d\n", ret);

    return (ret == 0);
}

bool
TLSConfiguration_setOwnKeyFromFile(TLSConfiguration self, const char* filename, const char* keyPassword)
{
    int ret =  mbedtls_pk_parse_keyfile(&(self->ownKey), filename, keyPassword);

    if (ret != 0)
        DEBUG_PRINT("TLS", "mbedtls_pk_parse_keyfile returned %d\n", ret);

    return (ret == 0);
}

bool
TLSConfiguration_addAllowedCertificate(TLSConfiguration self, uint8_t* certificate, int certLen)
{
    mbedtls_x509_crt* cert = (mbedtls_x509_crt*) GLOBAL_CALLOC(1, sizeof(mbedtls_x509_crt));

    int ret = mbedtls_x509_crt_parse(cert, certificate, certLen);

    if (ret == 0) {
        LinkedList_add(self->allowedCertificates, cert);
        return true;
    }
    else {
        GLOBAL_FREEMEM(cert);
        return false;
    }
}

bool
TLSConfiguration_addAllowedCertificateFromFile(TLSConfiguration self, const char* filename)
{
    mbedtls_x509_crt* cert = (mbedtls_x509_crt*) GLOBAL_CALLOC(1, sizeof(mbedtls_x509_crt));

    int ret = mbedtls_x509_crt_parse_file(cert, filename);

    if (ret == 0) {
        LinkedList_add(self->allowedCertificates, cert);
        return true;
    }
    else {
        GLOBAL_FREEMEM(cert);
        return false;
    }
}

bool
TLSConfiguration_addCACertificate(TLSConfiguration self, uint8_t* certificate, int certLen)
{
    int ret =  mbedtls_x509_crt_parse(&(self->cacerts), certificate, certLen);

    if (ret != 0) {
        DEBUG_PRINT("TLS", "mbedtls_x509_crt_parse returned %d\n", ret);
        return false;
    }

    return (ret == 0);
}

bool
TLSConfiguration_addCACertificateFromFile(TLSConfiguration self, const char* filename)
{
    int ret =  mbedtls_x509_crt_parse_file(&(self->cacerts), filename);

    if (ret != 0)
        DEBUG_PRINT("TLS", "mbedtls_x509_crt_parse returned %d\n", ret);

    return (ret == 0);
}

void
TLSConfiguration_setRenegotiationTime(TLSConfiguration self, int timeInMs)
{
    self->renegotiationTimeInMs = timeInMs;
}

void
TLSConfiguration_destroy(TLSConfiguration self)
{
    mbedtls_x509_crt_free(&(self->ownCertificate));
    mbedtls_x509_crt_free(&(self->cacerts));
    mbedtls_pk_free(&(self->ownKey));
    mbedtls_ssl_config_free(&(self->conf));

    LinkedList certElem = LinkedList_getNext(self->allowedCertificates);

    while (certElem) {
        mbedtls_x509_crt* cert = (mbedtls_x509_crt*) LinkedList_getData(certElem);

        mbedtls_x509_crt_free(cert);

        certElem = LinkedList_getNext(certElem);
    }

    LinkedList_destroy(self->allowedCertificates);

    GLOBAL_FREEMEM(self);
}


static int
readFunction(void* ctx, unsigned char* buf, size_t len)
{
    int ret = Socket_read((Socket) ctx, buf, len);

    if ((ret == 0) && (len > 0)) {
        return MBEDTLS_ERR_SSL_WANT_READ;
    }

    return ret;
}

TLSSocket
TLSSocket_create(Socket socket, TLSConfiguration configuration, bool storeClientCert)
{
    TLSSocket self = (TLSSocket) GLOBAL_CALLOC(1, sizeof(struct sTLSSocket));

    if (self != NULL) {

        self->socket = socket;
        self->tlsConfig = configuration;
        self->storePeerCert = storeClientCert;
        self->peerCert = NULL;
        self->peerCertLength = 0;

        memcpy(&(self->conf), &(configuration->conf), sizeof(mbedtls_ssl_config));

        mbedtls_ssl_conf_verify(&(self->conf), verifyCertificate, (void*) self);

        int ret;

        mbedtls_ssl_conf_ca_chain( &(self->conf), &(configuration->cacerts), NULL );

        ret = mbedtls_ssl_conf_own_cert( &(self->conf), &(configuration->ownCertificate), &(configuration->ownKey));

        if (ret != 0)
            DEBUG_PRINT("TLS", "mbedtls_ssl_conf_own_cert returned %d\n", ret);

        ret = mbedtls_ssl_setup( &(self->ssl), &(self->conf) );

        if (ret != 0)
            DEBUG_PRINT("TLS", "mbedtls_ssl_setup returned %d\n", ret);

        mbedtls_ssl_set_bio(&(self->ssl), socket, (mbedtls_ssl_send_t*) Socket_write,
                (mbedtls_ssl_recv_t*) readFunction, NULL);

        while( (ret = mbedtls_ssl_handshake(&(self->ssl)) ) != 0 )
        {
            if( ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE )
            {
                DEBUG_PRINT("TLS", "mbedtls_ssl_handshake returned %d\n\n", ret );

                GLOBAL_FREEMEM(self);

                return NULL;
            }
        }
    }

    return self;
}

uint8_t*
TLSSocket_getPeerCertificate(TLSSocket self, int* certSize)
{
    if (certSize)
        *certSize = self->peerCertLength;

    return self->peerCert;
}

bool
TLSSocket_performHandshake(TLSSocket self)
{
    if (mbedtls_ssl_renegotiate(&(self->ssl)) == 0)
        return true;
    else
        return false;
}

int
TLSSocket_read(TLSSocket self, uint8_t* buf, int size)
{
    int ret = mbedtls_ssl_read(&(self->ssl), buf, size);

    if ((ret == MBEDTLS_ERR_SSL_WANT_READ) || (ret == MBEDTLS_ERR_SSL_WANT_WRITE))
        return 0;

    if (ret < 0) {

        switch (ret)
        {
        case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
            DEBUG_PRINT("TLS", " connection was closed gracefully\n");
            return -1;

        case MBEDTLS_ERR_NET_CONN_RESET:
            DEBUG_PRINT("TLS", " connection was reset by peer\n");
            return -1;

        default:
            DEBUG_PRINT("TLS", " mbedtls_ssl_read returned -0x%x\n", -ret);
            return -1;
        }
    }

    return ret;
}

int
TLSSocket_write(TLSSocket self, uint8_t* buf, int size)
{
    int ret;
    int len = size;

    while ((ret = mbedtls_ssl_write(&(self->ssl), buf, len)) <= 0)
    {
        if (ret == MBEDTLS_ERR_NET_CONN_RESET)
        {
            DEBUG_PRINT("TLS", "peer closed the connection\n");
            return -1;
        }

        if ((ret != MBEDTLS_ERR_SSL_WANT_READ) && (ret != MBEDTLS_ERR_SSL_WANT_WRITE))
        {
            DEBUG_PRINT("TLS", "mbedtls_ssl_write returned %d\n", ret);
            return -1;
        }
    }

    len = ret;

    return len;
}

void
TLSSocket_close(TLSSocket self)
{
    int ret;

    /* TODO add timeout? */

    while ((ret = mbedtls_ssl_close_notify(&(self->ssl))) < 0)
    {
        if ((ret != MBEDTLS_ERR_SSL_WANT_READ) && (ret != MBEDTLS_ERR_SSL_WANT_WRITE))
        {
            DEBUG_PRINT("TLS", "mbedtls_ssl_close_notify returned %d\n", ret);
            break;
        }
    }

    Thread_sleep(10);

    mbedtls_ssl_config_free(&(self->conf));
    mbedtls_ssl_free(&(self->ssl));

    if (self->peerCert)
        GLOBAL_FREEMEM(self->peerCert);

    GLOBAL_FREEMEM(self);
}
