/*
 * TLSv1 credentials
 * Copyright (c) 2006-2009, Jouni Malinen <j@w1.fi>
 *
 * This software may be distributed under the terms of the BSD license.
 * See README for more details.
 */

#include "wpa/includes.h"

#include "wpa/common.h"
#include "wpa2/utils/base64.h"
#include "crypto/crypto.h"
#include "wpa2/tls/x509v3.h"
#include "wpa2/tls/tlsv1_cred.h"

#ifdef MEMLEAK_DEBUG
static const char mem_debug_file[] ICACHE_RODATA_ATTR = __FILE__;
#endif


struct tlsv1_credentials* tlsv1_cred_alloc(void)
{
    struct tlsv1_credentials* cred;
    cred = (struct tlsv1_credentials*)os_zalloc(sizeof(*cred));
    return cred;
}


void tlsv1_cred_free(struct tlsv1_credentials* cred)
{
    if (cred == NULL) {
        return;
    }

    x509_certificate_chain_free(cred->trusted_certs);
    x509_certificate_chain_free(cred->cert);
    crypto_private_key_free(cred->key);
    os_free(cred->dh_p);
    os_free(cred->dh_g);
    os_free(cred);
}


static int tlsv1_add_cert_der(struct x509_certificate** chain,
                              const u8* buf, size_t len)
{
    struct x509_certificate* cert, *p;
    char name[128];

    cert = x509_certificate_parse(buf, len);

    if (cert == NULL) {
        wpa_printf(MSG_INFO, "TLSv1: %s - failed to parse certificate",
                   __func__);
        return -1;
    }

    p = *chain;

    while (p && p->next) {
        p = p->next;
    }

    if (p && x509_name_compare(&cert->subject, &p->issuer) == 0) {
        /*
         * The new certificate is the issuer of the last certificate in
         * the chain - add the new certificate to the end.
         */
        p->next = cert;
    } else {
        /* Add to the beginning of the chain */
        cert->next = *chain;
        *chain = cert;
    }

    x509_name_string(&cert->subject, name, sizeof(name));
    wpa_printf(MSG_DEBUG, "TLSv1: Added certificate: %s", name);

    return 0;
}


static const char* pem_cert_begin = "-----BEGIN CERTIFICATE-----";
static const char* pem_cert_end = "-----END CERTIFICATE-----";
static const char* pem_key_begin = "-----BEGIN RSA PRIVATE KEY-----";
static const char* pem_key_end = "-----END RSA PRIVATE KEY-----";
static const char* pem_key2_begin = "-----BEGIN PRIVATE KEY-----";
static const char* pem_key2_end = "-----END PRIVATE KEY-----";
static const char* pem_key_enc_begin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
static const char* pem_key_enc_end = "-----END ENCRYPTED PRIVATE KEY-----";


static const u8* search_tag(const char* tag, const u8* buf, size_t len)
{
    size_t i, plen;

    plen = os_strlen(tag);

    if (len < plen) {
        return NULL;
    }

    for (i = 0; i < len - plen; i++) {
        if (os_memcmp(buf + i, tag, plen) == 0) {
            return buf + i;
        }
    }

    return NULL;
}


static int tlsv1_add_cert(struct x509_certificate** chain,
                          const u8* buf, size_t len)
{
    const u8* pos, *end;
    unsigned char* der;
    size_t der_len;

    pos = search_tag(pem_cert_begin, buf, len);

    if (!pos) {
        wpa_printf(MSG_DEBUG, "TLSv1: No PEM certificate tag found - "
                   "assume DER format");
        return tlsv1_add_cert_der(chain, buf, len);
    }

    wpa_printf(MSG_DEBUG, "TLSv1: Converting PEM format certificate into "
               "DER format");

    while (pos) {
        pos += os_strlen(pem_cert_begin);
        end = search_tag(pem_cert_end, pos, buf + len - pos);

        if (end == NULL) {
            wpa_printf(MSG_INFO, "TLSv1: Could not find PEM "
                       "certificate end tag (%s)", pem_cert_end);
            return -1;
        }

        der = base64_decode(pos, end - pos, &der_len);

        if (der == NULL) {
            wpa_printf(MSG_INFO, "TLSv1: Could not decode PEM "
                       "certificate");
            return -1;
        }

        if (tlsv1_add_cert_der(chain, der, der_len) < 0) {
            wpa_printf(MSG_INFO, "TLSv1: Failed to parse PEM "
                       "certificate after DER conversion");
            os_free(der);
            return -1;
        }

        os_free(der);

        end += os_strlen(pem_cert_end);
        pos = search_tag(pem_cert_begin, end, buf + len - end);
    }

    return 0;
}


static int tlsv1_set_cert_chain(struct x509_certificate** chain,
                                const char* cert, const u8* cert_blob,
                                size_t cert_blob_len)
{
    if (cert_blob) {
        return tlsv1_add_cert(chain, cert_blob, cert_blob_len);
    }

    if (cert) {
        u8* buf = NULL;
        size_t len = 0;
        int ret;

        //buf = (u8 *) os_readfile(cert, &len);
        if (buf == NULL) {
            wpa_printf(MSG_INFO, "TLSv1: Failed to read '%s'",
                       cert);
            return -1;
        }

        ret = tlsv1_add_cert(chain, buf, len);
        os_free(buf);
        return ret;
    }

    return 0;
}


/**
 * tlsv1_set_ca_cert - Set trusted CA certificate(s)
 * @cred: TLSv1 credentials from tlsv1_cred_alloc()
 * @cert: File or reference name for X.509 certificate in PEM or DER format
 * @cert_blob: cert as inlined data or %NULL if not used
 * @cert_blob_len: ca_cert_blob length
 * @path: Path to CA certificates (not yet supported)
 * Returns: 0 on success, -1 on failure
 */
int tlsv1_set_ca_cert(struct tlsv1_credentials* cred, const char* cert,
                      const u8* cert_blob, size_t cert_blob_len,
                      const char* path)
{
    if (tlsv1_set_cert_chain(&cred->trusted_certs, cert,
                             cert_blob, cert_blob_len) < 0) {
        return -1;
    }

    if (path) {
        /* TODO: add support for reading number of certificate files */
        wpa_printf(MSG_INFO, "TLSv1: Use of CA certificate directory "
                   "not yet supported");
        return -1;
    }

    return 0;
}


/**
 * tlsv1_set_cert - Set certificate
 * @cred: TLSv1 credentials from tlsv1_cred_alloc()
 * @cert: File or reference name for X.509 certificate in PEM or DER format
 * @cert_blob: cert as inlined data or %NULL if not used
 * @cert_blob_len: cert_blob length
 * Returns: 0 on success, -1 on failure
 */
int tlsv1_set_cert(struct tlsv1_credentials* cred, const char* cert,
                   const u8* cert_blob, size_t cert_blob_len)
{
    return tlsv1_set_cert_chain(&cred->cert, cert,
                                cert_blob, cert_blob_len);
}


static struct crypto_private_key* tlsv1_set_key_pem(const u8* key, size_t len)
{
    const u8* pos, *end;
    unsigned char* der;
    size_t der_len;
    struct crypto_private_key* pkey;

    pos = search_tag(pem_key_begin, key, len);

    if (!pos) {
        pos = search_tag(pem_key2_begin, key, len);

        if (!pos) {
            return NULL;
        }

        pos += os_strlen(pem_key2_begin);
        end = search_tag(pem_key2_end, pos, key + len - pos);

        if (!end) {
            return NULL;
        }
    } else {
        const u8* pos2;
        pos += os_strlen(pem_key_begin);
        end = search_tag(pem_key_end, pos, key + len - pos);

        if (!end) {
            return NULL;
        }

        pos2 = search_tag("Proc-Type: 4,ENCRYPTED", pos, end - pos);

        if (pos2) {
            wpa_printf(MSG_DEBUG, "TLSv1: Unsupported private key "
                       "format (Proc-Type/DEK-Info)");
            return NULL;
        }
    }

    der = base64_decode(pos, end - pos, &der_len);

    if (!der) {
        return NULL;
    }

    pkey = crypto_private_key_import(der, der_len, NULL);
    os_free(der);
    return pkey;
}


static struct crypto_private_key* tlsv1_set_key_enc_pem(const u8* key,
        size_t len,
        const char* passwd)
{
    const u8* pos, *end;
    unsigned char* der;
    size_t der_len;
    struct crypto_private_key* pkey;

    if (passwd == NULL) {
        return NULL;
    }

    pos = search_tag(pem_key_enc_begin, key, len);

    if (!pos) {
        return NULL;
    }

    pos += os_strlen(pem_key_enc_begin);
    end = search_tag(pem_key_enc_end, pos, key + len - pos);

    if (!end) {
        return NULL;
    }

    der = base64_decode(pos, end - pos, &der_len);

    if (!der) {
        return NULL;
    }

    pkey = crypto_private_key_import(der, der_len, passwd);
    os_free(der);
    return pkey;
}


static int tlsv1_set_key(struct tlsv1_credentials* cred,
                         const u8* key, size_t len, const char* passwd)
{
    cred->key = crypto_private_key_import(key, len, passwd);

    if (cred->key == NULL) {
        cred->key = tlsv1_set_key_pem(key, len);
    }

    if (cred->key == NULL) {
        cred->key = tlsv1_set_key_enc_pem(key, len, passwd);
    }

    if (cred->key == NULL) {
        wpa_printf(MSG_INFO, "TLSv1: Failed to parse private key");
        return -1;
    }

    return 0;
}


/**
 * tlsv1_set_private_key - Set private key
 * @cred: TLSv1 credentials from tlsv1_cred_alloc()
 * @private_key: File or reference name for the key in PEM or DER format
 * @private_key_passwd: Passphrase for decrypted private key, %NULL if no
 * passphrase is used.
 * @private_key_blob: private_key as inlined data or %NULL if not used
 * @private_key_blob_len: private_key_blob length
 * Returns: 0 on success, -1 on failure
 */
int tlsv1_set_private_key(struct tlsv1_credentials* cred,
                          const char* private_key,
                          const char* private_key_passwd,
                          const u8* private_key_blob,
                          size_t private_key_blob_len)
{
    crypto_private_key_free(cred->key);
    cred->key = NULL;

    if (private_key_blob)
        return tlsv1_set_key(cred, private_key_blob,
                             private_key_blob_len,
                             private_key_passwd);

    if (private_key) {
        u8* buf = NULL;
        size_t len = 0;
        int ret;

        //buf = (u8 *) os_readfile(private_key, &len);
        if (buf == NULL) {
            wpa_printf(MSG_INFO, "TLSv1: Failed to read '%s'",
                       private_key);
            return -1;
        }

        ret = tlsv1_set_key(cred, buf, len, private_key_passwd);
        os_free(buf);
        return ret;
    }

    return 0;
}


static int tlsv1_set_dhparams_der(struct tlsv1_credentials* cred,
                                  const u8* dh, size_t len)
{
    struct asn1_hdr hdr;
    const u8* pos, *end;

    pos = dh;
    end = dh + len;

    /*
     * DHParameter ::= SEQUENCE {
     *   prime INTEGER, -- p
     *   base INTEGER, -- g
     *   privateValueLength INTEGER OPTIONAL }
     */

    /* DHParamer ::= SEQUENCE */
    if (asn1_get_next(pos, len, &hdr) < 0 ||
            hdr.class != ASN1_CLASS_UNIVERSAL ||
            hdr.tag != ASN1_TAG_SEQUENCE) {
        wpa_printf(MSG_DEBUG, "DH: DH parameters did not start with a "
                   "valid SEQUENCE - found class %d tag 0x%x",
                   hdr.class, hdr.tag);
        return -1;
    }

    pos = hdr.payload;

    /* prime INTEGER */
    if (asn1_get_next(pos, end - pos, &hdr) < 0) {
        return -1;
    }

    if (hdr.class != ASN1_CLASS_UNIVERSAL ||
            hdr.tag != ASN1_TAG_INTEGER) {
        wpa_printf(MSG_DEBUG, "DH: No INTEGER tag found for p; "
                   "class=%d tag=0x%x", hdr.class, hdr.tag);
        return -1;
    }

    wpa_hexdump(MSG_MSGDUMP, "DH: prime (p)", hdr.payload, hdr.length);

    if (hdr.length == 0) {
        return -1;
    }

    os_free(cred->dh_p);
    cred->dh_p = os_malloc(hdr.length);

    if (cred->dh_p == NULL) {
        return -1;
    }

    os_memcpy(cred->dh_p, hdr.payload, hdr.length);
    cred->dh_p_len = hdr.length;
    pos = hdr.payload + hdr.length;

    /* base INTEGER */
    if (asn1_get_next(pos, end - pos, &hdr) < 0) {
        return -1;
    }

    if (hdr.class != ASN1_CLASS_UNIVERSAL ||
            hdr.tag != ASN1_TAG_INTEGER) {
        wpa_printf(MSG_DEBUG, "DH: No INTEGER tag found for g; "
                   "class=%d tag=0x%x", hdr.class, hdr.tag);
        return -1;
    }

    wpa_hexdump(MSG_MSGDUMP, "DH: base (g)", hdr.payload, hdr.length);

    if (hdr.length == 0) {
        return -1;
    }

    os_free(cred->dh_g);
    cred->dh_g = os_malloc(hdr.length);

    if (cred->dh_g == NULL) {
        return -1;
    }

    os_memcpy(cred->dh_g, hdr.payload, hdr.length);
    cred->dh_g_len = hdr.length;

    return 0;
}


static const char* pem_dhparams_begin = "-----BEGIN DH PARAMETERS-----";
static const char* pem_dhparams_end = "-----END DH PARAMETERS-----";


static int tlsv1_set_dhparams_blob(struct tlsv1_credentials* cred,
                                   const u8* buf, size_t len)
{
    const u8* pos, *end;
    unsigned char* der;
    size_t der_len;

    pos = search_tag(pem_dhparams_begin, buf, len);

    if (!pos) {
        wpa_printf(MSG_DEBUG, "TLSv1: No PEM dhparams tag found - "
                   "assume DER format");
        return tlsv1_set_dhparams_der(cred, buf, len);
    }

    wpa_printf(MSG_DEBUG, "TLSv1: Converting PEM format dhparams into DER "
               "format");

    pos += os_strlen(pem_dhparams_begin);
    end = search_tag(pem_dhparams_end, pos, buf + len - pos);

    if (end == NULL) {
        wpa_printf(MSG_INFO, "TLSv1: Could not find PEM dhparams end "
                   "tag (%s)", pem_dhparams_end);
        return -1;
    }

    der = base64_decode(pos, end - pos, &der_len);

    if (der == NULL) {
        wpa_printf(MSG_INFO, "TLSv1: Could not decode PEM dhparams");
        return -1;
    }

    if (tlsv1_set_dhparams_der(cred, der, der_len) < 0) {
        wpa_printf(MSG_INFO, "TLSv1: Failed to parse PEM dhparams "
                   "DER conversion");
        os_free(der);
        return -1;
    }

    os_free(der);

    return 0;
}


/**
 * tlsv1_set_dhparams - Set Diffie-Hellman parameters
 * @cred: TLSv1 credentials from tlsv1_cred_alloc()
 * @dh_file: File or reference name for the DH params in PEM or DER format
 * @dh_blob: DH params as inlined data or %NULL if not used
 * @dh_blob_len: dh_blob length
 * Returns: 0 on success, -1 on failure
 */
int tlsv1_set_dhparams(struct tlsv1_credentials* cred, const char* dh_file,
                       const u8* dh_blob, size_t dh_blob_len)
{
    if (dh_blob) {
        return tlsv1_set_dhparams_blob(cred, dh_blob, dh_blob_len);
    }

    if (dh_file) {
        u8* buf = NULL;
        size_t len = 0;
        int ret;

        //buf = (u8 *) os_readfile(dh_file, &len);
        if (buf == NULL) {
            wpa_printf(MSG_INFO, "TLSv1: Failed to read '%s'",
                       dh_file);
            return -1;
        }

        ret = tlsv1_set_dhparams_blob(cred, buf, len);
        os_free(buf);
        return ret;
    }

    return 0;
}
