/*
 *   stunnel       TLS offloading and load-balancing proxy
 *   Copyright (C) 1998-2026 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"

/* this mirrors a private OpenSSL struct whose layout has stayed stable across
 * the supported OpenSSL versions, so breakage seems unlikely in current
 * releases, but it is still an unsupported internal dependency that could
 * change, especially in a future major version */
#if OPENSSL_VERSION_NUMBER>=0x10100000L
struct ssl_comp_st {
    int id;
    const char *name;
    COMP_METHOD *method;
};
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */

    /* 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 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;
#if OPENSSL_VERSION_NUMBER<0x10100000L
int unsafe_openssl;
#endif /* OpenSSL version < 1.1.0 */

#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;
}

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

    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 */
}

#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();
    unsafe_openssl=OpenSSL_version_num()<0x0090810fL ||
        (OpenSSL_version_num()>=0x10000000L &&
        OpenSSL_version_num()<0x1000002fL);
#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 */
}

/* release libcrypto resources at shutdown */
void crypto_cleanup(void) {
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    OPENSSL_cleanup();
#endif
}

/* initialize libssl before parsing the configuration file */
int ssl_init(void) {
    index_ssl_cli=SSL_get_ex_new_index(0, NULL,
        NULL, NULL, NULL);
    index_ssl_ctx_opt=SSL_CTX_get_ex_new_index(0, NULL,
        NULL, NULL, NULL);
    index_session_authenticated=SSL_SESSION_get_ex_new_index(0, NULL,
        cb_new_auth, NULL, NULL);
    index_session_connect_address=SSL_SESSION_get_ex_new_index(0, NULL,
        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;
    }
    return 0;
}

/* release libssl resources at shutdown */
void ssl_cleanup(void) {
    /* no libssl cleanup is needed for now */
}

#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 */
    (void)argp; /* squash the unused parameter warning */
    if(!CRYPTO_set_ex_data(ad, idx, (void *)(-1)))
        ssl_error(NULL, "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 */
    (void)argp; /* squash the unused parameter warning */
    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 */
    (void)argp; /* squash the unused parameter warning */
    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 !=
            (OSSL_PROVIDER_available(NULL, "fips") &&
            EVP_default_properties_is_fips_enabled(NULL))) {
        if(global->option.fips) { /* need to enable */
            if(!fips_available()) {
                ssl_error(NULL, "FIPS PROVIDER");
                return 1;
            }
            if(!EVP_default_properties_enable_fips(NULL, 1)) {
                s_log(LOG_ERR, "Enabling FIPS provider failed");
                return 1;
            }
        } else { /* need to disable */
            if(fips_default()) {
                s_log(LOG_ERR, "Refusing to override 'fips=yes' default property");
                return 1;
            }
            if(!EVP_default_properties_enable_fips(NULL, 0)) {
                s_log(LOG_ERR, "Disabling FIPS provider failed");
                return 1;
            }
        }
    }
    s_log(LOG_NOTICE, "FIPS provider %s",
        (OSSL_PROVIDER_available(NULL, "fips") &&
        EVP_default_properties_is_fips_enabled(NULL)) ?
        "enabled" : "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
            ssl_error(NULL, "FIPS_mode_set");
            return 1;
        }
    }
    s_log(LOG_NOTICE, "FIPS mode %s",
        global->option.fips ? "enabled" : "disabled");
#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */
#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 algorithms 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;
}

NOEXPORT const char *COMP_get_name(const COMP_METHOD *meth) {
    return SSL_COMP_get_name(meth);
}

#endif /* OPENSSL_VERSION_NUMBER<0x10100000L */

/* the following function mostly re-implements the defunct
 * OpenSSL's SSL_COMP_add_compression_method() function */
NOEXPORT int add_compression_method(STACK_OF(SSL_COMP) *sk,
        int id, COMP_METHOD *meth) {
    SSL_COMP *comp;

    /* validate the requested compression method */
    if(!meth || COMP_get_type(meth)==NID_undef) {
        s_log(LOG_ERR, "Compression type disabled in this OpenSSL build");
        return 1; /* FAILED */
    }

    /* build an SSL_COMP object and push it to the stack */
    comp=OPENSSL_malloc(sizeof(SSL_COMP));
    if(!comp)
        return 1; /* FAILED */
    comp->id=id;
    comp->name=COMP_get_name(meth); /* SSL_COMP_add_compression_method() bug */
    comp->method=meth;              /* SSL_COMP_add_compression_method() bug */
    if(!sk_SSL_COMP_push(sk, comp)) {
        OPENSSL_free(comp);
        s_log(LOG_ERR, "Failed to add compression method");
        return 1; /* FAILED */
    }
    return 0; /* SUCCESS */
}

NOEXPORT int compression_set(GLOBAL_OPTIONS *global) {
    STACK_OF(SSL_COMP) *methods;
    int retval=0;

    methods=SSL_COMP_get_compression_methods();
    if(!methods) /* make sure we have a valid stack */
        return global->compression!=COMP_NONE;

    /* empty the current compression method stack */
    while(sk_SSL_COMP_num(methods))
        sk_SSL_COMP_pop(methods);

    /* add the requested compression methods */
    switch(global->compression) {
    case COMP_DEFLATE: /* RFC 1951 on standard id 0x01 */
        retval|=add_compression_method(methods, 0x01, COMP_zlib());
        break;
    case COMP_ZLIB: /* RFC 1951 + historic/obsolete private id 0xe0 */
        retval|=add_compression_method(methods, 0x01, COMP_zlib());
        retval|=add_compression_method(methods, 0xe0, COMP_zlib());
        break;
#if OPENSSL_VERSION_NUMBER>=0x30200000L
    case COMP_ZSTD: /* RFC 8878 on stunnel's nonstandard id 0xe1 */
        retval|=add_compression_method(methods, 0xe1, COMP_zstd());
        break;
    case COMP_BROTLI: /* RFC 7932 on stunnel's nonstandard id 0xe2 */
        retval|=add_compression_method(methods, 0xe2, COMP_brotli());
        break;
#endif
    case COMP_NONE: /* make static code analyzers happy */
        break;
    }
    return retval;
}

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="name not available";
        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) {
        ssl_error(NULL, "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) {
        ssl_error(NULL, "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 */
