/*
 *   stunnel       TLS offloading and load-balancing proxy
 *   Copyright (C) 1998-2025 Michal Trojnara <Michal.Trojnara@stunnel.org>
 *
 *   This program is free software; you can redistribute it and/or modify it
 *   under the terms of the GNU General Public License as published by the
 *   Free Software Foundation; either version 2 of the License, or (at your
 *   option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *   See the GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License along
 *   with this program; if not, see <http://www.gnu.org/licenses>.
 *
 *   Linking stunnel statically or dynamically with other modules is making
 *   a combined work based on stunnel. Thus, the terms and conditions of
 *   the GNU General Public License cover the whole combination.
 *
 *   In addition, as a special exception, the copyright holder of stunnel
 *   gives you permission to combine stunnel with free software programs or
 *   libraries that are released under the GNU LGPL and with code included
 *   in the standard release of OpenSSL under the OpenSSL License (or
 *   modified versions of such code, with unchanged license). You may copy
 *   and distribute such a system following the terms of the GNU GPL for
 *   stunnel and the licenses of the other code concerned.
 *
 *   Note that people who make modified versions of stunnel are not obligated
 *   to grant this special exception for their modified versions; it is their
 *   choice whether to do so. The GNU General Public License gives permission
 *   to release a modified version without this exception; this exception
 *   also makes it possible to release a modified version which carries
 *   forward this exception.
 */

#include "prototypes.h"

    /* global OpenSSL initialization: compression, engine, entropy */
#if OPENSSL_VERSION_NUMBER>=0x10100000L
NOEXPORT void cb_new_auth(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
        int idx, long argl, void *argp);
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
NOEXPORT int cb_new_auth(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
        int idx, long argl, void *argp);
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
#if OPENSSL_VERSION_NUMBER>=0x30000000L
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, const CRYPTO_EX_DATA *from,
    void **from_d, int idx, long argl, void *argp);
#elif OPENSSL_VERSION_NUMBER>=0x10100000L
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, const CRYPTO_EX_DATA *from,
    void *from_d, int idx, long argl, void *argp);
#else
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, CRYPTO_EX_DATA *from,
    void *from_d, int idx, long argl, void *argp);
#endif
NOEXPORT void cb_free_addr(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
    int idx, long argl, void *argp);
#ifndef OPENSSL_NO_COMP
NOEXPORT void compression_init(void);
NOEXPORT int compression_set(GLOBAL_OPTIONS *);
NOEXPORT void compression_list(void);
#endif
NOEXPORT int prng_init(GLOBAL_OPTIONS *);
NOEXPORT int add_rand_file(GLOBAL_OPTIONS *, const char *);
NOEXPORT void update_rand_file(const char *);

int index_ssl_cli, index_ssl_ctx_opt;
int index_session_authenticated, index_session_connect_address;
#ifndef OPENSSL_NO_COMP
NOEXPORT STACK_OF(SSL_COMP) *comp_methods[STUNNEL_COMPS];
#endif

#ifdef USE_FIPS
int fips_default(void) {
    static int cache=-1;

    if(cache == -1) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        cache=EVP_default_properties_is_fips_enabled(NULL);
#else /* OPENSSL_VERSION_NUMBER < 0x30000000L */
        cache=FIPS_mode();
#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
    }
    return cache;
}
#endif /* USE_FIPS */

int fips_available(void) { /* either FIPS provider or container is available */
#ifdef USE_FIPS
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    static OSSL_PROVIDER *fips=NULL;

    if(!fips)
        fips=OSSL_PROVIDER_try_load(NULL, "fips", 1);
    return fips && OSSL_PROVIDER_available(NULL, "fips");
#else /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
    /* checking for OPENSSL_FIPS would be less precise, because it only
     * depends on the compiled, and not on the running OpenSSL version */
    return strstr(OpenSSL_version(OPENSSL_VERSION), "-fips") != NULL ||
        strstr(OpenSSL_version(OPENSSL_VERSION), "FIPS") != NULL; /* RHEL */
#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
#else /* USE_FIPS */
    return 0;
#endif /* USE_FIPS */
}

/* initialize libcrypto before invoking API functions that require it */
void crypto_init(void) {
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    OPENSSL_INIT_SETTINGS *conf;
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
#ifdef USE_WIN32
    TCHAR stunnel_exe_path[MAX_PATH];
    LPTSTR c;
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    char *stunnel_dir, *path;
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */

    /* identify stunnel_exe_path */
    GetModuleFileName(0, stunnel_exe_path, MAX_PATH);
    c=_tcsrchr(stunnel_exe_path, TEXT('\\')); /* last backslash */
    if(c) { /* found */
        *c=TEXT('\0'); /* truncate the program name */
        c=_tcsrchr(stunnel_exe_path, TEXT('\\')); /* previous backslash */
        if(c && !_tcscmp(c+1, TEXT("bin")))
            *c=TEXT('\0'); /* truncate "bin" */
    }

    /* set current working directory */
#ifndef _WIN32_WCE
    if(!SetCurrentDirectory(stunnel_exe_path)) {
        LPTSTR errmsg=str_tprintf(TEXT("Cannot set directory to %s"),
            stunnel_exe_path);
        message_box(errmsg, MB_ICONERROR);
        str_free(errmsg);
        exit(1);
    }
    /* try to enter the "config" subdirectory, ignore the result */
    SetCurrentDirectory(TEXT("config"));
#endif

    stunnel_dir=tstr2str(stunnel_exe_path);
    if(!stunnel_dir) /* fail-safe */
        stunnel_dir=str_dup("..");

    /* setup the environment */
    path=str_printf("%s\\engines", stunnel_dir);
    _putenv_s("OPENSSL_ENGINES", path);
    str_free(path);
    path=str_printf("%s\\ossl-modules", stunnel_dir);
    _putenv_s("OPENSSL_MODULES", path);
    str_free(path);
    path=str_printf("%s\\config\\openssl.cnf", stunnel_dir);
    _putenv_s("OPENSSL_CONF", path);
    str_free(path);
#endif /* USE_WIN32 */

    /* initialize OpenSSL */
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    conf=OPENSSL_INIT_new();
#ifdef USE_WIN32
    path=str_printf("%s\\config\\openssl.cnf", stunnel_dir);
    if(!OPENSSL_INIT_set_config_filename(conf, path)) {
        s_log(LOG_ERR, "Failed to set OpenSSL configuration file name");
    }
    str_free(path);
#endif /* USE_WIN32 */
    OPENSSL_init_crypto(
        OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_LOAD_CONFIG, conf);
    OPENSSL_INIT_free(conf);
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    OPENSSL_config(NULL);
    SSL_load_error_strings();
    SSL_library_init();
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */

#ifdef USE_WIN32
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    /* setup the default search path for providers */
    /* WARNING: OpenSSL needs to already be initialized */
    path=str_printf("%s\\ossl-modules", stunnel_dir);
    if(!OSSL_PROVIDER_set_default_search_path(NULL, path)) {
        s_log(LOG_ERR, "Failed to set default ossl-modules path");
    }
    str_free(path);
#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    str_free(stunnel_dir);
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
#endif /* USE_WIN32 */
}

/* initialize lbssl before parsing the configuration file */
int ssl_init(void) {
    index_ssl_cli=SSL_get_ex_new_index(0,
        strdup("CLI pointer"), NULL, NULL, NULL);
    index_ssl_ctx_opt=SSL_CTX_get_ex_new_index(0,
        strdup("SERVICE_OPTIONS pointer"), NULL, NULL, NULL);
    index_session_authenticated=SSL_SESSION_get_ex_new_index(0,
        strdup("session authenticated"), cb_new_auth, NULL, NULL);
    index_session_connect_address=SSL_SESSION_get_ex_new_index(0,
        strdup("session connect address"), NULL, cb_dup_addr, cb_free_addr);
    if(index_ssl_cli<0 || index_ssl_ctx_opt<0 ||
            index_session_authenticated<0 ||
            index_session_connect_address<0) {
        s_log(LOG_ERR, "Application specific data initialization failed");
        return 1;
    }
#ifndef OPENSSL_NO_DH
    dh_params=get_dh2048();
    if(!dh_params) {
        s_log(LOG_ERR, "Failed to get default DH parameters");
        return 1;
    }
#endif /* OPENSSL_NO_DH */
#ifndef OPENSSL_NO_COMP
    compression_init();
#endif /* OPENSSL_NO_COMP */
    return 0;
}

#ifndef OPENSSL_NO_DH
#if OPENSSL_VERSION_NUMBER<0x10100000L
/* this is needed for dhparam.c generated with OpenSSL >= 1.1.0
 * to be linked against the older versions */
int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g) {
    if(!p || !g) /* q is optional */
        return 0;
    BN_free(dh->p);
    BN_free(dh->q);
    BN_free(dh->g);
    dh->p=p;
    dh->q=q;
    dh->g=g;
    if(q)
        dh->length=BN_num_bits(q);
    return 1;
}
#endif
#endif

#if OPENSSL_VERSION_NUMBER>=0x10100000L
NOEXPORT void cb_new_auth(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
        int idx, long argl, void *argp) {
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
NOEXPORT int cb_new_auth(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
        int idx, long argl, void *argp) {
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    (void)parent; /* squash the unused parameter warning */
    (void)ptr; /* squash the unused parameter warning */
    (void)argl; /* squash the unused parameter warning */
    s_log(LOG_DEBUG, "Initializing application specific data for %s",
        (char *)argp);
    if(!CRYPTO_set_ex_data(ad, idx, (void *)(-1)))
        sslerror("CRYPTO_set_ex_data");
#if OPENSSL_VERSION_NUMBER<0x10100000L
    return 1; /* success */
#endif /* OPENSSL_VERSION_NUMBER<0x10100000L */
}

#if OPENSSL_VERSION_NUMBER>=0x30000000L
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, const CRYPTO_EX_DATA *from,
        void **from_d, int idx, long argl, void *argp) {
#elif OPENSSL_VERSION_NUMBER>=0x10100000L
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, const CRYPTO_EX_DATA *from,
        void *from_d, int idx, long argl, void *argp) {
#else
NOEXPORT int cb_dup_addr(CRYPTO_EX_DATA *to, CRYPTO_EX_DATA *from,
        void *from_d, int idx, long argl, void *argp) {
#endif
    SOCKADDR_UNION *src, *dst;
    socklen_t len;

    (void)to; /* squash the unused parameter warning */
    (void)from; /* squash the unused parameter warning */
    (void)idx; /* squash the unused parameter warning */
    (void)argl; /* squash the unused parameter warning */
    s_log(LOG_DEBUG, "Duplicating application specific data for %s",
        (char *)argp);
    src=*(void **)from_d;
    len=addr_len(src);
    dst=str_alloc_detached((size_t)len);
    memcpy(dst, src, (size_t)len);
    *(void **)from_d=dst;
    return 1;
}

NOEXPORT void cb_free_addr(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
        int idx, long argl, void *argp) {
    (void)parent; /* squash the unused parameter warning */
    (void)ad; /* squash the unused parameter warning */
    (void)idx; /* squash the unused parameter warning */
    (void)argl; /* squash the unused parameter warning */
    s_log(LOG_DEBUG, "Deallocating application specific data for %s",
        (char *)argp);
    str_free(ptr);
}

int ssl_configure(GLOBAL_OPTIONS *global) { /* configure global TLS settings */
#ifdef USE_FIPS
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    if(global->option.fips) {
        if(!EVP_default_properties_is_fips_enabled(NULL)) {
            if(!fips_available()) {
                sslerror("FIPS PROVIDER");
                return 1;
            }
            if(!EVP_default_properties_enable_fips(NULL, 1) ||
                    !EVP_default_properties_is_fips_enabled(NULL)) {
                s_log(LOG_ERR, "Enabling FIPS provider failed");
                return 1;
            }
            s_log(LOG_NOTICE, "FIPS provider enabled");
        }
    } else {
        if(EVP_default_properties_is_fips_enabled(NULL)) {
            if(!EVP_default_properties_enable_fips(NULL, 0) ||
                    EVP_default_properties_is_fips_enabled(NULL)) {
                s_log(LOG_ERR, "Disabling FIPS provider failed");
                return 1;
            }
            s_log(LOG_NOTICE, "FIPS provider disabled");
        }
    }
#else /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
    if(FIPS_mode()!=global->option.fips) {
        RAND_set_rand_method(NULL); /* reset RAND methods */
        if(!FIPS_mode_set(global->option.fips)) {
#if OPENSSL_VERSION_NUMBER>=0x10100000L
            OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
#else
            ERR_load_crypto_strings();
#endif
            sslerror("FIPS_mode_set");
            return 1;
        }
    }
#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
    s_log(LOG_NOTICE, "FIPS mode %s",
        global->option.fips ? "enabled" : "disabled");
#endif /* USE_FIPS */

#ifndef OPENSSL_NO_COMP
    if(compression_set(global))
        return 1;
    compression_list();
#endif /* OPENSSL_NO_COMP */
    if(prng_init(global))
        return 1;

#ifndef OPENSSL_NO_ENGINE
    ENGINE_load_builtin_engines();
#endif

    /* cryptographic algoritms can only be configured once,
     * after all the engines are initialized */
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, NULL);
#else
    OpenSSL_add_all_algorithms();
#endif
    return 0; /* SUCCESS */
}

#ifndef OPENSSL_NO_COMP

#if OPENSSL_VERSION_NUMBER<0x10100000L

NOEXPORT int COMP_get_type(const COMP_METHOD *meth) {
    return meth->type;
}

NOEXPORT const char *SSL_COMP_get0_name(const SSL_COMP *comp) {
    return comp->name;
}

NOEXPORT int SSL_COMP_get_id(const SSL_COMP *comp) {
    return comp->id;
}

#endif /* OPENSSL_VERSION_NUMBER<0x10100000L */

#if OPENSSL_VERSION_NUMBER<0x10002000L

NOEXPORT void SSL_COMP_set0_compression_methods(STACK_OF(SSL_COMP) *new_meths) {
    STACK_OF(SSL_COMP) *old_meths;
    int num, i;

    old_meths=SSL_COMP_get_compression_methods();
    sk_SSL_COMP_zero(old_meths);

    num=sk_SSL_COMP_num(new_meths);
    for(i=0; i<num; ++i)
        sk_SSL_COMP_push(old_meths, sk_SSL_COMP_value(new_meths, i));
}

#endif /* OPENSSL_VERSION_NUMBER>=0x10002000L */

NOEXPORT void compression_init(void) {
    STACK_OF(SSL_COMP) *methods;
    COMP_METHOD *zlib;

    memset(comp_methods, 0, sizeof comp_methods);

    /* setup COMP_NONE */
    comp_methods[COMP_NONE]=sk_SSL_COMP_new_null();

    /* setup COMP_DEFLATE (RFC 1951) */
    methods=SSL_COMP_get_compression_methods();
    if(!methods || !sk_SSL_COMP_num(methods))
        return;
    comp_methods[COMP_DEFLATE]=sk_SSL_COMP_dup(methods);

    /* setup COMP_ZLIB (DEFLATE + obsolete private id 0xe0) */
    zlib=COMP_zlib();
    if(!zlib || COMP_get_type(zlib)==NID_undef)
        return;
    if(SSL_COMP_add_compression_method(0xe0, zlib))
        return;
    comp_methods[COMP_ZLIB]=methods; /* reuse the memory */
}

NOEXPORT int compression_set(GLOBAL_OPTIONS *global) {
    if(!comp_methods[global->compression]) {
        s_log(LOG_ERR, "Configured compression is unsupported by OpenSSL");
        return 1;
    }
    SSL_COMP_set0_compression_methods(comp_methods[global->compression]);
    return 0; /* success */
}

NOEXPORT void compression_list(void) {
    STACK_OF(SSL_COMP) *methods;
    int num_methods, i;

    methods=SSL_COMP_get_compression_methods();
    num_methods=sk_SSL_COMP_num(methods);
    if(!num_methods) {
        s_log(LOG_INFO, "Compression disabled");
        return;
    }
    s_log(LOG_INFO, "Compression enabled: %d method%s",
        num_methods, num_methods==1 ? "" : "s");
    for(i=0; i<num_methods; ++i) {
        SSL_COMP *comp;
        const char *name;

        comp=sk_SSL_COMP_value(methods, i);
        if(!comp)
            continue;
        name=SSL_COMP_get0_name(comp);
        /* see OpenSSL commit 847406923534dd791f73d0cda15d3f17f513f2a5 */
        if(!name)
            name="unknown";
        s_log(LOG_INFO, "Compression id 0x%02x: %s",
            SSL_COMP_get_id(comp), name);
    }
}
#endif /* OPENSSL_NO_COMP */

NOEXPORT int prng_init(GLOBAL_OPTIONS *global) {
    int totbytes=0;
    char filename[256];
    const RAND_METHOD *meth=RAND_get_rand_method();

    /* skip PRNG initialization when no seeding methods are available */
    if(meth==NULL || meth->status==NULL || meth->add==NULL) {
        s_log(LOG_DEBUG, "No PRNG seeding methods");
        return 0; /* success */
    }

    if(RAND_status()) {
        s_log(LOG_DEBUG, "No PRNG seeding was required");
        return 0; /* success */
    }

    /* if they specify a rand file on the command line we
       assume that they really do want it, so try it first */
    if(global->rand_file) {
        totbytes+=add_rand_file(global, global->rand_file);
        if(RAND_status())
            return 0; /* success */
    }

    /* try the $RANDFILE or $HOME/.rnd files */
    filename[0]='\0';
    RAND_file_name(filename, sizeof filename);
    if(filename[0]) {
        totbytes+=add_rand_file(global, filename);
        if(RAND_status())
            return 0; /* success */
    }

#ifdef USE_WIN32

#if OPENSSL_VERSION_NUMBER<0x10100000L
    RAND_screen();
    if(RAND_status()) {
        s_log(LOG_DEBUG, "Seeded PRNG with RAND_screen");
        return 0; /* success */
    }
    s_log(LOG_DEBUG, "RAND_screen failed to sufficiently seed PRNG");
#endif

#else /* USE_WIN32 */

#ifndef OPENSSL_NO_EGD
    if(global->egd_sock) {
        int bytes=RAND_egd(global->egd_sock);
        if(bytes>=0) {
            s_log(LOG_DEBUG, "Snagged %d random bytes from EGD Socket %s",
                bytes, global->egd_sock);
            return 0; /* OpenSSL always gets what it needs or fails,
                         so no need to check if seeded sufficiently */
        }
        s_log(LOG_WARNING, "EGD Socket %s failed", global->egd_sock);
    }
#endif

#ifndef RANDOM_FILE
    /* try the good-old default /dev/urandom, if no RANDOM_FILE is defined */
    totbytes+=add_rand_file(global, "/dev/urandom");
    if(RAND_status())
        return 0; /* success */
#endif

#endif /* USE_WIN32 */

    /* random file specified during configure */
    s_log(LOG_ERR, "PRNG seeded with %d bytes total", totbytes);
    s_log(LOG_ERR, "PRNG was not seeded with enough random bytes");
    return 1; /* FAILED */
}

NOEXPORT int add_rand_file(GLOBAL_OPTIONS *global, const char *filename) {
    int readbytes;
    struct stat sb;

    if(stat(filename, &sb))
        return 0; /* could not stat() file --> return 0 bytes */

    readbytes=RAND_load_file(filename, global->random_bytes);
    if(readbytes<0) {
        sslerror("RAND_load_file");
        s_log(LOG_INFO, "Cannot retrieve any random data from %s",
            filename);
        return 0;
    }
    s_log(LOG_DEBUG, "Snagged %d random bytes from %s", readbytes, filename);

    /* write new random data for future seeding if it's a regular file */
    if(global->option.rand_write && S_ISREG(sb.st_mode))
        update_rand_file(filename);

    return readbytes;
}

NOEXPORT void update_rand_file(const char *filename) {
    int writebytes;

    writebytes=RAND_write_file(filename);
    if(writebytes<0) {
        sslerror("RAND_write_file");
        s_log(LOG_WARNING, "Failed to write strong random data to %s - "
            "may be a permissions or seeding problem", filename);
        return;
    }
    s_log(LOG_DEBUG, "Wrote %d new random bytes to %s",
        writebytes, filename);
}

/* end of ssl.c */
