/*
 * Copyright (c) 2018-2020 Not for Radio, LLC
 *
 * See the file LICENSE in this distribution.
 *
 */
/* vim: set ts=4 sw=4 et: */

#include <fts.h>
#include <string.h>

#include <openssl/asn1t.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/ets_crypto.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

/*
 * Set to non-zero to enable error messages from ETS_load_keys() to standard
 * error.  Otherwise, any errors will wind up being silently ignored as
 * SSL_CTX_new() does not check the return value of ssl_ctx_system_config().
 */
#define ETS_LOAD_KEYS_DEBUG_ENABLE 1
#if ETS_LOAD_KEYS_DEBUG_ENABLE
#define ETS_LOAD_KEYS_DEBUG(...)                         \
    do {                                                 \
        fprintf(stderr, "%s:%d ", __func__, __LINE__);   \
        fprintf(stderr, __VA_ARGS__);                    \
        fprintf(stderr, "\n");                           \
    } while (0)
#endif

ASN1_SEQUENCE(ETS_VISIBILITY_INFORMATION) = {
    ASN1_SIMPLE(ETS_VISIBILITY_INFORMATION, fingerprint, ASN1_OCTET_STRING),
    ASN1_SIMPLE(ETS_VISIBILITY_INFORMATION, accessDescription, ASN1_UTF8STRING),
} ASN1_SEQUENCE_END(ETS_VISIBILITY_INFORMATION)

IMPLEMENT_ASN1_FUNCTIONS(ETS_VISIBILITY_INFORMATION)

static ETS_PKEY *ETS_PKEY_new(EVP_PKEY *);
static void ETS_PKEY_free(ETS_PKEY *);
static int ets_load_key(const char *, ETS_PKEYS *);

static void str_free(char *s)
{
    OPENSSL_free(s);
}

int
i2a_ETS_VISIBILITY_INFORMATION(BIO *bp, const ETS_VISIBILITY_INFORMATION *vi)
{
    ASN1_STRING_print_ex(bp, vi->fingerprint, ASN1_STRFLGS_DUMP_ALL);
    BIO_printf(bp, ":");
    ASN1_STRING_print_ex(bp, vi->accessDescription, ASN1_STRFLGS_UTF8_CONVERT);

    return BIO_get_mem_data(bp, NULL);
}

int
i2t_ETS_VISIBILITY_INFORMATION(char *buf, int buf_len, const ETS_VISIBILITY_INFORMATION *vi)
{
    unsigned char *p;
    BIO *tmp = BIO_new(BIO_s_mem());
    int tlen;

    if (tmp == NULL) {
        CRYPTOerr(CRYPTO_F_I2T_ETS_VISIBILITY_INFORMATION,
              ERR_R_BIO_LIB);
        return -1;
    }
    
    tlen = i2a_ETS_VISIBILITY_INFORMATION(tmp, vi);
    if (tlen >= 0) {
        BIO_get_mem_data(tmp, &p);
        tlen = BIO_snprintf(buf, buf_len, "%.*s", tlen, p);
    }
    BIO_free(tmp);

    return tlen;
}

ETS_PKEYS *
ETS_PKEYS_new_null(void)
{
    return (sk_ETS_PKEY_new_null());
}

void
ETS_PKEYS_free(ETS_PKEYS *ets_static_keys)
{
    sk_ETS_PKEY_pop_free(ets_static_keys, ETS_PKEY_free);
}

EVP_PKEY *
ETS_find_key(ETS_PKEYS *ets_static_keys, const unsigned char *fp, size_t fplen)
{
    int i;

    /* Ignore invalid fingerprints.  */
    if (ets_static_keys == NULL || fp == NULL) {
        CRYPTOerr(CRYPTO_F_ETS_FIND_KEY,
              ERR_R_PASSED_NULL_PARAMETER);
        return NULL;
    }

    if (fplen * 8 != ETS_FINGERPRINT_BITS) {
        CRYPTOerr(CRYPTO_F_ETS_FIND_KEY,
              ERR_R_PASSED_INVALID_ARGUMENT);
        return NULL;
    }

    for (i = 0; i < sk_ETS_PKEY_num(ets_static_keys); i++) {
        ETS_PKEY *pk;

        pk = sk_ETS_PKEY_value(ets_static_keys, i);
        if (memcmp(fp, pk->fingerprint, fplen) != 0)
            continue;

        /*
         * We have found the public key; we take a reference count and
         * pass it to the caller.
         */
        (void)EVP_PKEY_up_ref(pk->pkey);

        return pk->pkey;
    }
    return NULL;
}

int
ETS_fingerprint_pkey(EVP_PKEY *pkey, unsigned char *d, size_t dlen)
{
    unsigned char *p;
    size_t plen;

    if (pkey == NULL || d == NULL) {
        CRYPTOerr(CRYPTO_F_ETS_FINGERPRINT_PKEY,
              ERR_R_PASSED_NULL_PARAMETER);
        return 0;
    }
    if (dlen * 8 != ETS_FINGERPRINT_BITS) {
        CRYPTOerr(CRYPTO_F_ETS_FINGERPRINT_PKEY,
              ERR_R_PASSED_INVALID_ARGUMENT);
        return 0;
    }

    p = NULL;
    plen = EVP_PKEY_get1_tls_encodedpoint(pkey, &p);
    if (plen == 0) {
        CRYPTOerr(CRYPTO_F_ETS_FINGERPRINT_PKEY,
              ERR_R_EVP_LIB);
        return 0;
    }

    if (ETS_FINGERPRINT_BITS != SHA256_DIGEST_LENGTH * 8) {
        uint8_t raw[SHA256_DIGEST_LENGTH];

        (void)SHA256(p, plen, raw);

        memcpy(d, raw, dlen); /* Truncate.  */
    } else {
        (void)SHA256(p, plen, d);
    }

    OPENSSL_free(p);

    return 1;
}

int
ETS_load_keys(const char *path_list, ETS_PKEYS **ets_static_keysp)
{
    char * path_arg[2] = { NULL, NULL };
    STACK_OF(OPENSSL_STRING) *paths;
    OPENSSL_STRING path;
    const char *search_start;
    const char *start;
    const char *end;
    ETS_PKEYS *ets_static_keys;
    FTSENT *fe;
    FTS *fts;

    if (path_list == NULL || ets_static_keysp == NULL) {
        ETS_LOAD_KEYS_DEBUG("NULL parameter"); 
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
              ERR_R_PASSED_NULL_PARAMETER);
        return 0;
    }

    paths = sk_OPENSSL_STRING_new_null();
    if (paths == NULL) {
        ETS_LOAD_KEYS_DEBUG("malloc failure"); 
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
              ERR_R_MALLOC_FAILURE);
        return 0;
    }

    ets_static_keys = NULL;
    /*
     * Extract paths from colon-separated path_list, ignoring escaped colons
     * ("\:") along the way.
     */
    start = path_list;
    do {
        while (*start == ':')
            start++;
        if (*start == '\0')
            break;
        /*
         * At this point, start is a non-empty string that does not begin
         * with ':'.
         */
        search_start = start + 1;
        do {
            end = strchr(search_start, ':');
            if (end != NULL)
                search_start = end + 1;
        } while (end != NULL && *(end - 1) == '\\');
        /*
         * end now points to first non-escaped colon after start, or is NULL
         * if none.
         */
        if (end == NULL)
            path = strdup(start);
        else {
            path = strndup(start, end - start);
            start = end + 1;
        }
        if (path == NULL) {
            ETS_LOAD_KEYS_DEBUG("malloc failure"); 
            CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
                  ERR_R_MALLOC_FAILURE);
            goto err;
        }
        if (!sk_OPENSSL_STRING_push(paths, path)) {
            ETS_LOAD_KEYS_DEBUG("malloc failure"); 
            CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
                  ERR_R_MALLOC_FAILURE);
            goto err;
        }
    } while (end != NULL);
            
    ets_static_keys = ETS_PKEYS_new_null();
    if (ets_static_keys == NULL) {
        ETS_LOAD_KEYS_DEBUG("malloc failure"); 
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
              ERR_R_MALLOC_FAILURE);
        goto err;
    }

    while ((path = sk_OPENSSL_STRING_shift(paths)) != NULL) {
        path_arg[0] = path;
        fts = fts_open(path_arg, FTS_LOGICAL, NULL);
        if (fts == NULL) {
            ETS_LOAD_KEYS_DEBUG("fts_open() failed"); 
            CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
                  ERR_R_SYS_LIB);
            goto err;
        }

        while ((fe = fts_read(fts)) != NULL) {
            switch (fe->fts_info) {
            case FTS_F:
                break;
            case FTS_ERR:
                fts_close(fts);
                ETS_LOAD_KEYS_DEBUG("fts_read() failed"); 
                CRYPTOerr(CRYPTO_F_ETS_LOAD_KEYS,
                      ERR_R_SYS_LIB);
                goto err;
            default:
                continue;
            }

            /*
             * Try to load each file as an EVP_PKEY PEM file.
             */
            if (!ets_load_key(fe->fts_accpath, ets_static_keys))
                ETS_LOAD_KEYS_DEBUG("could not load %s as a private key - ignoring",
                                    fe->fts_accpath); 
        }

        fts_close(fts);
    }

    if (sk_ETS_PKEY_num(ets_static_keys) == 0) {
        ETS_PKEYS_free(ets_static_keys);
        ets_static_keys = NULL;
    }

    *ets_static_keysp = ets_static_keys;

    return 1;

err:
    sk_OPENSSL_STRING_pop_free(paths, str_free);
    ETS_PKEYS_free(ets_static_keys);
    return 0;
}

static ETS_PKEY *
ETS_PKEY_new(EVP_PKEY *pkey)
{
    ETS_PKEY *pk;

    pk = OPENSSL_malloc(sizeof *pk);
    if (pk == NULL) {
        CRYPTOerr(CRYPTO_F_ETS_PKEY_NEW,
              ERR_R_MALLOC_FAILURE);
        return 0;
    }
    if (!ETS_fingerprint_pkey(pkey, pk->fingerprint, sizeof pk->fingerprint)) {
        OPENSSL_free(pk);
        CRYPTOerr(CRYPTO_F_ETS_PKEY_NEW,
              ERR_R_INTERNAL_ERROR);
        return 0;
    }

    /*
     * The caller loans us a reference to pkey.
     */
    pk->pkey = pkey;

    return pk;
}

static void
ETS_PKEY_free(ETS_PKEY *pk)
{
    if (pk == NULL)
        return;
    EVP_PKEY_free(pk->pkey);
    OPENSSL_free(pk);
}

static int
ets_load_key(const char *file, ETS_PKEYS *ets_static_keys)
{
    EVP_PKEY *pkey;
    ETS_PKEY *pk;
    FILE *fp;

    fp = fopen(file, "r");
    if (fp == NULL) {
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEY,
              ERR_R_SYS_LIB);
        return 0;
    }

    pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
    if (pkey == NULL) {
        fclose(fp);
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEY,
              ERR_R_PEM_LIB);
        return 0;
    }

    fclose(fp);

    /*
     * Generate and cache the fingerprint for this private key.
     */
    pk = ETS_PKEY_new(pkey);
    if (pk == NULL) {
        EVP_PKEY_free(pkey);
        CRYPTOerr(CRYPTO_F_ETS_LOAD_KEY,
              ERR_R_INTERNAL_ERROR);
        return 0;
    }

    /*
     * We hold a reference to pkey, which is loaned to the list supplied by
     * the caller.  Our reference comes from PEM_read_PrivateKey.
     *
     * The caller will call ETS_PKEYS_free when done with all keys to drop
     * those references (eventually.)
     */
    sk_ETS_PKEY_push(ets_static_keys, pk);

    return 1;
}

/* Local Variables:       */
/* c-basic-offset: 4      */
/* tab-width: 4           */
/* indent-tabs-mode: nil  */
/* End:                   */
