/*------------------------------------------------------------------
 * us5052.c - Unit Tests for User Story 5052 - Enable EST over CoAP when
 *            in server mode
 *
 * August, 2018
 *
 * Copyright (c) 2018 by cisco Systems, Inc.
 * All rights reserved.
 *------------------------------------------------------------------
 */
#include <stdio.h>
#ifndef WIN32
#include <unistd.h>
#endif 
#include <est.h>
#include <curl/curl.h>
#include "curl_utils.h"
#include "test_utils.h"
#include "st_server.h"
#include <openssl/ssl.h>

#ifdef HAVE_CUNIT
#include "CUnit/Basic.h"
#include "CUnit/Automated.h"
#endif

#define EST_UT_MAX_CMD_LEN       512
#define US5052_SERVER_IP         "127.0.0.1"
#define US5052_CSSL_NOT_SET_MSG "The path for the openssl installation used by"\
" the python emulator was not specified.\n Please set the environment variable"\
" COAP_EMU_SSL"

#ifndef WIN32
#define US5052_CACERTS       "CA/estCA/cacert.crt"
#define US5052_EXPLICIT_CERT "US5052/cert-RA.pem" 
#define US5052_EXPLICIT_KEY  "US5052/key-RA.pem"
#define US5052_TRUSTED_CERT  "CA/trustedcerts.crt"
#define US5052_SERVER_CERT_AND_KEY "CA/estCA/private/estservercertandkey.pem"
#define US5052_HUGE_TRUSTED_CERT "CA/mfgCAs/trustedcertswithhugesudichain.crt"
#define US5052_HUGE_CERT_AND_KEY "CA/mfgCAs/sudiCA/private/certandkey2-estHugeSUDI.pem"
#define US5052_HUGER_TRUSTED_CERT "CA/mfgCAs/trustedcertswithhugersudichain.crt"
#define US5052_HUGER_CERT_AND_KEY "CA/mfgCAs/sudiCA/private/certandkey2-estHugerSUDI.pem"
#define US5052_MIDSIZE_CSR "US5052/midsize.csr"
#else
#define US5052_CACERTS       "CA\\estCA\\cacert.crt"
#define US5052_EXPLICIT_CERT "US5052\\cert-RA.pem" 
#define US5052_EXPLICIT_KEY  "US5052\\key-RA.pem"
#define US5052_TRUSTED_CERT  "CA\\trustedcerts.crt"
#define US5052_SERVER_CERT_AND_KEY "CA\\estCA\\private\\estservercertandkey.pem"
#define US5052_HUGE_TRUSTED_CERT "CA\\mfgCAs\\trustedcertswithhugesudichain.crt"
#define US5052_HUGE_CERT_AND_KEY "CA\\mfgCAs\\sudiCA\\private\\certandkey2-estHugeSUDI.pem"
#define US5052_HUGER_TRUSTED_CERT "CA\\mfgCAs\\trustedcertswithhugersudichain.crt"
#define US5052_HUGER_CERT_AND_KEY "CA\\mfgCAs\\sudiCA\\private\\certandkey2-estHugerSUDI.pem"
#define US5052_MIDSIZE_CSR "US5052\\midsize.csr"
#endif

static unsigned char *cacerts = NULL;
static int cacerts_len = 0;

static char *cssl_emulator_path = NULL;

static int coap_mode_support = 0;

CU_pSuite coap_sanity_psuite = NULL;

/*
 * Used to start server in CoAP mode
 */
#define US5052_SERVER_PORT      29001
/*
 * Used to test the CoAP init API function
 */
#define US5052_API_TEST_PORT     29002

#if 0
static EVP_PKEY * generate_private_key (void)
{
    RSA *rsa = RSA_new();
    BIGNUM *bn = BN_new();
    EVP_PKEY *pkey;

    /*
     * create an RSA keypair and assign them to a PKEY and return it.
     */
    BN_set_word(bn, 0x10001);
    RSA_generate_key_ex(rsa, 1024, bn, NULL);

    pkey = EVP_PKEY_new();
    if (pkey == NULL) {
        printf("\nError allocating PKEY structure for new key pair\n");
        return NULL;
    }
    if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
        printf("\nError assigning RSA key pair to PKEY structure\n");
        return NULL;
    }

    RSA_free(rsa);
    BN_free(bn);

    return (pkey);
}

/*
 * Callback function passed to est_client_init()
 */
static int client_manual_cert_verify (X509 *cur_cert, int openssl_cert_error)
{
    BIO * bio_err;
    bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
    int approve = 0;
    const ASN1_BIT_STRING *cur_cert_sig;
    const X509_ALGOR *cur_cert_sig_alg;

    /*
     * Print out the specifics of this cert
     */
    printf(
        "%s: OpenSSL/EST server cert verification failed with the following error: openssl_cert_error = %d (%s)\n",
        __FUNCTION__,
        openssl_cert_error,
        X509_verify_cert_error_string(openssl_cert_error));

    printf("Failing Cert:\n");
    X509_print_fp(stdout, cur_cert);
    /*
     * Next call prints out the signature which can be used as the fingerprint
     * This fingerprint can be checked against the anticipated value to determine
     * whether or not the server's cert should be approved.
     */
#ifdef HAVE_OLD_OPENSSL    
    X509_get0_signature((ASN1_BIT_STRING **)&cur_cert_sig,
                        (X509_ALGOR **)&cur_cert_sig_alg, cur_cert);
    X509_signature_print(bio_err, (X509_ALGOR *)cur_cert_sig_alg,
                         (ASN1_BIT_STRING *)cur_cert_sig);
#else    
    X509_get0_signature(&cur_cert_sig, &cur_cert_sig_alg, cur_cert);
    X509_signature_print(bio_err, cur_cert_sig_alg, cur_cert_sig);
#endif    

    if (openssl_cert_error == X509_V_ERR_UNABLE_TO_GET_CRL) {
        approve = 1;
    }

    BIO_free(bio_err);

    return approve;
}

static FILE *outfile;
static size_t write_func (void *ptr, size_t size, size_t nmemb, void *userdata)
{
    size_t written;
    written = fwrite(ptr, size, nmemb, outfile);
    return written;
}
#endif

static void us5052_clean (void)
{
}

static int us5052_start_server (int manual_enroll, int nid)
{
    int rv;

    rv = st_start_coap(US5052_SERVER_PORT,
                       US5052_SERVER_CERT_AND_KEY,
                       US5052_SERVER_CERT_AND_KEY,
                       "US5052 test realm",
                       US5052_CACERTS,
                       US5052_TRUSTED_CERT,
                       "CA/estExampleCA.cnf",
                       manual_enroll,
                       0,
                       nid);
    if (rv != EST_ERR_NONE) {
        printf("Failed to start CoAP st_server\n");
    }

    st_set_default_est_event_callbacks();
    
    return rv;

}

/*
 * This routine is called when CUnit initializes this test
 * suite.  This can be used to allocate data or open any
 * resources required for all the test cases.
 */
static int us5052_init_suite (void)
{
    int rv;

    est_init_logger(EST_LOG_LVL_INFO, NULL);

    /*
     * Read in the CA certificates
     */
    cacerts_len = read_binary_file(US5052_CACERTS, &cacerts);
    if (cacerts_len <= 0) {
        return 1;
    }

    us5052_clean();

    /*
     * Start an instance of the EST server with
     * automatic enrollment enabled.
     */
    rv = us5052_start_server(0, 0);

    cssl_emulator_path = getenv("COAP_EMU_SSL");
    
    return rv;
}

/*
 * This routine is called when CUnit uninitializes this test
 * suite.  This can be used to deallocate data or close any
 * resources that were used for the test cases.
 */
static int us5052_destroy_suite (void)
{
    st_stop();
    free(cacerts);
    return 0;
}

/*
 * Test the CoAP Initialization API function,
 * est_server_coap_init_start()
 */
static void us5052_test1 (void)
{
    EST_CTX *ectx;
    BIO *certin, *keyin;
    X509 *x;
    EVP_PKEY *priv_key;
    int coap_rc;

    unsigned char *trustcerts = NULL;
    int trustcerts_len = 0;

    unsigned char *cacerts = NULL;
    int cacerts_len = 0;
    
    LOG_FUNC_NM;
    
    /*
     * Set up the EST library in server mode.  This requires a number
     * of values to be passed to est_server_init()
     */

    /*
     * The server's ID certificate.
     */
    certin = BIO_new(BIO_s_file());
    if (BIO_read_filename(certin, US5052_SERVER_CERT_AND_KEY) <= 0) {
        printf("Unable to read server certificate file %s\n",
               US5052_SERVER_CERT_AND_KEY);
        return;
    }    
    /*
     * Read the file, which is expected to be PEM encoded.
     */
    x = PEM_read_bio_X509(certin, NULL, NULL, NULL);
    if (x == NULL) {
        printf("Error while reading PEM encoded server certificate file %s\n",
               US5052_SERVER_CERT_AND_KEY);
        return;
    }
    BIO_free(certin);

    /*
     * Read in the server's private key
     */
    keyin = BIO_new(BIO_s_file());
    if (BIO_read_filename(keyin, US5052_SERVER_CERT_AND_KEY) <= 0) {
        printf("Unable to read server private key file %s\n",
               US5052_SERVER_CERT_AND_KEY);
        return;
    }
    /*
     * Read in the private key file, which is expected to be a PEM
     * encoded private key.
     */
    priv_key = PEM_read_bio_PrivateKey(keyin, NULL, NULL, NULL);
    if (priv_key == NULL) {
        printf("Error while reading PEM encoded private key file %s\n",
               US5052_SERVER_CERT_AND_KEY);
        return;
    }
    BIO_free(keyin);
    
    /*
     * CA certs to use as the trust store
     */
    trustcerts_len = read_binary_file(US5052_TRUSTED_CERT, &trustcerts);
    if (trustcerts_len <= 0) {
        printf("Trusted certs file %s could not be read\n",
               US5052_TRUSTED_CERT);
        return;
    }

    /*
     * Read in the CA certs to use as response to /cacerts responses
     */
    cacerts_len = read_binary_file(US5052_CACERTS, &cacerts);
    if (cacerts_len <= 0) {
        printf("CA chain file %s file could not be read\n", US5052_CACERTS);
        return;
    }

    /*
     * Initialize the library and get an EST context
     */
    ectx = est_server_init(trustcerts, trustcerts_len, cacerts, cacerts_len,
                           EST_CERT_FORMAT_PEM, "estrealm", x, priv_key);
    if (!ectx) {
        printf("Unable to initialize EST context.  Aborting!!!\n");
        return;
    }

    /*
     * Now test est_server_coap_init_and_start()
     *
     * No context
     */
    coap_rc = est_server_coap_init_start(NULL, US5052_API_TEST_PORT);
    CU_ASSERT(coap_rc == EST_ERR_NO_CTX);
    if (coap_rc != EST_ERR_NO_CTX) {
        printf("Failed to detect no EST context passed in\n");
    }

    /*
     * Invalid port number
     */
    coap_rc = est_server_coap_init_start(ectx, -1);
    CU_ASSERT(coap_rc == EST_ERR_INVALID_PORT_NUM);
    if (coap_rc != EST_ERR_INVALID_PORT_NUM) {
        printf("Failed to detect no port number passed in\n");
    }

    /*
     * Port number too large
     */
    coap_rc = est_server_coap_init_start(ectx, 65536);
    CU_ASSERT(coap_rc == EST_ERR_INVALID_PORT_NUM);
    if (coap_rc != EST_ERR_INVALID_PORT_NUM) {
        printf("Failed to detect port number too large\n");
    }
}


/*
 * Test /cacerts (crts) over coap.
 */
static void us5052_test2 (void)
{
    char cmd[EST_UT_MAX_CMD_LEN];
    int rv = 0;
        
    LOG_FUNC_NM;

    CU_ASSERT(cssl_emulator_path != NULL);
    if (!cssl_emulator_path) {
        printf(US5052_CSSL_NOT_SET_MSG);
        return;
    }

    /*
     * Build the est client over coap emulator command and issue it
     */
    snprintf(cmd, EST_UT_MAX_CMD_LEN,
             "export LD_LIBRARY_PATH=%s/lib; export PATH=%s/bin:$PATH; "
             "/usr/bin/env python ../util/est_coap_client.py --test GET_CACERTS --port %d "
             " --key %s --cert %s --cacert %s --debug --csr %s",
             cssl_emulator_path,
             cssl_emulator_path,
             US5052_SERVER_PORT,
             US5052_SERVER_CERT_AND_KEY, US5052_SERVER_CERT_AND_KEY,
             US5052_CACERTS,
             US5052_MIDSIZE_CSR);

    printf("%s\n", cmd);

    rv = system(cmd);
    CU_ASSERT(rv == 0);

}


/*
 * Test /simpleenroll (sen) over coap.
 */
static void us5052_test3 (void)
{
    char cmd[EST_UT_MAX_CMD_LEN];
    int rv = 0;
        
    LOG_FUNC_NM;
    
    CU_ASSERT(cssl_emulator_path != NULL);
    if (!cssl_emulator_path) {
        printf(US5052_CSSL_NOT_SET_MSG);
        return;
    }
    
    /*
     * Build the est client over coap emulator command and issue it
     */
    snprintf(cmd, EST_UT_MAX_CMD_LEN,
             "export LD_LIBRARY_PATH=%s/lib; export PATH=%s/bin:$PATH; "
             "/usr/bin/env python ../util/est_coap_client.py --test SIMPLE_ENROLL --port %d "
             " --key %s --cert %s --cacert %s --debug --csr %s",
             cssl_emulator_path,
             cssl_emulator_path,
             US5052_SERVER_PORT,
             US5052_SERVER_CERT_AND_KEY, US5052_SERVER_CERT_AND_KEY,
             US5052_CACERTS,
             US5052_MIDSIZE_CSR);

    printf("%s\n", cmd);

    rv = system(cmd);
    CU_ASSERT(rv == 0);

}


/*
 * Test /simplereenroll (sren) over coap.
 */
static void us5052_test4 (void)
{
    char cmd[EST_UT_MAX_CMD_LEN];
    int rv = 0;
        
    LOG_FUNC_NM;
    
    CU_ASSERT(cssl_emulator_path != NULL);
    if (!cssl_emulator_path) {
        printf(US5052_CSSL_NOT_SET_MSG);
        return;
    }
    
    /*
     * Build the est client over coap emulator command and issue it
     */
    snprintf(cmd, EST_UT_MAX_CMD_LEN,
             "export LD_LIBRARY_PATH=%s/lib; export PATH=%s/bin:$PATH; "
             "/usr/bin/env python ../util/est_coap_client.py --test SIMPLE_REENROLL --port %d "
             " --key %s --cert %s --cacert %s --debug --csr %s",
             cssl_emulator_path,
             cssl_emulator_path,
             US5052_SERVER_PORT,
             US5052_SERVER_CERT_AND_KEY, US5052_SERVER_CERT_AND_KEY,
             US5052_CACERTS,
             US5052_MIDSIZE_CSR);

    printf("%s\n", cmd);

    rv = system(cmd);
    CU_ASSERT(rv == 0);

}

/*
 * Test handshake behavior with a huge certificate record
 */
static void us5052_test5 (void)
{
    char cmd[EST_UT_MAX_CMD_LEN];
    int rv = 0;
        
    LOG_FUNC_NM;
    
    CU_ASSERT(cssl_emulator_path != NULL);
    if (!cssl_emulator_path) {
        printf(US5052_CSSL_NOT_SET_MSG);
        return;
    }
    
    /*
     * Build the est client over coap emulator command and issue it.
     * This will pass because it is just shy of the max record size
     */
    snprintf(cmd, EST_UT_MAX_CMD_LEN,
             "export LD_LIBRARY_PATH=%s/lib; export PATH=%s/bin:$PATH; "
             "/usr/bin/env python ../util/est_coap_client.py --test SIMPLE_REENROLL --port %d "
             " --key %s --cert %s --cacert %s --debug --csr %s",
             cssl_emulator_path,
             cssl_emulator_path,
             US5052_SERVER_PORT,
             US5052_HUGE_CERT_AND_KEY, US5052_HUGE_CERT_AND_KEY,
             US5052_HUGE_TRUSTED_CERT,
             US5052_MIDSIZE_CSR);

    printf("%s\n", cmd);

    rv = system(cmd);
    CU_ASSERT(rv == 0);

//NOTE: Removed because openssl on client emulator can't send too big a cert
#if 0
    /*
     * Build the est client over coap emulator command and issue it.
     * This will fail during the client send because the cert over the max 
     * record size
     */
    snprintf(cmd, EST_UT_MAX_CMD_LEN,
             "export LD_LIBRARY_PATH=%s/lib; export PATH=%s/bin:$PATH; "
             "/usr/bin/env python ../util/est_coap_client.py --test SIMPLE_REENROLL --port %d "
             " --key %s --cert %s --cacert %s --debug --csr %s",
             cssl_emulator_path,
             cssl_emulator_path,
             US5052_SERVER_PORT,
             US5052_HUGER_CERT_AND_KEY, US5052_HUGER_CERT_AND_KEY,
             US5052_HUGER_TRUSTED_CERT,
             US5052_MIDSIZE_CSR);

    printf("%s\n", cmd);

    rv = system(cmd);
    CU_ASSERT(rv != 0);
#endif
}

/* The main() function for setting up and running the tests.
 * Returns a CUE_SUCCESS on successful running, another
 * CUnit error code on failure.
 */
int us5052_add_suite (void)
{
#ifdef HAVE_CUNIT
    CU_pSuite pSuite = NULL;

    /*
     * check to see if coap mode support has been compiled in
     */
    if (!coap_mode_supported(US5052_SERVER_CERT_AND_KEY, US5052_TRUSTED_CERT,
                             US5052_CACERTS, US5052_API_TEST_PORT)) {
        printf("CoAP mode is not supported in this build of EST.  Rebuild "
               "using --with-libcoap-dir= \n");
        coap_mode_support = 0;
        return 0;
    }
    coap_mode_support = 1;
    
    /* add a suite to the registry */
    pSuite = CU_add_suite("us5052_CoAP_API",
            us5052_init_suite,
            us5052_destroy_suite);
    if (NULL == pSuite) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    if (coap_mode_support) {

        /* add the tests to the suite */
        if ((NULL == CU_add_test(pSuite, "Test CoAP init API", us5052_test1)) ||
            (NULL == CU_add_test(pSuite, "Test /crts request", us5052_test2)) ||
            (NULL == CU_add_test(pSuite, "Test /sen request", us5052_test3))  ||
            (NULL == CU_add_test(pSuite, "Test /sren request", us5052_test4)) ||
            (NULL == CU_add_test(pSuite, "Test huge handshake record", us5052_test5))
            ) {
            CU_cleanup_registry();
            return CU_get_error();
        }
    }

    coap_sanity_psuite = pSuite;
     
    return CUE_SUCCESS;
#endif
}

