/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *    Copyright 2021 (c) Christian von Arnim, ISW University of Stuttgart (for VDW and umati)
 *    Copyright 2022 (c) Wind River Systems, Inc.
 *
 */

#include <open62541/plugin/create_certificate.h>

#include "securitypolicy_openssl_common.h"
#include "ua_openssl_version_abstraction.h"

#if defined(UA_ENABLE_ENCRYPTION_OPENSSL) || defined(UA_ENABLE_ENCRYPTION_LIBRESSL)


#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>

/**
 * Join an array of UA_String to a single NULL-Terminated UA_String
 * separated by character sep
 */
static UA_StatusCode
join_string_with_sep(const UA_String *strings, size_t stringsSize,
                     char sep, UA_String *out) {
    if(!out)
        return UA_STATUSCODE_BADINVALIDARGUMENT;

    UA_String_clear(out);
    size_t totalSize = stringsSize;
    for(size_t iStr = 0; iStr < stringsSize; ++iStr) {
        totalSize += strings[iStr].length;
    }

    UA_ByteString_allocBuffer(out, totalSize);
    if(!out->data) {
        return UA_STATUSCODE_BADOUTOFMEMORY;
    }

    size_t pos = 0;
    for(size_t iStr = 0; iStr < stringsSize; ++iStr) {
        memcpy(&out->data[pos], strings[iStr].data, strings[iStr].length);
        pos += strings[iStr].length;
        out->data[pos] = (UA_Byte) sep;
        ++pos;
    }
    out->data[out->length-1] = 0;

    return UA_STATUSCODE_GOOD;
}

/**
 * Search for a character in a string (like strchr).
 * \todo Handle UTF-8
 *
 * \return index of the character or -1 on case of an error.
 */

static UA_Int32
UA_String_chr(const UA_String *pUaStr, char needl) {
    UA_Byte byteNeedl = (UA_Byte)needl;
    for(size_t i = 0; (size_t)i < pUaStr->length; ++i) {
        if(pUaStr->data[i] == byteNeedl) {
            return (UA_Int32) i;
        }
    }
    return -1;
}

/* char *value cannot be const due to openssl 1.0 compatibility */
static UA_StatusCode
add_x509V3ext(const UA_Logger *logger, X509 *x509, int nid, char *value) {
    X509_EXTENSION *ex;
    X509V3_CTX ctx;
    X509V3_set_ctx_nodb(&ctx);
    X509V3_set_ctx(&ctx, x509, x509, NULL, NULL, 0);
    ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value);
    if(!ex)
    {
#if UA_LOGLEVEL <= 300
        const char * file =  NULL;
        int line =  0;
        const char * data =  NULL;
        int flags =  0;
        get_error_line_data(&file, &line, &data, &flags);
        UA_LOG_INFO(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Internal SSL error file: %s:%d data: %s", file, line, data);
#endif
        return UA_STATUSCODE_BADINTERNALERROR;
    }
    X509_add_ext(x509, ex, -1);
    X509_EXTENSION_free(ex);
    return UA_STATUSCODE_GOOD;
}

#if (OPENSSL_VERSION_NUMBER >= 0x30000000L)

/* generate the RSA key */

static EVP_PKEY * UA_RSA_Generate_Key (size_t keySizeBits){
    return EVP_RSA_gen(keySizeBits);
}

#endif

UA_StatusCode
UA_CreateCertificate(const UA_Logger *logger, const UA_String *subject,
                     size_t subjectSize, const UA_String *subjectAltName,
                     size_t subjectAltNameSize, UA_CertificateFormat certFormat,
                     UA_KeyValueMap *params, UA_ByteString *outPrivateKey,
                     UA_ByteString *outCertificate) {
    if(!outPrivateKey || !outCertificate || !logger || !subjectAltName || !subject ||
       subjectAltNameSize == 0 || subjectSize == 0 ||
       (certFormat != UA_CERTIFICATEFORMAT_DER && certFormat != UA_CERTIFICATEFORMAT_PEM))
        return UA_STATUSCODE_BADINVALIDARGUMENT;

    /* Use the maximum size */
    UA_UInt16 keySizeBits = 4096;
    /* Default to 1 year */
    UA_UInt16 expiresInDays = 365;

    if(params) {
        const UA_UInt16 *keySizeBitsValue = (const UA_UInt16 *)UA_KeyValueMap_getScalar(
            params, UA_QUALIFIEDNAME(0, "key-size-bits"), &UA_TYPES[UA_TYPES_UINT16]);
        if(keySizeBitsValue)
            keySizeBits = *keySizeBitsValue;

        const UA_UInt16 *expiresInDaysValue = (const UA_UInt16 *)UA_KeyValueMap_getScalar(
            params, UA_QUALIFIEDNAME(0, "expires-in-days"), &UA_TYPES[UA_TYPES_UINT16]);
        if(expiresInDaysValue)
            expiresInDays = *expiresInDaysValue;
    }

    UA_ByteString_init(outPrivateKey);
    UA_ByteString_init(outCertificate);

    UA_String fullAltSubj = UA_STRING_NULL;
    UA_Int32 serial = 1;

    /** \TODO: Seed Random generator
    * See: (https://www.openssl.org/docs/man1.1.0/man3/RAND_add.html) */
    BIO *memCert = NULL;
    BIO *memPKey = NULL;

    UA_StatusCode errRet = UA_STATUSCODE_GOOD;

    X509 *x509 = X509_new();

#if (OPENSSL_VERSION_NUMBER >= 0x30000000L)
    EVP_PKEY *pkey = UA_RSA_Generate_Key(keySizeBits);
    if((pkey == NULL) || (x509 == NULL)) {
        errRet = UA_STATUSCODE_BADOUTOFMEMORY;
        goto cleanup;
    }    
#else
    BIGNUM *exponent = BN_new();
    EVP_PKEY *pkey = EVP_PKEY_new();
    RSA *rsa = RSA_new();
    if(!pkey || !x509 || !exponent || !rsa) {
        errRet = UA_STATUSCODE_BADOUTOFMEMORY;
        goto cleanup;
    }

    UA_LOG_INFO(logger, UA_LOGCATEGORY_SECURECHANNEL,
                "Create Certificate: Generating RSA key. This may take a while.");

    if(BN_set_word(exponent, RSA_F4) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting RSA exponent failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    if(RSA_generate_key_ex(rsa, (int) keySizeBits, exponent, NULL) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Generating RSA key failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    if(EVP_PKEY_assign_RSA(pkey, rsa) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Assign RSA key failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }
    /* rsa will be freed by pkey */
    rsa = NULL;

#endif  /* end of OPENSSL_VERSION_NUMBER >= 0x30000000L */

    /* x509v3 has version 2
     * (https://www.openssl.org/docs/man1.1.0/man3/X509_set_version.html) */
    if(X509_set_version(x509, 2) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting version failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    if(ASN1_INTEGER_set(X509_get_serialNumber(x509), serial) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting serial number failed.");
        /* Only memory errors are possible */
        errRet = UA_STATUSCODE_BADOUTOFMEMORY;
        goto cleanup;
    }

    if(X509_gmtime_adj(X509_get_notBefore(x509), 0) == NULL) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'not before' failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    if(X509_gmtime_adj(X509_get_notAfter(x509), (UA_Int64)60 * 60 * 24 * expiresInDays) ==
       NULL) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'not before' failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    if(X509_set_pubkey(x509, pkey) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting publik key failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    X509_NAME *name = X509_get_subject_name(x509);
    if(name == NULL) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Getting name failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    for(UA_UInt32 iSubject = 0; iSubject < subjectSize; ++iSubject) {
        UA_Int32 sep = UA_String_chr(&subject[iSubject], '=');
        char field[16];
        if(sep == -1 || sep == 0 ||
            ((size_t) sep == (subject[iSubject].length - 1)) || sep >= 15) {
            UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                         "Create Certificate: Subject must contain one '=' with "
                         "content before and after.");
            errRet = UA_STATUSCODE_BADINTERNALERROR;
            goto cleanup;
        }
        memcpy(field, subject[iSubject].data, (size_t) sep);
        field[sep] = 0;
        UA_Byte* pData = &subject[iSubject].data[sep + 1];
        if(X509_NAME_add_entry_by_txt(
               name, field, MBSTRING_ASC,
               (const unsigned char *)pData,
               (int) subject[iSubject].length - (int) sep - 1, -1, 0) != 1) {
            UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                         "Create Certificate: Setting subject failed.");
            errRet = UA_STATUSCODE_BADINTERNALERROR;
            goto cleanup;
        }
    }
    /* Self signed, so issuer == subject */
    if(X509_set_issuer_name(x509, name) != 1) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting name failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    errRet = add_x509V3ext(logger, x509, NID_basic_constraints, "CA:FALSE");
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'Basic Constraints' failed.");
        goto cleanup;
    }

    /* See https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 for
     * possible values */
    errRet = add_x509V3ext(logger, x509, NID_key_usage,
                           "digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyCertSign");
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'Key Usage' failed.");
        goto cleanup;
    }

    errRet = add_x509V3ext(logger, x509, NID_ext_key_usage, "serverAuth,clientAuth");
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'Extended Key Usage' failed.");
        goto cleanup;
    }

    errRet = add_x509V3ext(logger, x509, NID_subject_key_identifier, "hash");
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'Subject Key Identifier' failed.");
        goto cleanup;
    }

    errRet = join_string_with_sep(subjectAltName, subjectAltNameSize, ',', &fullAltSubj);
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Joining altSubject failed.");
        goto cleanup;
    }

    errRet = add_x509V3ext(logger, x509, NID_subject_alt_name, (char*) fullAltSubj.data);
    if(errRet != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Setting 'Subject Alternative Name' failed.");
        goto cleanup;
    }

    if(X509_sign(x509, pkey, EVP_sha256()) == 0) {
        UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                     "Create Certificate: Signing failed.");
        errRet = UA_STATUSCODE_BADINTERNALERROR;
        goto cleanup;
    }

    switch(certFormat) {
        case UA_CERTIFICATEFORMAT_DER: {
            unsigned char *p;
            /* Private Key */
            /* get length */
            outPrivateKey->length = (size_t)i2d_PrivateKey(pkey, NULL);
            if((int)outPrivateKey->length <= 0) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Create private DER key failed.");
                errRet = UA_STATUSCODE_BADINTERNALERROR;
                goto cleanup;
            }
            /* allocate buffer */
            UA_ByteString_allocBuffer(outPrivateKey, outPrivateKey->length);
            memset(outPrivateKey->data, 0, outPrivateKey->length);
            p = outPrivateKey->data;
            i2d_PrivateKey(pkey, &p);

            /* Certificate */
            /* get length */
            outCertificate->length = (size_t)i2d_X509(x509, NULL);
            if((int)outCertificate->length <= 0) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Create DER-certificate failed.");
                errRet = UA_STATUSCODE_BADINTERNALERROR;
                goto cleanup;
            }
            /* allocate buffer */
            UA_ByteString_allocBuffer(outCertificate, outCertificate->length);
            memset(outCertificate->data, 0, outCertificate->length);
            p = outCertificate->data;
            i2d_X509(x509, &p);
            break;
        }
        case UA_CERTIFICATEFORMAT_PEM: {
            /* Private Key */
            memPKey = BIO_new(BIO_s_mem());
            if(!memPKey) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Allocate Membuffer for PKey failed.");
                errRet = UA_STATUSCODE_BADOUTOFMEMORY;
                goto cleanup;
            }

            if(PEM_write_bio_PrivateKey(memPKey, pkey, NULL, NULL, 0, 0, NULL) != 1) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Generate PEM-PrivateKey failed.");
                errRet = UA_STATUSCODE_BADINTERNALERROR;
                goto cleanup;
            }

            UA_ByteString tmpPem = UA_BYTESTRING_NULL;
            tmpPem.length = (size_t) BIO_get_mem_data(memPKey, &tmpPem.data);
            errRet = UA_ByteString_copy(&tmpPem, outPrivateKey);
            if(errRet != UA_STATUSCODE_GOOD) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Copy PEM PKey failed.");
                goto cleanup;
            }

            /* Certificate */
            memCert = BIO_new(BIO_s_mem());
            if(!memCert) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Allocate Membuffer for Cert failed.");
                errRet = UA_STATUSCODE_BADOUTOFMEMORY;
                goto cleanup;
            }

            if(PEM_write_bio_X509(memCert, x509) != 1) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Generate PEM-Certifcate failed.");
                errRet = UA_STATUSCODE_BADINTERNALERROR;
                goto cleanup;
            }

            tmpPem.length = (size_t) BIO_get_mem_data(memCert, &tmpPem.data);
            errRet = UA_ByteString_copy(&tmpPem, outCertificate);
            if(errRet != UA_STATUSCODE_GOOD) {
                UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
                            "Create Certificate: Copy PEM Certificate failed.");
                goto cleanup;
            }
            break;
        }
    }

cleanup:
    UA_String_clear(&fullAltSubj);
#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
    RSA_free(rsa);
    BN_free(exponent);    
#endif
    X509_free(x509);
    EVP_PKEY_free(pkey);
    BIO_free(memCert);
    BIO_free(memPKey);
    return errRet;
}

#endif
