diff --git a/.github/workflows/seed-src.yml b/.github/workflows/seed-src.yml new file mode 100644 index 00000000..37e89703 --- /dev/null +++ b/.github/workflows/seed-src.yml @@ -0,0 +1,55 @@ +name: SEED-SRC Tests + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +# END OF COMMON SECTION + +jobs: + seed_src_test: + name: SEED-SRC Test + runs-on: ubuntu-22.04 + timeout-minutes: 20 + strategy: + matrix: + wolfssl_ref: [ + 'master', + 'v5.8.4-stable'] + openssl_ref: [ + 'openssl-3.5.4', + 'openssl-3.0.17'] + + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build and test wolfProvider with SEED-SRC + run: | + # Force wolfSSL to not use getrandom syscall via ac_cv_func_getrandom=no. + # This ensures /dev/urandom is used as the entropy source, which is + # required to test the SEED-SRC feature's fork-safe caching behavior. + WOLFSSL_CONFIG_OPTS="--enable-all-crypto --with-eccminsz=192 --with-max-ecc-bits=1024 --enable-opensslcoexist --enable-sha ac_cv_func_getrandom=no" \ + OPENSSL_TAG=${{ matrix.openssl_ref }} \ + WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ + ./scripts/build-wolfprovider.sh --enable-seed-src + + - name: Print errors + if: ${{ failure() }} + run: | + if [ -f test-suite.log ] ; then + cat test-suite.log + fi + if [ -f scripts/build-release.log ] ; then + echo "=== Build Release Log (last 100 lines) ===" + tail -100 scripts/build-release.log + fi + diff --git a/configure.ac b/configure.ac index 59f54db0..f24e1ca0 100644 --- a/configure.ac +++ b/configure.ac @@ -132,6 +132,17 @@ AC_ARG_ENABLE([replace-default], AM_CONDITIONAL([BUILD_REPLACE_DEFAULT], [test "x$ENABLED_REPLACE_DEFAULT" = "xyes"]) +# SEED-SRC entropy source with /dev/urandom caching +AC_ARG_ENABLE([seed-src], + [AS_HELP_STRING([--enable-seed-src],[Enable SEED-SRC entropy source with /dev/urandom caching for fork-safe entropy (default: disabled).])], + [ ENABLED_SEED_SRC=$enableval ], + [ ENABLED_SEED_SRC=no ] + ) + +if test "x$ENABLED_SEED_SRC" = "xyes"; then + AM_CFLAGS="$AM_CFLAGS -DWP_HAVE_SEED_SRC" +fi + # Set OpenSSL lib directory for installing libdefault.so if test "x$ENABLED_REPLACE_DEFAULT" = "xyes"; then if test -d "$OPENSSL_INSTALL_DIR/lib64"; then diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index b9271fef..1a13cb3a 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -170,6 +170,7 @@ typedef void (*DFUNC)(void); #define WP_NAMES_DHX "DHX" /* DRBG names. */ +#define WP_NAMES_SEED_SRC "SEED-SRC" #define WP_NAMES_CTR_DRBG "CTR-DRBG" #define WP_NAMES_HASH_DRBG "HASH-DRBG" @@ -351,6 +352,9 @@ extern const OSSL_DISPATCH wp_hkdf_keyexch_functions[]; extern const OSSL_DISPATCH wp_tls1_prf_keyexch_functions[]; /* DRBG implementations. */ +#ifdef WP_HAVE_SEED_SRC +extern const OSSL_DISPATCH wp_seed_src_functions[]; +#endif extern const OSSL_DISPATCH wp_drbg_functions[]; /* Decode implementations. */ diff --git a/include/wolfprovider/internal.h b/include/wolfprovider/internal.h index da50bcb0..e7cc933d 100644 --- a/include/wolfprovider/internal.h +++ b/include/wolfprovider/internal.h @@ -155,6 +155,24 @@ typedef struct WOLFPROV_CTX { BIO_METHOD *coreBioMethod; } WOLFPROV_CTX; +#ifdef WP_HAVE_SEED_SRC +/* + * Global /dev/urandom subsystem functions. + * + * These manage a cached file handle to /dev/urandom that is opened lazily + * on first entropy request (matching OpenSSL's default provider behavior). + * The file stays open so child processes inherit it and can read even + * in sandboxed environments that block openat(). + */ +int wp_urandom_init(void); +void wp_urandom_cleanup(void); +int wp_urandom_read(unsigned char* buf, size_t len); + +#ifndef WP_SINGLE_THREADED +wolfSSL_Mutex *wp_get_urandom_mutex(void); +#endif +#endif /* WP_HAVE_SEED_SRC */ + WC_RNG* wp_provctx_get_rng(WOLFPROV_CTX* provCtx); #ifndef WP_SINGLE_THREADED diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index ffe7b8f8..c548329d 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -27,6 +27,8 @@ show_help() { echo " Note: Requires --replace-default. Only for test builds, not for production." echo " --leave-silent Enable leave silent mode to suppress logging of return 0 in probing functions where expected failures may occur." echo " Note: This only affects logging; the calling function is still responsible for handling all return values appropriately." + echo " --enable-seed-src Enable SEED-SRC entropy source with /dev/urandom caching for fork-safe entropy." + echo " Note: This also enables WC_RNG_SEED_CB in wolfSSL." echo "" echo "Environment Variables:" echo " OPENSSL_TAG OpenSSL tag to use (e.g., openssl-3.5.0)" @@ -43,6 +45,7 @@ show_help() { echo " WOLFPROV_REPLACE_DEFAULT If set to 1, patches OpenSSL so wolfProvider is the default provider" echo " WOLFPROV_REPLACE_DEFAULT_TESTING If set to 1, enables direct provider loading in unit tests (requires WOLFPROV_REPLACE_DEFAULT=1)" echo " WOLFPROV_LEAVE_SILENT If set to 1, suppress logging of return 0 in functions where return 0 is expected behavior sometimes." + echo " WOLFPROV_SEED_SRC If set to 1, enables SEED-SRC with /dev/urandom caching (also enables WC_RNG_SEED_CB in wolfSSL)" echo "" } @@ -129,6 +132,9 @@ for arg in "$@"; do --leave-silent) WOLFPROV_LEAVE_SILENT=1 ;; + --enable-seed-src) + WOLFPROV_SEED_SRC=1 + ;; *) args_wrong+="$arg, " ;; diff --git a/scripts/utils-wolfprovider.sh b/scripts/utils-wolfprovider.sh index 26d2a171..eb744fc5 100644 --- a/scripts/utils-wolfprovider.sh +++ b/scripts/utils-wolfprovider.sh @@ -106,6 +106,10 @@ install_wolfprov() { WOLFPROV_CONFIG_OPTS+=" --enable-replace-default" fi + if [ "$WOLFPROV_SEED_SRC" = "1" ]; then + WOLFPROV_CONFIG_OPTS+=" --enable-seed-src" + fi + if [ "${WOLFPROV_LEAVE_SILENT}" = "1" ]; then WOLFPROV_CONFIG_CFLAGS="${WOLFPROV_CONFIG_CFLAGS} -DWOLFPROV_LEAVE_SILENT_MODE" fi diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index c19dd688..6d2e6f9d 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -32,6 +32,12 @@ WOLFSSL_FIPS_CONFIG_CFLAGS=${WOLFSSL_CONFIG_CFLAGS:-"-I${OPENSSL_INSTALL_DIR}/in WOLFSSL_CONFIG_OPTS=${WOLFSSL_CONFIG_OPTS:-'--enable-all-crypto --with-eccminsz=192 --with-max-ecc-bits=1024 --enable-opensslcoexist --enable-sha'} WOLFSSL_CONFIG_CFLAGS=${WOLFSSL_CONFIG_CFLAGS:-"-I${OPENSSL_INSTALL_DIR}/include -DWC_RSA_NO_PADDING -DWOLFSSL_PUBLIC_MP -DHAVE_PUBLIC_FFDHE -DHAVE_FFDHE_6144 -DHAVE_FFDHE_8192 -DWOLFSSL_PSS_LONG_SALT -DWOLFSSL_PSS_SALT_LEN_DISCOVER -DRSA_MIN_SIZE=1024 -DWOLFSSL_OLD_OID_SUM "} +# Add WC_RNG_SEED_CB when SEED-SRC is enabled (allows custom seed callback for fork safety) +if [ "$WOLFPROV_SEED_SRC" = "1" ]; then + WOLFSSL_CONFIG_CFLAGS="${WOLFSSL_CONFIG_CFLAGS} -DWC_RNG_SEED_CB" + WOLFSSL_FIPS_CONFIG_CFLAGS="${WOLFSSL_FIPS_CONFIG_CFLAGS} -DWC_RNG_SEED_CB" +fi + WOLFSSL_DEBUG_ASN_TEMPLATE=${DWOLFSSL_DEBUG_ASN_TEMPLATE:-0} WOLFPROV_DISABLE_ERR_TRACE=${WOLFPROV_DISABLE_ERR_TRACE:-0} WOLFPROV_DEBUG=${WOLFPROV_DEBUG:-0} diff --git a/src/include.am b/src/include.am index f9dc392a..8c580401 100644 --- a/src/include.am +++ b/src/include.am @@ -36,6 +36,7 @@ libwolfprov_la_SOURCES += src/wp_ecx_sig.c libwolfprov_la_SOURCES += src/wp_dh_kmgmt.c libwolfprov_la_SOURCES += src/wp_dh_exch.c libwolfprov_la_SOURCES += src/wp_drbg.c +libwolfprov_la_SOURCES += src/wp_seed_src.c libwolfprov_la_SOURCES += src/wp_dec_pem2der.c libwolfprov_la_SOURCES += src/wp_dec_epki2pki.c libwolfprov_la_SOURCES += src/wp_file_store.c diff --git a/src/wp_drbg.c b/src/wp_drbg.c index 811ca894..4dd2c282 100644 --- a/src/wp_drbg.c +++ b/src/wp_drbg.c @@ -47,32 +47,68 @@ * DRBG context structure. */ typedef struct wp_DrbgCtx { + /** Provider context. */ + WOLFPROV_CTX* provCtx; /** wolfSSL random number generator. HASH DRBG implementation. */ WC_RNG* rng; #ifndef WP_SINGLE_THREADED /** Mutex for multithreading access to this DRBG context. */ wolfSSL_Mutex* mutex; #endif + /** Parent DRBG context for getting entropy. */ + void* parent; + /** Parent's get_seed function. */ + OSSL_FUNC_rand_get_seed_fn* parentGetSeed; + /** Parent's clear_seed function. */ + OSSL_FUNC_rand_clear_seed_fn* parentClearSeed; + /** Whether we have a parent DRBG. */ + int hasParent; } wp_DrbgCtx; /** * Create a new DRBG context object. * - * @param [in] provCtx Provider context. + * @param [in] provCtx Provider context. + * @param [in] parent Parent DRBG context for getting entropy. + * @param [in] parentDispatch Parent's dispatch table. * @return DRBG object on success. * @return NULL on failure. */ -static wp_DrbgCtx* wp_drbg_new(WOLFPROV_CTX* provCtx) +static wp_DrbgCtx* wp_drbg_new(void* provCtx, void* parent, + const OSSL_DISPATCH* parentDispatch) { wp_DrbgCtx* ctx = NULL; - (void)provCtx; + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_drbg_new"); if (wolfssl_prov_is_running()) { ctx = OPENSSL_zalloc(sizeof(*ctx)); } + if (ctx != NULL) { + ctx->provCtx = (WOLFPROV_CTX*)provCtx; + ctx->parent = parent; + + /* Extract parent dispatch functions if available */ + if (parentDispatch != NULL) { + for (; parentDispatch->function_id != 0; parentDispatch++) { + switch (parentDispatch->function_id) { + case OSSL_FUNC_RAND_GET_SEED: + ctx->parentGetSeed = + OSSL_FUNC_rand_get_seed(parentDispatch); + ctx->hasParent = 1; + break; + case OSSL_FUNC_RAND_CLEAR_SEED: + ctx->parentClearSeed = + OSSL_FUNC_rand_clear_seed(parentDispatch); + break; + } + } + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ctx != NULL); return ctx; } @@ -110,23 +146,71 @@ static void wp_drbg_free(wp_DrbgCtx* ctx) * @param [in] pStrLen Length of personalization string in bytes. * @param [in] params Other parameters. Unused. * @return 1 on success. - * @return 0 on success. + * @return 0 on failure. */ static int wp_drbg_instantiate(wp_DrbgCtx* ctx, unsigned int strength, int predResist, const unsigned char* pStr, size_t pStrLen, const OSSL_PARAM params[]) { int ok = 1; + unsigned char* seed = NULL; + size_t seedLen = 0; WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_drbg_instantiate"); - (void)predResist; (void)params; if (strength > WP_DRBG_STRENGTH) { ok = 0; } - if (ok ) { + + if (ok && ctx->hasParent && ctx->parentGetSeed != NULL) { + /* Get entropy from parent DRBG (no file I/O needed) */ + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "Getting entropy from parent DRBG"); + + seedLen = ctx->parentGetSeed(ctx->parent, &seed, + 256, /* entropy bits */ + 32, /* min_len */ + 256, /* max_len */ + predResist, pStr, pStrLen); + + if (seedLen == 0 || seed == NULL) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "Failed to get seed from parent"); + ok = 0; + } + + if (ok) { + /* Initialize wolfCrypt RNG with parent-provided seed */ + ctx->rng = OPENSSL_zalloc(sizeof(WC_RNG)); + if (ctx->rng == NULL) { + ok = 0; + } + } + + if (ok) { + int rc = wc_InitRngNonce(ctx->rng, seed, (word32)seedLen); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, + "wc_InitRngNonce", rc); + OPENSSL_free(ctx->rng); + ctx->rng = NULL; + ok = 0; + } + } + + /* Clear the seed from parent */ + if (seed != NULL && ctx->parentClearSeed != NULL) { + ctx->parentClearSeed(ctx->parent, seed, seedLen); + } + } + else if (ok) { + /* No parent - this is the root DRBG, use /dev/urandom directly. + * This path should only be taken before sandbox activation. */ + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "No parent DRBG, using direct seeding"); + #if LIBWOLFSSL_VERSION_HEX >= 0x05000000 ctx->rng = wc_rng_new((byte*)pStr, (word32)pStrLen, NULL); if (ctx->rng == NULL) { @@ -143,7 +227,7 @@ static int wp_drbg_instantiate(wp_DrbgCtx* ctx, unsigned int strength, if (ok) { int rc = wc_InitRng(ctx->rng); if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_InitRng", rc); + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_InitRng", rc); OPENSSL_clear_free(ctx->rng, sizeof(*ctx->rng)); ok = 0; } @@ -215,7 +299,7 @@ static int wp_drbg_generate(wp_DrbgCtx* ctx, unsigned char* out, if (ok) { rc = wc_RNG_GenerateBlock(ctx->rng, out, (word32)outLen); if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_RNG_GenerateBlock", rc); + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_RNG_GenerateBlock", rc); ok = 0; } } @@ -295,7 +379,7 @@ static int wp_drbg_enable_locking(wp_DrbgCtx* ctx) if (ok) { int rc = wc_InitMutex(ctx->mutex); if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_InitMutex", rc); + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_InitMutex", rc); OPENSSL_free(ctx->mutex); ok = 0; } @@ -326,7 +410,7 @@ static int wp_drbg_lock(wp_DrbgCtx* ctx) if (ctx->mutex != NULL) { rc = wc_LockMutex(ctx->mutex); if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_LockMutex", rc); + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_LockMutex", rc); ok = 0; } } @@ -466,60 +550,59 @@ static int wp_drbg_verify_zeroization(wp_DrbgCtx* ctx) /** * Get secure seed. * - * @param [in, out] ctx DRBG context object. - * @param [out] pSeed Handle to seed buffer. - * @param [in] entropy Number of bits of required in seed. - * @param [in] minLen Minimum length of buffer to create in bytes. - * @param [in] minLen Maximum length of buffer to create in bytes. - * @param [in] predResist Prediction resistance required. - * @param [in] addIn Additional input to seed with. - * @param [in] addInLen Additional input to seed with. + * @param [in, out] ctx DRBG context object. + * @param [out] pSeed Handle to seed buffer. + * @param [in] entropy Number of bits of required in seed. + * @param [in] minLen Minimum length of buffer to create in bytes. + * @param [in] maxLen Maximum length of buffer to create in bytes. + * @param [in] prediction_resistance Prediction resistance required. + * @param [in] addIn Additional input to seed with. + * @param [in] addInLen Length of additional input. + * @return Number of bytes of seed on success. + * @return 0 on failure. */ static size_t wp_drbg_get_seed(wp_DrbgCtx* ctx, unsigned char** pSeed, int entropy, size_t minLen, size_t maxLen, int prediction_resistance, const unsigned char* addIn, size_t addInLen) { - int ok = 1; + size_t ret = 0; int rc; - unsigned char* buffer; + unsigned char* buffer = NULL; WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_drbg_get_seed"); (void)entropy; (void)maxLen; (void)prediction_resistance; - - buffer = OPENSSL_secure_malloc(minLen); - if (buffer == NULL) { - ok = 0; - } -#if 0 - if (ok && (addInLen > 0)) { - rc = wc_RNG_DRBG_Reseed(ctx->rng, addIn, addInLen); - if (rc != 0) { - ok = 0; - } - } -#else (void)addIn; (void)addInLen; -#endif - if (ok) { - rc = wc_RNG_GenerateBlock(ctx->rng, buffer, (word32)minLen); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_RNG_GenerateBlock", rc); - ok = 0; - } + + if (ctx->rng == NULL) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "DRBG not instantiated"); + goto end; } - if (ok) { - *pSeed = buffer; + + buffer = OPENSSL_secure_malloc(minLen); + if (buffer == NULL) { + goto end; } - if (!ok) { + + rc = wc_RNG_GenerateBlock(ctx->rng, buffer, (word32)minLen); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, + "wc_RNG_GenerateBlock", rc); OPENSSL_secure_free(buffer); + goto end; } - WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); - return ok; + *pSeed = buffer; + ret = minLen; + +end: + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ret > 0); + return ret; } /** diff --git a/src/wp_internal.c b/src/wp_internal.c index f65bcdc2..32f0a00a 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -30,7 +30,36 @@ #include #include -#if defined(HAVE_FIPS) && (!defined(WP_SINGLE_THREADED)) +#ifndef WP_SINGLE_THREADED + +#ifdef WP_HAVE_SEED_SRC +/* Global mutex for urandom file access (used by seed callback) */ +static wolfSSL_Mutex urandomMutex; + +/** + * Initialize the urandom mutex on library load. + * + * This constructor runs when libwolfprov.so is loaded via dlopen() or at + * program startup. It ensures the urandomMutex is initialized before use. + */ +__attribute__((constructor)) +static void wolfprov_init_urandom_mutex(void) +{ + wc_InitMutex(&urandomMutex); +} + +/** + * Get the global urandom mutex. + * + * @return Pointer to the urandom mutex. + */ +wolfSSL_Mutex *wp_get_urandom_mutex(void) +{ + return &urandomMutex; +} +#endif /* WP_HAVE_SEED_SRC */ + +#ifdef HAVE_FIPS static wolfSSL_Mutex castMutex; /** @@ -45,11 +74,17 @@ static void wolfprov_init_cast_mutex(void) wc_InitMutex(&castMutex); } -wolfSSL_Mutex *wp_get_cast_mutex() +/** + * Get the FIPS CAST mutex. + * + * @return Pointer to the CAST mutex. + */ +wolfSSL_Mutex *wp_get_cast_mutex(void) { return &castMutex; } -#endif +#endif /* HAVE_FIPS */ +#endif /* !WP_SINGLE_THREADED */ /** * Get the wolfSSL random number generator from the provider context. diff --git a/src/wp_seed_src.c b/src/wp_seed_src.c new file mode 100644 index 00000000..19e4dd06 --- /dev/null +++ b/src/wp_seed_src.c @@ -0,0 +1,742 @@ +/* wp_seed_src.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider 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 3 of the License, or + * (at your option) any later version. + * + * wolfProvider 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 wolfProvider. If not, see . + */ + +#include + +#ifdef WP_HAVE_SEED_SRC + +#include +#include + +#include +#include +#include +#include + +#include +#include + +/* Include wolfSSL port header for XFOPEN/XFREAD/XFCLOSE macros */ +#include + +/* + * /dev/urandom file caching for fork-safe entropy. + * + * These functions manage a cached file handle to /dev/urandom that is + * opened lazily on first entropy request and kept open. This matches + * OpenSSL's default provider behavior. The file stays open so child + * processes after fork() can inherit the underlying fd and read from it + * without needing to call openat(), which may be blocked by seccomp sandboxes. + */ + +#define URANDOM_PATH "/dev/urandom" + +/* + * Global cached /dev/urandom file handle. + * Opened lazily on first entropy request, kept open for the lifetime of the + * provider. This matches OpenSSL's model where random devices are opened + * on-demand and cached. + */ +static XFILE g_urandom_file = XBADFILE; + +/* + * Flag indicating whether the seed callback has been registered. + */ +static int g_seed_cb_registered = 0; + +/** + * wolfSSL seed callback that uses the cached /dev/urandom file. + * + * This callback is registered with wc_SetSeed_Cb() and is called by wolfSSL's + * DRBG when it needs entropy (including after fork detection). + * + * @param [in] os OS_Seed structure (unused) + * @param [out] seed Buffer to fill with seed data + * @param [in] sz Number of bytes to generate + * @return 0 on success + * @return -1 on failure + */ +#ifdef WC_RNG_SEED_CB +static int wp_wolfssl_seed_cb(OS_Seed* os, byte* seed, word32 sz) +{ + size_t bytesRead; + size_t total = 0; + + (void)os; + +#ifndef WP_SINGLE_THREADED + /* Lock before checking/opening file to prevent race conditions. + * The urandom mutex is initialized via constructor at library load, + * so it's guaranteed to be ready for use here. + */ + if (wc_LockMutex(wp_get_urandom_mutex()) != 0) { + return -1; + } +#endif + + /* Lazy open: open file on first entropy request */ + if (g_urandom_file == XBADFILE) { + g_urandom_file = XFOPEN(URANDOM_PATH, "rb"); + if (g_urandom_file == XBADFILE) { +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + return -1; + } + } + + /* Read until we have all the bytes we need */ + while (total < sz) { + bytesRead = XFREAD(seed + total, 1, sz - total, g_urandom_file); + if (bytesRead > 0) { + total += bytesRead; + } + else { + /* EOF or error */ +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + return -1; + } + } + +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + + return 0; +} +#endif /* WC_RNG_SEED_CB */ + +/** + * Initialize the urandom subsystem. + * + * This performs lazy initialization - the file handle is not opened until + * first entropy request. This matches OpenSSL's default provider behavior. + * The seed callback is registered here so wolfSSL can use our entropy source. + * + * @return 0 on success. + * @return -1 on failure. + */ +int wp_urandom_init(void) +{ +#ifndef WP_SINGLE_THREADED + /* Lock to ensure thread-safe initialization. + * The urandom mutex is initialized via constructor at library load. + */ + if (wc_LockMutex(wp_get_urandom_mutex()) != 0) { + return -1; + } +#endif + + /* Initialize global file handle to invalid - will be opened lazily */ + g_urandom_file = XBADFILE; + +#ifdef WC_RNG_SEED_CB + /* Register our seed callback with wolfSSL. + * This is critical for fork safety - wolfSSL will call this callback + * instead of wc_GenerateSeed() when it needs to reseed after fork. + * The callback will open the file lazily on first use. + */ + if (!g_seed_cb_registered) { + if (wc_SetSeed_Cb(wp_wolfssl_seed_cb) != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + "wc_SetSeed_Cb failed", -1); + /* Non-fatal - continue without callback */ + } + else { + g_seed_cb_registered = 1; + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + "wp_urandom_init: registered wolfSSL seed callback", 0); + } + } +#endif + +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + + return 0; +} + +/** + * Clean up the urandom subsystem. + * + * Closes the cached file handle if it was opened, and unregisters + * the seed callback. + */ +void wp_urandom_cleanup(void) +{ +#ifndef WP_SINGLE_THREADED + /* Lock to ensure thread-safe cleanup. + * The urandom mutex is initialized via constructor at library load. + */ + if (wc_LockMutex(wp_get_urandom_mutex()) != 0) { + return; + } +#endif + +#ifdef WC_RNG_SEED_CB + /* Unregister seed callback */ + if (g_seed_cb_registered) { + wc_SetSeed_Cb(NULL); + g_seed_cb_registered = 0; + } +#endif + + /* Close global file if it was opened */ + if (g_urandom_file != XBADFILE) { + XFCLOSE(g_urandom_file); + g_urandom_file = XBADFILE; + WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, + "wp_urandom_cleanup: closed " URANDOM_PATH); + } + +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + + /* Note: global urandom mutex is managed via constructor/destructor */ +} + +/** + * Read random bytes from /dev/urandom. + * + * Opens /dev/urandom lazily on first call, then keeps it open for subsequent + * reads. This matches OpenSSL's default provider behavior. The file stays open + * so child processes can inherit it and read even in sandboxed environments. + * + * @param [out] buf Buffer to fill with random bytes. + * @param [in] len Number of bytes to read. + * @return Number of bytes read on success. + * @return -1 on failure. + */ +int wp_urandom_read(unsigned char* buf, size_t len) +{ + size_t bytesRead; + size_t total = 0; + + if (buf == NULL || len == 0) { + return -1; + } + +#ifndef WP_SINGLE_THREADED + if (wc_LockMutex(wp_get_urandom_mutex()) != 0) { + return -1; + } +#endif + + /* Lazy open: open file on first entropy request */ + if (g_urandom_file == XBADFILE) { + g_urandom_file = XFOPEN(URANDOM_PATH, "rb"); + if (g_urandom_file == XBADFILE) { + WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, + "wp_urandom_read: failed to open " URANDOM_PATH); +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + return -1; + } + WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, + "wp_urandom_read: opened " URANDOM_PATH); + } + + /* Read until we have all the bytes we need */ + while (total < len) { + bytesRead = XFREAD(buf + total, 1, len - total, g_urandom_file); + if (bytesRead > 0) { + total += bytesRead; + } + else { + /* EOF or error - shouldn't happen with /dev/urandom */ + WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, + "wp_urandom_read: XFREAD failed"); +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + return -1; + } + } + +#ifndef WP_SINGLE_THREADED + wc_UnLockMutex(wp_get_urandom_mutex()); +#endif + + return (int)total; +} + + +/** + * SEED-SRC context structure. + * + * SEED-SRC acts as the root entropy source in OpenSSL's DRBG hierarchy. + * It uses the provider context's pre-seeded WC_RNG which was initialized + * at provider load time (before any sandbox restrictions). + */ +typedef struct wp_SeedSrcCtx { + /** Provider context containing pre-seeded WC_RNG. */ + WOLFPROV_CTX* provCtx; + /** Current state of the SEED-SRC. */ + int state; +} wp_SeedSrcCtx; + +static int wp_seed_src_adin_mix_in(unsigned char *buf, size_t bufLen, + const unsigned char *adin, size_t adinLen) +{ + if (adin == NULL || adinLen == 0) + /* Nothing to mix in -> success */ + return 1; + + if ((buf == NULL) || (bufLen == 0)) { + return 0; + } + + if (adin != NULL && adinLen > 0) { + size_t i; + + /* xor the additional data into the pool */ + for (i = 0; i < adinLen; ++i) + buf[i % bufLen] ^= adin[i]; + } + + return 1; +} + +/** + * Create a new SEED-SRC context object. + * + * @param [in] provCtx Provider context. + * @param [in] parent Parent RNG (unused for SEED-SRC, it's the root). + * @param [in] parent_dispatch Parent dispatch table (unused). + * @return SEED-SRC context object on success. + * @return NULL on failure. + */ +static wp_SeedSrcCtx* wp_seed_src_new(WOLFPROV_CTX* provCtx, void* parent, + const OSSL_DISPATCH* parent_dispatch) +{ + wp_SeedSrcCtx* ctx = NULL; + + (void)parent; + (void)parent_dispatch; + + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_new"); + + if (wolfssl_prov_is_running()) { + ctx = OPENSSL_zalloc(sizeof(*ctx)); + if (ctx != NULL) { + ctx->provCtx = provCtx; + ctx->state = EVP_RAND_STATE_UNINITIALISED; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ctx != NULL); + return ctx; +} + +/** + * Free the SEED-SRC context object. + * + * @param [in, out] ctx SEED-SRC context object. + */ +static void wp_seed_src_free(wp_SeedSrcCtx* ctx) +{ + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_free"); + + if (ctx != NULL) { + /* Don't free provCtx->rng - it's owned by the provider context */ + OPENSSL_free(ctx); + } + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + 1); +} + +/** + * Instantiate the SEED-SRC. + * + * Since the provider context's RNG is already initialized at provider load + * time, this just marks the SEED-SRC as ready. + * + * @param [in, out] ctx SEED-SRC context object. + * @param [in] strength Strength in bits required. + * @param [in] predResist Prediction resistance required. + * @param [in] pStr Personalization string (unused). + * @param [in] pStrLen Length of personalization string. + * @param [in] params Other parameters (unused). + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_seed_src_instantiate(wp_SeedSrcCtx* ctx, unsigned int strength, + int predResist, const unsigned char* pStr, size_t pStrLen, + const OSSL_PARAM params[]) +{ + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_instantiate"); + + (void)strength; + (void)predResist; + (void)pStr; + (void)pStrLen; + (void)params; + + if (ctx->provCtx == NULL) { + ok = 0; + } + else { + /* The provider context's RNG is already seeded from provider init */ + ctx->state = EVP_RAND_STATE_READY; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ok); + return ok; +} + +/** + * Uninstantiate the SEED-SRC. + * + * @param [in, out] ctx SEED-SRC context object. + * @return 1 on success. + */ +static int wp_seed_src_uninstantiate(wp_SeedSrcCtx* ctx) +{ + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_uninstantiate"); + + ctx->state = EVP_RAND_STATE_UNINITIALISED; + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + 1); + return 1; +} + +/** + * Generate random data from the SEED-SRC. + * + * Uses the provider context's pre-seeded WC_RNG to generate random bytes. + * This does not require any file I/O since the RNG was seeded at provider + * load time. + * + * @param [in, out] ctx SEED-SRC context object. + * @param [out] out Buffer to write random data to. + * @param [in] outLen Length of output buffer. + * @param [in] strength Strength in bits required. + * @param [in] predResist Prediction resistance required. + * @param [in] addIn Additional input (unused). + * @param [in] addInLen Length of additional input. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_seed_src_generate(wp_SeedSrcCtx* ctx, unsigned char* out, + size_t outLen, unsigned int strength, int predResist, + const unsigned char* addIn, size_t addInLen) +{ + int ok = 1; + unsigned char *buf = NULL; + int rc; + + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_generate"); + + (void)strength; + (void)predResist; + + if (ctx->state != EVP_RAND_STATE_READY) { + ok = 0; + } + if (ok && ctx->provCtx == NULL) { + ok = 0; + } + if (ok) { + buf = OPENSSL_zalloc(outLen); + if (buf == NULL) { + ok = 0; + } + } + if (ok) { + /* + * Read directly from /dev/urandom. + * The file is opened lazily on first request and kept open, so child + * processes can inherit the fd and read even in seccomp sandboxes. + */ + rc = wp_urandom_read(buf, outLen); + if (rc != (int)outLen) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + "wp_urandom_read failed", rc); + ok = 0; + } + } + if (ok) { + /* Mix in additional input if provided */ + ok = wp_seed_src_adin_mix_in(buf, outLen, addIn, addInLen); + } + if (ok) { + memcpy(out, buf, outLen); + } + if (buf != NULL) { + OPENSSL_free(buf); + } + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ok); + return ok; +} + +/* + * SEED-SRC locking functions. + * + * These are no-ops, matching OpenSSL's default provider implementation. + * The DRBG layer that uses SEED-SRC as a parent has its own locking mechanism, + * so SEED-SRC itself doesn't need to expose locking via the provider API. + * + * Note: The actual seed generation in wp_urandom_read() and wp_wolfssl_seed_cb() + * uses internal mutex protection for thread-safe access to the shared file handle. + */ + +/** + * Enable locking on the SEED-SRC context. + * + * No-op - matches OpenSSL's SEED-SRC implementation. + * + * @param [in, out] ctx SEED-SRC context object (unused). + * @return 1 always. + */ +static int wp_seed_src_enable_locking(wp_SeedSrcCtx* ctx) +{ + (void)ctx; + return 1; +} + +/** + * Lock the SEED-SRC context. + * + * No-op - matches OpenSSL's SEED-SRC implementation. + * + * @param [in, out] ctx SEED-SRC context object (unused). + * @return 1 always. + */ +static int wp_seed_src_lock(wp_SeedSrcCtx* ctx) +{ + (void)ctx; + return 1; +} + +/** + * Unlock the SEED-SRC context. + * + * No-op - matches OpenSSL's SEED-SRC implementation. + * + * @param [in, out] ctx SEED-SRC context object (unused). + * @return 1 always. + */ +static int wp_seed_src_unlock(wp_SeedSrcCtx* ctx) +{ + (void)ctx; + return 1; +} + +/** + * Return an array of supported gettable parameters for the SEED-SRC context. + * + * @param [in] ctx SEED-SRC context object (unused). + * @param [in] provCtx Provider context object (unused). + * @return Array of parameters with data type. + */ +static const OSSL_PARAM* wp_seed_src_gettable_ctx_params(wp_SeedSrcCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_supported_gettable_seed_src_ctx_params[] = { + OSSL_PARAM_int(OSSL_RAND_PARAM_STATE, NULL), + OSSL_PARAM_uint(OSSL_RAND_PARAM_STRENGTH, NULL), + OSSL_PARAM_size_t(OSSL_RAND_PARAM_MAX_REQUEST, NULL), + OSSL_PARAM_END + }; + + (void)ctx; + (void)provCtx; + + return wp_supported_gettable_seed_src_ctx_params; +} + +/** + * Get the SEED-SRC context parameters. + * + * @param [in] ctx SEED-SRC context object. + * @param [in, out] params Array of parameters and values. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_seed_src_get_ctx_params(wp_SeedSrcCtx* ctx, OSSL_PARAM params[]) +{ + int ok = 1; + OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_get_ctx_params"); + + p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_STATE); + if ((p != NULL) && (!OSSL_PARAM_set_int(p, ctx->state))) { + ok = 0; + } + if (ok) { + /* TODO: review strength value */ + p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_STRENGTH); + if ((p != NULL) && (!OSSL_PARAM_set_uint(p, 256))) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_MAX_REQUEST); + if ((p != NULL) && (!OSSL_PARAM_set_size_t(p, (1 << 16)))) { + ok = 0; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ok); + return ok; +} + +/** + * Get seed from the SEED-SRC for a child DRBG. + * + * Uses the provider context's pre-seeded WC_RNG to generate seed bytes. + * This does not require any file I/O since the RNG was seeded at provider + * load time (before any sandbox restrictions). + * + * @param [in, out] ctx SEED-SRC context object. + * @param [out] pSeed Handle to seed buffer. + * @param [in] entropy Number of entropy bits required. + * @param [in] minLen Minimum length of buffer in bytes. + * @param [in] maxLen Maximum length of buffer in bytes. + * @param [in] prediction_resistance Prediction resistance required. + * @param [in] addIn Additional input. + * @param [in] addInLen Length of additional input. + * @return Length of seed on success. + * @return 0 on failure. + */ +static size_t wp_seed_src_get_seed(wp_SeedSrcCtx* ctx, unsigned char** pSeed, + int entropy, size_t minLen, size_t maxLen, int prediction_resistance, + const unsigned char* addIn, size_t addInLen) +{ + size_t ret = 0; + unsigned char* buffer = NULL; + int rc; + + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_get_seed"); + + (void)entropy; + (void)maxLen; + (void)prediction_resistance; + + if (ctx->state != EVP_RAND_STATE_READY) { + goto end; + } + + if (ctx->provCtx == NULL) { + goto end; + } + + buffer = OPENSSL_zalloc(minLen); + if (buffer == NULL) { + goto end; + } + + /* + * Read directly from /dev/urandom. + * The file is opened lazily on first request and kept open, so child + * processes can inherit the fd and read even in seccomp sandboxes. + */ + rc = wp_urandom_read(buffer, minLen); + if (rc != (int)minLen) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + "wp_urandom_read failed", rc); + OPENSSL_free(buffer); + buffer = NULL; + goto end; + } + + /* Mix in additional input if provided */ + if (!wp_seed_src_adin_mix_in(buffer, minLen, addIn, addInLen)) { + OPENSSL_free(buffer); + buffer = NULL; + goto end; + } + + *pSeed = buffer; + buffer = NULL; + ret = minLen; + +end: + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ret > 0); + return ret; +} + +/** + * Securely zeroize and free the seed buffer. + * + * @param [in] ctx SEED-SRC context object (unused). + * @param [in, out] seed Seed to zeroize. + * @param [in] seedLen Length of seed in bytes. + */ +static void wp_seed_src_clear_seed(wp_SeedSrcCtx* ctx, unsigned char* seed, + size_t seedLen) +{ + (void)ctx; + OPENSSL_secure_clear_free(seed, seedLen); +} + +/** + * Verify zeroization of the SEED-SRC components. + * + * @param [in] ctx SEED-SRC context object (unused). + * @return 1 on success. + */ +static int wp_seed_src_verify_zeroization(wp_SeedSrcCtx* ctx) +{ + WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_seed_src_verify_zeroization"); + + (void)ctx; + + WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + 1); + return 1; +} + + +/** Dispatch table for SEED-SRC. */ +const OSSL_DISPATCH wp_seed_src_functions[] = { + { OSSL_FUNC_RAND_NEWCTX, (DFUNC)wp_seed_src_new }, + { OSSL_FUNC_RAND_FREECTX, (DFUNC)wp_seed_src_free }, + { OSSL_FUNC_RAND_INSTANTIATE, (DFUNC)wp_seed_src_instantiate }, + { OSSL_FUNC_RAND_UNINSTANTIATE, (DFUNC)wp_seed_src_uninstantiate }, + { OSSL_FUNC_RAND_GENERATE, (DFUNC)wp_seed_src_generate }, + { OSSL_FUNC_RAND_ENABLE_LOCKING, (DFUNC)wp_seed_src_enable_locking }, + { OSSL_FUNC_RAND_LOCK, (DFUNC)wp_seed_src_lock }, + { OSSL_FUNC_RAND_UNLOCK, (DFUNC)wp_seed_src_unlock }, + { OSSL_FUNC_RAND_GETTABLE_CTX_PARAMS, (DFUNC)wp_seed_src_gettable_ctx_params }, + { OSSL_FUNC_RAND_GET_CTX_PARAMS, (DFUNC)wp_seed_src_get_ctx_params }, + { OSSL_FUNC_RAND_GET_SEED, (DFUNC)wp_seed_src_get_seed }, + { OSSL_FUNC_RAND_CLEAR_SEED, (DFUNC)wp_seed_src_clear_seed }, + { OSSL_FUNC_RAND_VERIFY_ZEROIZATION, (DFUNC)wp_seed_src_verify_zeroization }, + { 0, NULL } +}; + +#endif /* WP_HAVE_SEED_SRC */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index daab28bf..4f1fec58 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -20,6 +20,7 @@ #include #include + #include #include #include @@ -192,14 +193,14 @@ static int bio_core_new(BIO *bio) WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } - + static int bio_core_free(BIO *bio) { WOLFPROV_ENTER(WP_LOG_COMP_PROVIDER, "bio_core_free"); BIO_set_init(bio, 0); wolfssl_prov_bio_free(BIO_get_data(bio)); - + WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -247,6 +248,17 @@ static WOLFPROV_CTX* wolfssl_prov_ctx_new(void) } } +#ifdef WP_HAVE_SEED_SRC + /* Initialize urandom subsystem (registers seed callback, lazy file open) */ + if (ctx != NULL) { + if (wp_urandom_init() != 0) { + /* Non-fatal - fall back to normal RNG on platforms without urandom */ + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + "wp_urandom_init failed, will use WC_RNG instead", 0); + } + } +#endif + return ctx; } @@ -257,6 +269,11 @@ static WOLFPROV_CTX* wolfssl_prov_ctx_new(void) */ static void wolfssl_prov_ctx_free(WOLFPROV_CTX* ctx) { +#ifdef WP_HAVE_SEED_SRC + /* Clean up urandom subsystem */ + wp_urandom_cleanup(); +#endif + BIO_meth_free(ctx->coreBioMethod); #ifndef WP_SINGLE_THREADED wc_FreeMutex(&ctx->rng_mutex); @@ -570,6 +587,10 @@ static const OSSL_ALGORITHM wolfprov_kdfs[] = { /* List of RNG algorithm implementations available in wolfSSL provider. */ static const OSSL_ALGORITHM wolfprov_rands[] = { +#ifdef WP_HAVE_SEED_SRC + { WP_NAMES_SEED_SRC, WOLFPROV_PROPERTIES, wp_seed_src_functions, + "" }, +#endif { WP_NAMES_CTR_DRBG, WOLFPROV_PROPERTIES, wp_drbg_functions, "" }, { WP_NAMES_HASH_DRBG, WOLFPROV_PROPERTIES, wp_drbg_functions, @@ -766,7 +787,7 @@ static const OSSL_ALGORITHM wolfprov_encoder[] = { /*{ WP_NAMES_RSA, WOLFPROV_PROPERTIES ",output=text", \*/ { WP_NAMES_RSA, WP_ENCODER_PROPERTIES(type-specific, text), \ wp_rsa_text_encoder_functions, - "" }, + "" }, #endif #endif /* WP_HAVE_RSA */ @@ -1374,4 +1395,3 @@ int OSSL_provider_init(const OSSL_CORE_HANDLE* handle, return wolfssl_provider_init(handle, in, out, provCtx); } #endif - diff --git a/test/include.am b/test/include.am index 6dbad0cc..97793609 100644 --- a/test/include.am +++ b/test/include.am @@ -17,6 +17,7 @@ test_unit_test_SOURCES = \ test/test_cmac.c \ test/test_dh.c \ test/test_digest.c \ + test/test_drbg_seed_src.c \ test/test_ecc.c \ test/test_ecx.c \ test/test_gmac.c \ @@ -30,6 +31,7 @@ test_unit_test_SOURCES = \ test/test_pkcs7_x509.c \ test/test_rand.c \ test/test_rsa.c \ + test/test_seccomp_sandbox.c \ test/test_tls1_prf.c \ test/unit.c test_unit_test_LDADD = libwolfprov.la diff --git a/test/test_drbg_seed_src.c b/test/test_drbg_seed_src.c new file mode 100644 index 00000000..11333c3e --- /dev/null +++ b/test/test_drbg_seed_src.c @@ -0,0 +1,484 @@ +/* test_drbg_seed_src.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider 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 3 of the License, or + * (at your option) any later version. + * + * wolfProvider 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 wolfProvider. If not, see . + */ + +/* + * This test file verifies the SEED-SRC and parent-child DRBG hierarchy + * that is needed for OpenSSH sandbox compatibility. + * + * OpenSSH Flow: + * 1. Provider loads, SEED-SRC is initialized (can access /dev/urandom) + * 2. OpenSSH forks and calls RAND_poll() BEFORE activating sandbox + * 3. This instantiates the DRBG hierarchy: SEED-SRC -> Primary DRBG -> Child DRBGs + * 4. After sandbox activation, child DRBGs get entropy from parent (no file I/O) + * + * NOTE: This test requires WP_HAVE_SEED_SRC to be defined (--enable-seed-src). + */ + +#include "unit.h" + +#ifdef WP_HAVE_SEED_SRC + +#include +#include +#include + +/** + * Test that we can fetch a SEED-SRC and generate random bytes from it. + * + * This tests the basic SEED-SRC functionality. + */ +static int test_seed_src_basic(OSSL_LIB_CTX *libCtx, const char *propq) +{ + int err = 0; + EVP_RAND *seed_src = NULL; + EVP_RAND_CTX *seed_ctx = NULL; + unsigned char buf[32]; + int state; + + PRINT_MSG("Testing SEED-SRC basic functionality with propq: %s", + propq ? propq : "(null)"); + + /* Fetch SEED-SRC */ + seed_src = EVP_RAND_fetch(libCtx, "SEED-SRC", propq); + if (seed_src == NULL) { + PRINT_ERR_MSG("Failed to fetch SEED-SRC"); + err = 1; + goto cleanup; + } + PRINT_MSG("Fetched SEED-SRC: %s", EVP_RAND_get0_name(seed_src)); + + /* Create context */ + seed_ctx = EVP_RAND_CTX_new(seed_src, NULL); + if (seed_ctx == NULL) { + PRINT_ERR_MSG("Failed to create SEED-SRC context"); + err = 1; + goto cleanup; + } + + /* Instantiate */ + if (EVP_RAND_instantiate(seed_ctx, 0, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate SEED-SRC"); + err = 1; + goto cleanup; + } + + /* Check state using EVP_RAND_get_state */ + state = EVP_RAND_get_state(seed_ctx); + if (state != EVP_RAND_STATE_READY) { + PRINT_ERR_MSG("SEED-SRC not in READY state: %d", state); + err = 1; + goto cleanup; + } + PRINT_MSG("SEED-SRC is in READY state"); + + /* Generate random bytes */ + if (EVP_RAND_generate(seed_ctx, buf, sizeof(buf), 0, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Failed to generate from SEED-SRC"); + err = 1; + goto cleanup; + } + PRINT_BUFFER("SEED-SRC output", buf, sizeof(buf)); + + PRINT_MSG("SEED-SRC basic test passed"); + +cleanup: + EVP_RAND_CTX_free(seed_ctx); + EVP_RAND_free(seed_src); + return err; +} + +/** + * Test the parent-child DRBG hierarchy: SEED-SRC -> CTR-DRBG + * + * This mimics what OpenSSL does internally and what OpenSSH relies on. + */ +static int test_seed_src_parent_child(OSSL_LIB_CTX *libCtx, const char *propq) +{ + int err = 0; + EVP_RAND *seed_src = NULL; + EVP_RAND *ctr_drbg = NULL; + EVP_RAND_CTX *seed_ctx = NULL; + EVP_RAND_CTX *drbg_ctx = NULL; + unsigned char buf[64]; + + PRINT_MSG("Testing SEED-SRC -> CTR-DRBG parent-child hierarchy with propq: %s", + propq ? propq : "(null)"); + + /* Fetch SEED-SRC */ + seed_src = EVP_RAND_fetch(libCtx, "SEED-SRC", propq); + if (seed_src == NULL) { + PRINT_ERR_MSG("Failed to fetch SEED-SRC"); + err = 1; + goto cleanup; + } + + /* Create SEED-SRC context (parent) */ + seed_ctx = EVP_RAND_CTX_new(seed_src, NULL); + if (seed_ctx == NULL) { + PRINT_ERR_MSG("Failed to create SEED-SRC context"); + err = 1; + goto cleanup; + } + + /* Instantiate SEED-SRC parent first */ + if (EVP_RAND_instantiate(seed_ctx, 0, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate SEED-SRC parent"); + err = 1; + goto cleanup; + } + PRINT_MSG("Instantiated SEED-SRC parent"); + + /* Fetch CTR-DRBG */ + ctr_drbg = EVP_RAND_fetch(libCtx, "CTR-DRBG", propq); + if (ctr_drbg == NULL) { + PRINT_ERR_MSG("Failed to fetch CTR-DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Fetched CTR-DRBG: %s", EVP_RAND_get0_name(ctr_drbg)); + + /* Create CTR-DRBG context with SEED-SRC as parent */ + drbg_ctx = EVP_RAND_CTX_new(ctr_drbg, seed_ctx); + if (drbg_ctx == NULL) { + PRINT_ERR_MSG("Failed to create CTR-DRBG context with parent"); + err = 1; + goto cleanup; + } + PRINT_MSG("Created CTR-DRBG with SEED-SRC as parent"); + + /* Set cipher parameter before instantiation */ + { + OSSL_PARAM params[2]; + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_CIPHER, + (char*)"AES-256-CTR", 0); + params[1] = OSSL_PARAM_construct_end(); + if (EVP_RAND_CTX_set_params(drbg_ctx, params) != 1) { + PRINT_ERR_MSG("Failed to set CTR-DRBG cipher param"); + err = 1; + goto cleanup; + } + } + + /* Instantiate CTR-DRBG - this should get entropy from parent SEED-SRC */ + if (EVP_RAND_instantiate(drbg_ctx, 256, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate CTR-DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Instantiated CTR-DRBG (got entropy from SEED-SRC parent)"); + + /* Generate random bytes from child DRBG */ + if (EVP_RAND_generate(drbg_ctx, buf, sizeof(buf), 256, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Failed to generate from CTR-DRBG"); + err = 1; + goto cleanup; + } + PRINT_BUFFER("CTR-DRBG output (via SEED-SRC parent)", buf, sizeof(buf)); + + /* Generate more bytes to ensure it continues working */ + if (EVP_RAND_generate(drbg_ctx, buf, sizeof(buf), 256, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Failed to generate second block from CTR-DRBG"); + err = 1; + goto cleanup; + } + PRINT_BUFFER("CTR-DRBG second output", buf, sizeof(buf)); + + PRINT_MSG("SEED-SRC -> CTR-DRBG parent-child test passed"); + +cleanup: + EVP_RAND_CTX_free(drbg_ctx); + EVP_RAND_CTX_free(seed_ctx); + EVP_RAND_free(ctr_drbg); + EVP_RAND_free(seed_src); + return err; +} + +/** + * Test the parent-child DRBG hierarchy: SEED-SRC -> HASH-DRBG + */ +static int test_seed_src_hash_drbg(OSSL_LIB_CTX *libCtx, const char *propq) +{ + int err = 0; + EVP_RAND *seed_src = NULL; + EVP_RAND *hash_drbg = NULL; + EVP_RAND_CTX *seed_ctx = NULL; + EVP_RAND_CTX *drbg_ctx = NULL; + unsigned char buf[64]; + + PRINT_MSG("Testing SEED-SRC -> HASH-DRBG parent-child hierarchy with propq: %s", + propq ? propq : "(null)"); + + /* Fetch SEED-SRC */ + seed_src = EVP_RAND_fetch(libCtx, "SEED-SRC", propq); + if (seed_src == NULL) { + PRINT_ERR_MSG("Failed to fetch SEED-SRC"); + err = 1; + goto cleanup; + } + + /* Create SEED-SRC context (parent) */ + seed_ctx = EVP_RAND_CTX_new(seed_src, NULL); + if (seed_ctx == NULL) { + PRINT_ERR_MSG("Failed to create SEED-SRC context"); + err = 1; + goto cleanup; + } + + /* Instantiate SEED-SRC parent first */ + if (EVP_RAND_instantiate(seed_ctx, 0, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate SEED-SRC parent"); + err = 1; + goto cleanup; + } + PRINT_MSG("Instantiated SEED-SRC parent"); + + /* Fetch HASH-DRBG */ + hash_drbg = EVP_RAND_fetch(libCtx, "HASH-DRBG", propq); + if (hash_drbg == NULL) { + PRINT_ERR_MSG("Failed to fetch HASH-DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Fetched HASH-DRBG: %s", EVP_RAND_get0_name(hash_drbg)); + + /* Create HASH-DRBG context with SEED-SRC as parent */ + drbg_ctx = EVP_RAND_CTX_new(hash_drbg, seed_ctx); + if (drbg_ctx == NULL) { + PRINT_ERR_MSG("Failed to create HASH-DRBG context with parent"); + err = 1; + goto cleanup; + } + PRINT_MSG("Created HASH-DRBG with SEED-SRC as parent"); + + /* Set digest parameter before instantiation */ + { + OSSL_PARAM params[2]; + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_DIGEST, + (char*)"SHA-256", 0); + params[1] = OSSL_PARAM_construct_end(); + if (EVP_RAND_CTX_set_params(drbg_ctx, params) != 1) { + PRINT_ERR_MSG("Failed to set HASH-DRBG digest param"); + err = 1; + goto cleanup; + } + } + + /* Instantiate HASH-DRBG - this should get entropy from parent SEED-SRC */ + if (EVP_RAND_instantiate(drbg_ctx, 256, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate HASH-DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Instantiated HASH-DRBG (got entropy from SEED-SRC parent)"); + + /* Generate random bytes from child DRBG */ + if (EVP_RAND_generate(drbg_ctx, buf, sizeof(buf), 256, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Failed to generate from HASH-DRBG"); + err = 1; + goto cleanup; + } + PRINT_BUFFER("HASH-DRBG output (via SEED-SRC parent)", buf, sizeof(buf)); + + PRINT_MSG("SEED-SRC -> HASH-DRBG parent-child test passed"); + +cleanup: + EVP_RAND_CTX_free(drbg_ctx); + EVP_RAND_CTX_free(seed_ctx); + EVP_RAND_free(hash_drbg); + EVP_RAND_free(seed_src); + return err; +} + +/** + * Test a three-level hierarchy: SEED-SRC -> Primary DRBG -> Public DRBG + * + * This is closer to how OpenSSL actually structures its internal DRBGs. + */ +static int test_seed_src_three_level(OSSL_LIB_CTX *libCtx, const char *propq) +{ + int err = 0; + EVP_RAND *seed_src = NULL; + EVP_RAND *ctr_drbg = NULL; + EVP_RAND_CTX *seed_ctx = NULL; + EVP_RAND_CTX *primary_ctx = NULL; + EVP_RAND_CTX *public_ctx = NULL; + unsigned char buf[32]; + + PRINT_MSG("Testing three-level hierarchy: SEED-SRC -> Primary -> Public"); + + /* Fetch algorithms */ + seed_src = EVP_RAND_fetch(libCtx, "SEED-SRC", propq); + ctr_drbg = EVP_RAND_fetch(libCtx, "CTR-DRBG", propq); + if (seed_src == NULL || ctr_drbg == NULL) { + PRINT_ERR_MSG("Failed to fetch RAND algorithms"); + err = 1; + goto cleanup; + } + + /* Level 1: SEED-SRC (root entropy source) */ + seed_ctx = EVP_RAND_CTX_new(seed_src, NULL); + if (seed_ctx == NULL) { + PRINT_ERR_MSG("Failed to create SEED-SRC context"); + err = 1; + goto cleanup; + } + + /* Instantiate SEED-SRC */ + if (EVP_RAND_instantiate(seed_ctx, 0, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate SEED-SRC"); + err = 1; + goto cleanup; + } + PRINT_MSG("Created Level 1: SEED-SRC (root entropy source)"); + + /* Level 2: Primary DRBG (child of SEED-SRC) */ + primary_ctx = EVP_RAND_CTX_new(ctr_drbg, seed_ctx); + if (primary_ctx == NULL) { + PRINT_ERR_MSG("Failed to create primary CTR-DRBG context"); + err = 1; + goto cleanup; + } + + { + OSSL_PARAM params[2]; + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_CIPHER, + (char*)"AES-256-CTR", 0); + params[1] = OSSL_PARAM_construct_end(); + if (EVP_RAND_CTX_set_params(primary_ctx, params) != 1) { + PRINT_ERR_MSG("Failed to set primary CTR-DRBG cipher param"); + err = 1; + goto cleanup; + } + } + + if (EVP_RAND_instantiate(primary_ctx, 256, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate primary DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Created Level 2: Primary CTR-DRBG (parent: SEED-SRC)"); + + /* Level 3: Public DRBG (child of Primary DRBG) */ + public_ctx = EVP_RAND_CTX_new(ctr_drbg, primary_ctx); + if (public_ctx == NULL) { + PRINT_ERR_MSG("Failed to create public CTR-DRBG context"); + err = 1; + goto cleanup; + } + + { + OSSL_PARAM params[2]; + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_CIPHER, + (char*)"AES-256-CTR", 0); + params[1] = OSSL_PARAM_construct_end(); + if (EVP_RAND_CTX_set_params(public_ctx, params) != 1) { + PRINT_ERR_MSG("Failed to set public CTR-DRBG cipher param"); + err = 1; + goto cleanup; + } + } + + if (EVP_RAND_instantiate(public_ctx, 256, 0, NULL, 0, NULL) != 1) { + PRINT_ERR_MSG("Failed to instantiate public DRBG"); + err = 1; + goto cleanup; + } + PRINT_MSG("Created Level 3: Public CTR-DRBG (parent: Primary DRBG)"); + + /* Generate from the public (leaf) DRBG */ + if (EVP_RAND_generate(public_ctx, buf, sizeof(buf), 256, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Failed to generate from public DRBG"); + err = 1; + goto cleanup; + } + PRINT_BUFFER("Public DRBG output (3-level hierarchy)", buf, sizeof(buf)); + + PRINT_MSG("Three-level hierarchy test passed"); + +cleanup: + EVP_RAND_CTX_free(public_ctx); + EVP_RAND_CTX_free(primary_ctx); + EVP_RAND_CTX_free(seed_ctx); + EVP_RAND_free(ctr_drbg); + EVP_RAND_free(seed_src); + return err; +} + +/** + * Main test entry point - runs tests with OpenSSL default provider and wolfProvider. + */ +int test_drbg_seed_src(void *data) +{ + int err = 0; + + (void)data; + + PRINT_MSG("=== Testing DRBG SEED-SRC hierarchy with OpenSSL default provider ==="); + + /* Test with OpenSSL default provider */ + err = test_seed_src_basic(osslLibCtx, NULL); + if (err == 0) { + err = test_seed_src_parent_child(osslLibCtx, NULL); + } + if (err == 0) { + err = test_seed_src_hash_drbg(osslLibCtx, NULL); + } + if (err == 0) { + err = test_seed_src_three_level(osslLibCtx, NULL); + } + + if (err == 0) { + PRINT_MSG("=== OpenSSL default provider tests passed ==="); + } + + /* Test with wolfProvider */ + if (err == 0) { + PRINT_MSG("=== Testing DRBG SEED-SRC hierarchy with wolfProvider ==="); + err = test_seed_src_basic(wpLibCtx, NULL); + } + if (err == 0) { + err = test_seed_src_parent_child(wpLibCtx, NULL); + } + if (err == 0) { + err = test_seed_src_hash_drbg(wpLibCtx, NULL); + } + if (err == 0) { + err = test_seed_src_three_level(wpLibCtx, NULL); + } + + if (err == 0) { + PRINT_MSG("=== All DRBG SEED-SRC hierarchy tests passed ==="); + } + + return err; +} + +#else /* !WP_HAVE_SEED_SRC */ + +int test_drbg_seed_src(void *data) +{ + (void)data; + PRINT_MSG("SEED-SRC test skipped - not enabled"); + PRINT_MSG("Enable with: ./configure --enable-seed-src"); + return 0; +} + +#endif /* WP_HAVE_SEED_SRC */ + diff --git a/test/test_seccomp_sandbox.c b/test/test_seccomp_sandbox.c new file mode 100644 index 00000000..82d65b94 --- /dev/null +++ b/test/test_seccomp_sandbox.c @@ -0,0 +1,434 @@ +/* test_seccomp_sandbox.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider 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 3 of the License, or + * (at your option) any later version. + * + * wolfProvider 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 wolfProvider. If not, see . + */ + +/* + * This test mimics OpenSSH's seccomp sandbox behavior to verify that + * wolfProvider's DRBG can operate correctly after fork() when file + * descriptor operations are restricted. + * + * OpenSSH Flow (sshd-session.c privsep_preauth): + * 1. Parent forks child for privilege separation + * 2. Child calls reseed_prngs() BEFORE sandbox (RAND_poll, RAND_bytes) + * 3. Child applies seccomp sandbox (blocks open/openat syscalls) + * 4. Child performs crypto operations under sandbox restrictions + * + * The problem: After sandbox is applied, if DRBG needs to reseed, + * it cannot open /dev/urandom because openat() returns EACCES. + * + * This test verifies: + * - Default OpenSSL provider works under these conditions (baseline) + * - wolfProvider should eventually work (currently expected to fail) + * + * NOTE: This test is disabled by default because it uses seccomp which + * can interfere with other tests and debugging. Enable it by defining + * WP_TEST_SECCOMP_SANDBOX in CFLAGS: + * ./configure CFLAGS="-DWP_TEST_SECCOMP_SANDBOX" + */ + +#include "unit.h" + +#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Check for seccomp support */ +#ifdef __has_include + #if __has_include() && __has_include() + #define WP_HAVE_SECCOMP 1 + #endif +#else + /* Fallback: assume available on Linux */ + #define WP_HAVE_SECCOMP 1 +#endif + +#ifdef WP_HAVE_SECCOMP + +#include +#include +#include +#include + +/* Determine the correct audit architecture */ +#if defined(__x86_64__) + #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64 +#elif defined(__i386__) + #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_I386 +#elif defined(__aarch64__) + #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_AARCH64 +#elif defined(__arm__) + #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARM +#else + /* Unsupported architecture - disable test */ + #undef WP_HAVE_SECCOMP +#endif + +#endif /* WP_HAVE_SECCOMP */ + +#ifdef WP_HAVE_SECCOMP + +/* BPF macros for seccomp filter - mirrors OpenSSH sandbox-seccomp-filter.c */ +#define SC_DENY(_nr, _errno) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 1), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno)) + +#define SC_ALLOW(_nr) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 1), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW) + +/* + * Minimal seccomp filter that mimics OpenSSH's preauth sandbox. + * Key restrictions: + * - Deny open/openat with EACCES (prevents opening /dev/urandom) + * - Allow essential syscalls for the test to run + */ +static const struct sock_filter naomi_insns[] = { + /* Verify architecture */ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, arch)), + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, SECCOMP_AUDIT_ARCH, 1, 0), + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL), + + /* Load syscall number */ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)), + + /* Deny file open syscalls with EACCES - this is the key restriction */ +#ifdef __NR_open + SC_DENY(__NR_open, EACCES), +#endif +#ifdef __NR_openat + SC_DENY(__NR_openat, EACCES), +#endif + + /* Allow syscalls needed for the test to function */ +#ifdef __NR_read + SC_ALLOW(__NR_read), +#endif +#ifdef __NR_write + SC_ALLOW(__NR_write), +#endif +#ifdef __NR_close + SC_ALLOW(__NR_close), +#endif +#ifdef __NR_exit_group + SC_ALLOW(__NR_exit_group), +#endif +#ifdef __NR_exit + SC_ALLOW(__NR_exit), +#endif +#ifdef __NR_brk + SC_ALLOW(__NR_brk), +#endif +#ifdef __NR_mmap + SC_ALLOW(__NR_mmap), +#endif +#ifdef __NR_munmap + SC_ALLOW(__NR_munmap), +#endif +#ifdef __NR_mprotect + SC_ALLOW(__NR_mprotect), +#endif +#ifdef __NR_futex + SC_ALLOW(__NR_futex), +#endif +#ifdef __NR_getrandom + SC_ALLOW(__NR_getrandom), +#endif +#ifdef __NR_getpid + SC_ALLOW(__NR_getpid), +#endif +#ifdef __NR_gettid + SC_ALLOW(__NR_gettid), +#endif +#ifdef __NR_rt_sigprocmask + SC_ALLOW(__NR_rt_sigprocmask), +#endif +#ifdef __NR_rt_sigaction + SC_ALLOW(__NR_rt_sigaction), +#endif +#ifdef __NR_clock_gettime + SC_ALLOW(__NR_clock_gettime), +#endif +#ifdef __NR_nanosleep + SC_ALLOW(__NR_nanosleep), +#endif +#ifdef __NR_sched_yield + SC_ALLOW(__NR_sched_yield), +#endif +#ifdef __NR_mremap + SC_ALLOW(__NR_mremap), +#endif +#ifdef __NR_madvise + SC_ALLOW(__NR_madvise), +#endif + + /* Default: allow other syscalls (we only want to block file opens) */ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), +}; + +static const struct sock_fprog naomi_program = { + .len = (unsigned short)(sizeof(naomi_insns) / sizeof(naomi_insns[0])), + .filter = (struct sock_filter *)naomi_insns, +}; + +/* + * Apply seccomp sandbox restrictions mimicking OpenSSH behavior. + * Returns 0 on success, -1 on failure. + */ +static int apply_seccomp_sandbox(void) +{ + /* + * Set resource limits like OpenSSH does. + * RLIMIT_NOFILE = 1 allows existing fds but prevents new ones. + * Note: OpenSSH uses 1, not 0, because poll() fails with EINVAL + * if npfds > RLIMIT_NOFILE. + */ + struct rlimit rl_zero = {0, 0}; + struct rlimit rl_one = {1, 1}; + + if (setrlimit(RLIMIT_FSIZE, &rl_zero) == -1) { + PRINT_ERR_MSG("setrlimit(RLIMIT_FSIZE) failed: %s", strerror(errno)); + return -1; + } + + if (setrlimit(RLIMIT_NOFILE, &rl_one) == -1) { + PRINT_ERR_MSG("setrlimit(RLIMIT_NOFILE) failed: %s", strerror(errno)); + return -1; + } + + /* Apply seccomp filter - must set NO_NEW_PRIVS first */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { + PRINT_ERR_MSG("prctl(PR_SET_NO_NEW_PRIVS) failed: %s", strerror(errno)); + return -1; + } + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &naomi_program) == -1) { + PRINT_ERR_MSG("prctl(PR_SET_SECCOMP) failed: %s", strerror(errno)); + return -1; + } + + return 0; +} + +/* + * Child process test function. + * Applies sandbox and attempts RAND_bytes operations. + * Returns 0 on success, non-zero on failure. + */ +static int child_test_rand_under_sandbox(OSSL_LIB_CTX *libCtx) +{ + unsigned char buf[32]; + OSSL_LIB_CTX *origCtx; + int err = 0; + + /* Set the library context */ + origCtx = OSSL_LIB_CTX_set0_default(libCtx); + + /* Apply seccomp sandbox - mimics ssh_sandbox_child() */ + if (apply_seccomp_sandbox() != 0) { + PRINT_ERR_MSG("Failed to apply seccomp sandbox"); + OSSL_LIB_CTX_set0_default(origCtx); + return 1; + } + + /* + * Now try to generate random bytes under sandbox. + * This is the critical test - the DRBG may need to reseed, + * but it cannot open /dev/urandom because openat() is blocked. + */ + if (RAND_bytes(buf, sizeof(buf)) != 1) { + PRINT_ERR_MSG("RAND_bytes failed under sandbox"); + err = 1; + } + + /* Try a second call to potentially trigger reseed */ + if (err == 0 && RAND_bytes(buf, sizeof(buf)) != 1) { + PRINT_ERR_MSG("Second RAND_bytes failed under sandbox"); + err = 1; + } + + OSSL_LIB_CTX_set0_default(origCtx); + return err; +} + +/* + * Run the fork+sandbox test for a given library context. + * Returns 0 on success, non-zero on failure. + */ +static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) +{ + pid_t pid; + int status; + unsigned char buf[32]; + OSSL_LIB_CTX *origCtx; + + PRINT_MSG("Testing %s provider with fork+sandbox", provName); + + /* Pre-fork: Initialize DRBG by generating some random bytes */ + origCtx = OSSL_LIB_CTX_set0_default(libCtx); + if (RAND_bytes(buf, sizeof(buf)) != 1) { + PRINT_ERR_MSG("Pre-fork RAND_bytes failed for %s", provName); + OSSL_LIB_CTX_set0_default(origCtx); + return 1; + } + OSSL_LIB_CTX_set0_default(origCtx); + + PRINT_MSG("Pre-fork RAND_bytes succeeded, forking child..."); + + /* Fork child process - mimics OpenSSH privsep_preauth() */ + pid = fork(); + if (pid == -1) { + PRINT_ERR_MSG("fork() failed: %s", strerror(errno)); + return 1; + } + + if (pid == 0) { + /* Child process */ + int child_err; + + /* + * Note: In OpenSSH, reseed_prngs() is called here BEFORE sandbox. + * We intentionally skip that step to test the failure case. + * Once wolfProvider has proper fork-safe DRBG, the reseed + * should happen automatically or the DRBG should work without + * needing to open /dev/urandom. + */ + + child_err = child_test_rand_under_sandbox(libCtx); + + /* Exit with status indicating success (0) or failure (1) */ + _exit(child_err); + } + + /* Parent process - wait for child */ + if (waitpid(pid, &status, 0) == -1) { + PRINT_ERR_MSG("waitpid() failed: %s", strerror(errno)); + return 1; + } + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) { + PRINT_MSG("%s: Child succeeded under sandbox", provName); + return 0; + } + else { + PRINT_MSG("%s: Child failed under sandbox (exit code %d)", + provName, exit_code); + return 1; + } + } + else if (WIFSIGNALED(status)) { + PRINT_ERR_MSG("%s: Child killed by signal %d", provName, + WTERMSIG(status)); + return 1; + } + + PRINT_ERR_MSG("%s: Child exited abnormally", provName); + return 1; +} + +/* + * Main test function for seccomp sandbox DRBG behavior. + * + * This test verifies: + * 1. Default OpenSSL provider works under fork+sandbox (baseline) + * 2. wolfProvider behavior under fork+sandbox + * + * Currently wolfProvider is EXPECTED TO FAIL because the DRBG + * tries to open /dev/urandom after fork, which is blocked by seccomp. + */ +int test_seccomp_sandbox(void *data) +{ + int err = 0; + int wp_err; + + (void)data; + + PRINT_MSG("=== Seccomp Sandbox DRBG Test ==="); + PRINT_MSG("This test mimics OpenSSH's fork+sandbox behavior"); + + /* + * Test 1: Default OpenSSL provider (baseline - should pass) + * This verifies our test harness works correctly. + */ + PRINT_MSG(""); + PRINT_MSG("--- Test with OpenSSL default provider (baseline) ---"); + err = run_fork_sandbox_test(osslLibCtx, "OpenSSL default"); + if (err != 0) { + PRINT_ERR_MSG("BASELINE FAILED: OpenSSL default provider failed"); + PRINT_ERR_MSG("This indicates a problem with the test itself"); + return err; + } + PRINT_MSG("OpenSSL default provider: PASSED (baseline verified)"); + + /* + * Test 2: wolfProvider + * Currently expected to fail because DRBG tries to open /dev/urandom + * after fork, which is blocked by seccomp. + */ + PRINT_MSG(""); + PRINT_MSG("--- Test with wolfProvider ---"); + wp_err = run_fork_sandbox_test(wpLibCtx, "wolfProvider"); + + if (wp_err != 0) { + PRINT_MSG("wolfProvider: FAILED (expected - DRBG cannot reseed)"); + PRINT_MSG("This is expected until fork-safe DRBG is implemented"); + /* + * Return the error so the test is marked as failed. + * Once the fix is implemented, this test should pass. + */ + return wp_err; + } + + PRINT_MSG("wolfProvider: PASSED"); + PRINT_MSG("=== All seccomp sandbox tests passed ==="); + + return 0; +} + +#else /* !WP_HAVE_SECCOMP */ + +int test_seccomp_sandbox(void *data) +{ + (void)data; + PRINT_MSG("Seccomp sandbox test skipped - seccomp not available"); + return 0; +} + +#endif /* WP_HAVE_SECCOMP */ + +#else /* !(WP_TEST_SECCOMP_SANDBOX && WP_HAVE_SEED_SRC && WP_HAVE_RANDOM) */ + +int test_seccomp_sandbox(void *data) +{ + (void)data; + PRINT_MSG("Seccomp sandbox test skipped - not enabled or not supported"); + PRINT_MSG("Requires: --enable-seed-src and -DWP_TEST_SECCOMP_SANDBOX"); + return 0; +} + +#endif /* WP_TEST_SECCOMP_SANDBOX && WP_HAVE_SEED_SRC && WP_HAVE_RANDOM */ + diff --git a/test/unit.c b/test/unit.c index 399b3eb2..fd044da5 100644 --- a/test/unit.c +++ b/test/unit.c @@ -278,6 +278,8 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_RANDOM TEST_DECL(test_random, NULL), #endif + TEST_DECL(test_drbg_seed_src, NULL), + TEST_DECL(test_seccomp_sandbox, NULL), #ifdef WP_HAVE_DH TEST_DECL(test_dh_pgen_pkey, NULL), TEST_DECL(test_dh_pkey, NULL), diff --git a/test/unit.h b/test/unit.h index b0fdadfd..9213dfff 100644 --- a/test/unit.h +++ b/test/unit.h @@ -218,6 +218,12 @@ int test_random(void *data); #endif +/* DRBG SEED-SRC hierarchy tests */ +int test_drbg_seed_src(void *data); + +/* Seccomp sandbox test - mimics OpenSSH fork+sandbox behavior */ +int test_seccomp_sandbox(void *data); + int test_digest_sign(EVP_PKEY *pkey, OSSL_LIB_CTX* libCtx, unsigned char *data, size_t len, const char *md, const EVP_MD *mgf1Md, unsigned char *sig, size_t *sigLen, int padMode, int saltlen);