rsyslog/runtime/net_ossl.c
Rainer Gerhards d128ef88a5 tls: add native post-quantum guidance and tests
Why
Native post-quantum TLS support should be usable and testable on newer
distro baselines without adding provider-mode compatibility work for
older platforms.

Impact
Rsyslog now has native-PQ smoke tests, clearer TLS diagnostics, updated
CI baselines and helper images, and a new post-quantum tutorial for
supported distros.

Before/After
Before: Fedora CI still targeted Fedora 41, PQ-capable TLS settings had
no dedicated rsyslog tests or user-facing tutorial, and stricter clang
builds could fail on warning-group handling.
After: CI targets Fedora 43, native PQ usage is documented and smoke-
tested, helper images include the required tools, and the branch builds
and tests cleanly with the newer compiler/container combinations.

Technical Overview
The CI matrix now replaces the Fedora 41 lane with Fedora 43 and adds a
matching Fedora 43 development image.
The Debian 13 and Fedora 43 development containers now install the
GnuTLS CLI utilities needed for native PQ capability checks.
The OpenSSL TLS config path logs clearer messages when a command or
value is unavailable on the native OpenSSL build.
The GnuTLS TLS config path reports unsupported priority-string options
more explicitly.
Two new shell tests add native PQ smoke coverage for OpenSSL and GnuTLS
using the existing gnutlsPriorityString control surface.
Those tests self-skip unless the local native TLS libraries expose the
required hybrid group support.
The imtcp parameter docs and omfwd docs now explain the native-only PQ
support policy and include example configurations.
A new tutorial documents native PQ usage for OpenSSL and GnuTLS on
supported newer distro versions.
The shared runtime warning policy in rsyslog.h now tolerates clang
handling of unknown warning groups so older and newer clang lanes remain
warning-free under the existing finalize_it error-handling pattern.
Testbench follow-ups harden omfwd-lb-susp with isolated retry attempts,
skip rcvr_fail_restore on ARM where it is timing-flaky, and keep local
SC2181 suppressions where if-exec rewrites would reduce shell-script
usability.
The Fedora 43 Dockerfile now cleans the dnf cache after install and
locally suppresses the non-useful DL3041 package-version pinning warning.
Older distro versions remain intentionally unsupported for PQ in this
phase because we expect users to move to newer baselines first.
If there is demand later, older-version support can be considered in a
separate effort.

With the help of AI-Agents: Codex
2026-03-11 15:10:24 +01:00

2279 lines
84 KiB
C

/* net.c
* Implementation of network-related stuff.
*
* File begun on 2023-08-29 by Alorbach (extracted from net.c)
*
* Copyright 2023 Andre Lorbach and Adiscon GmbH.
*
* This file is part of rsyslog.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* -or-
* see COPYING.ASL20 in the source distribution
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <netdb.h>
#include <sys/select.h>
#include <sys/time.h>
#include "rsyslog.h"
#include "syslogd-types.h"
#include "module-template.h"
#include "parse.h"
#include "srUtils.h"
#include "obj.h"
#include "errmsg.h"
#include "net.h"
#include "net_ossl.h"
#include "nsd_ptcp.h"
#include "rsconf.h"
#define OCSP_TIMEOUT 5
#define OCSP_CACHE_MAX_ENTRIES 100
#define OCSP_CACHE_DEFAULT_TTL 3600
/* static data */
DEFobjStaticHelpers;
DEFobjCurrIf(glbl) DEFobjCurrIf(net) DEFobjCurrIf(nsd_ptcp)
/* Prototypes for openssl helper functions */
void net_ossl_lastOpenSSLErrorMsg(
uchar *fromHost, int ret, SSL *ssl, int severity, const char *pszCallSource, const char *pszOsslApi);
void net_ossl_set_ssl_verify_callback(SSL *pSsl, int flags);
void net_ossl_set_ctx_verify_callback(SSL_CTX *pCtx, int flags);
void net_ossl_set_bio_callback(BIO *conn);
int net_ossl_verify_callback(int status, X509_STORE_CTX *store);
#if OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(LIBRESSL_VERSION_NUMBER) && !defined(ENABLE_WOLFSSL)
rsRetVal net_ossl_apply_tlscgfcmd(net_ossl_t *pThis, uchar *tlscfgcmd);
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
rsRetVal net_ossl_chkpeercertvalidity(net_ossl_t *pThis, SSL *ssl, uchar *fromHostIP);
X509 *net_ossl_getpeercert(net_ossl_t *pThis, SSL *ssl, uchar *fromHostIP);
rsRetVal net_ossl_peerfingerprint(net_ossl_t *pThis, X509 *certpeer, uchar *fromHostIP);
#ifndef ENABLE_WOLFSSL
void ocsp_cache_cleanup(void);
#endif
rsRetVal net_ossl_chkpeername(net_ossl_t *pThis, X509 *certpeer, uchar *fromHostIP);
/*--------------------------------------MT OpenSSL helpers ------------------------------------------*/
#ifndef ENABLE_WOLFSSL
static MUTEX_TYPE *mutex_buf = NULL;
static sbool openssl_initialized = 0; // Avoid multiple initialization / deinitialization
void locking_function(int mode, int n, __attribute__((unused)) const char *file, __attribute__((unused)) int line) {
if (mode & CRYPTO_LOCK)
MUTEX_LOCK(mutex_buf[n]);
else
MUTEX_UNLOCK(mutex_buf[n]);
}
unsigned long id_function(void) {
return ((unsigned long)THREAD_ID);
}
struct CRYPTO_dynlock_value *dyn_create_function(__attribute__((unused)) const char *file,
__attribute__((unused)) int line) {
struct CRYPTO_dynlock_value *value;
value = (struct CRYPTO_dynlock_value *)malloc(sizeof(struct CRYPTO_dynlock_value));
if (!value) return NULL;
MUTEX_SETUP(value->mutex);
return value;
}
void dyn_lock_function(int mode,
struct CRYPTO_dynlock_value *l,
__attribute__((unused)) const char *file,
__attribute__((unused)) int line) {
if (mode & CRYPTO_LOCK)
MUTEX_LOCK(l->mutex);
else
MUTEX_UNLOCK(l->mutex);
}
void dyn_destroy_function(struct CRYPTO_dynlock_value *l,
__attribute__((unused)) const char *file,
__attribute__((unused)) int line) {
MUTEX_CLEANUP(l->mutex);
free(l);
}
/* set up support functions for openssl multi-threading. This must
* be done at library initialisation. If the function fails,
* processing can not continue normally. On failure, 0 is
* returned, on success 1.
*/
int opensslh_THREAD_setup(void) {
int i;
if (openssl_initialized == 1) {
DBGPRINTF("openssl: multithread setup already initialized\n");
return 1;
}
mutex_buf = (MUTEX_TYPE *)malloc(CRYPTO_num_locks() * sizeof(MUTEX_TYPE));
if (mutex_buf == NULL) return 0;
for (i = 0; i < CRYPTO_num_locks(); i++) MUTEX_SETUP(mutex_buf[i]);
#if OPENSSL_VERSION_NUMBER < 0x10100000L
CRYPTO_set_id_callback(id_function);
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
CRYPTO_set_locking_callback(locking_function);
/* The following three CRYPTO_... functions are the OpenSSL functions
for registering the callbacks we implemented above */
CRYPTO_set_dynlock_create_callback(dyn_create_function);
CRYPTO_set_dynlock_lock_callback(dyn_lock_function);
CRYPTO_set_dynlock_destroy_callback(dyn_destroy_function);
DBGPRINTF("openssl: multithread setup finished\n");
openssl_initialized = 1;
return 1;
}
/* shut down openssl - do this only when you are totally done
* with openssl.
*/
int opensslh_THREAD_cleanup(void) {
int i;
if (openssl_initialized == 0) {
DBGPRINTF("openssl: multithread cleanup already done\n");
return 1;
}
if (!mutex_buf) return 0;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
CRYPTO_set_id_callback(NULL);
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
CRYPTO_set_locking_callback(NULL);
CRYPTO_set_dynlock_create_callback(NULL);
CRYPTO_set_dynlock_lock_callback(NULL);
CRYPTO_set_dynlock_destroy_callback(NULL);
for (i = 0; i < CRYPTO_num_locks(); i++) MUTEX_CLEANUP(mutex_buf[i]);
free(mutex_buf);
mutex_buf = NULL;
DBGPRINTF("openssl: multithread cleanup finished\n");
openssl_initialized = 0;
return 1;
}
#else
int opensslh_THREAD_setup(void) {
return 1;
}
int opensslh_THREAD_cleanup(void) {
return 1;
}
#endif /* !ENABLE_WOLFSSL */
/*-------------------------------------- MT OpenSSL helpers -----------------------------------------*/
/*--------------------------------------OpenSSL helpers ------------------------------------------*/
/* globally initialize OpenSSL
*/
void osslGlblInit(void) {
DBGPRINTF("osslGlblInit: ENTER\n");
#ifdef ENABLE_WOLFSSL
if (!SSL_library_init()) {
LogError(0, RS_RET_NO_ERRCODE, "Error: OpenSSL initialization failed!");
}
#else
if ((opensslh_THREAD_setup() == 0) ||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
/* Setup OpenSSL library < 1.1.0 */
!SSL_library_init()
#else
/* Setup OpenSSL library >= 1.1.0 with system default settings */
OPENSSL_init_ssl(0, NULL) == 0
#endif
) {
LogError(0, RS_RET_NO_ERRCODE, "Error: OpenSSL initialization failed!");
}
#endif
#if defined(ENABLE_WOLFSSL) && defined(DEBUG_WOLFSSL)
wolfSSL_Debugging_ON();
#endif
/* Load readable error strings */
SSL_load_error_strings();
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
/*
* ERR_load_*(), ERR_func_error_string(), ERR_get_error_line(), ERR_get_error_line_data(), ERR_get_state()
* OpenSSL now loads error strings automatically so these functions are not needed.
* SEE FOR MORE:
* https://www.openssl.org/docs/manmaster/man7/migration_guide.html
*
*/
#else
/* Load error strings into mem*/
ERR_load_BIO_strings();
ERR_load_crypto_strings();
#endif
PRAGMA_DIAGNOSTIC_PUSH
PRAGMA_IGNORE_Wdeprecated_declarations
#ifndef OPENSSL_NO_ENGINE
// Initialize OpenSSL engine library
ENGINE_load_builtin_engines();
/* Register all of them for every algorithm they collectively implement */
ENGINE_register_all_complete();
// Iterate through all available engines
ENGINE *osslEngine = ENGINE_get_first();
const char *engine_id = NULL;
const char *engine_name = NULL;
while (osslEngine) {
// Print engine ID and name if the engine is loaded
if (ENGINE_get_init_function(osslEngine)) { // Check if engine is initialized
engine_id = ENGINE_get_id(osslEngine);
engine_name = ENGINE_get_name(osslEngine);
DBGPRINTF("osslGlblInit: Loaded Engine: ID = %s, Name = %s\n", engine_id, engine_name);
}
osslEngine = ENGINE_get_next(osslEngine);
}
// Free the engine reference when done
ENGINE_free(osslEngine);
#else
DBGPRINTF("osslGlblInit: OpenSSL compiled without ENGINE support - ENGINE support disabled\n");
#endif /* OPENSSL_NO_ENGINE */
PRAGMA_DIAGNOSTIC_POP
}
/* globally de-initialize OpenSSL */
void osslGlblExit(void) {
DBGPRINTF("openssl: entering osslGlblExit\n");
#ifndef ENABLE_WOLFSSL
ocsp_cache_cleanup();
#endif
#ifndef OPENSSL_NO_ENGINE
ENGINE_cleanup();
#endif
ERR_free_strings();
EVP_cleanup();
CRYPTO_cleanup_all_ex_data();
}
/* initialize openssl context; called on
* - listener creation
* - outbound connection creation
* Once created, the ctx object is used by-subobjects (accepted inbound connections)
*/
static rsRetVal net_ossl_osslCtxInit(net_ossl_t *pThis, const SSL_METHOD *method) {
DEFiRet;
int bHaveCA;
int bHaveCRL;
int bHaveCert;
int bHaveKey;
int bHaveExtraCAFiles;
const char *caFile, *crlFile, *certFile, *keyFile;
char *extraCaFiles, *extraCaFile;
/* Setup certificates */
caFile = (char *)((pThis->pszCAFile == NULL) ? glbl.GetDfltNetstrmDrvrCAF(runConf) : pThis->pszCAFile);
if (caFile == NULL) {
LogMsg(0, RS_RET_CA_CERT_MISSING, LOG_WARNING, "Warning: CA certificate is not set");
bHaveCA = 0;
} else {
dbgprintf("osslCtxInit: OSSL CA file: '%s'\n", caFile);
bHaveCA = 1;
}
crlFile = (char *)((pThis->pszCRLFile == NULL) ? glbl.GetDfltNetstrmDrvrCRLF(runConf) : pThis->pszCRLFile);
if (crlFile == NULL) {
bHaveCRL = 0;
} else {
dbgprintf("osslCtxInit: OSSL CRL file: '%s'\n", crlFile);
bHaveCRL = 1;
}
certFile = (char *)((pThis->pszCertFile == NULL) ? glbl.GetDfltNetstrmDrvrCertFile(runConf) : pThis->pszCertFile);
if (certFile == NULL) {
LogMsg(0, RS_RET_CERT_MISSING, LOG_WARNING, "Warning: Certificate file is not set");
bHaveCert = 0;
} else {
dbgprintf("osslCtxInit: OSSL CERT file: '%s'\n", certFile);
bHaveCert = 1;
}
keyFile = (char *)((pThis->pszKeyFile == NULL) ? glbl.GetDfltNetstrmDrvrKeyFile(runConf) : pThis->pszKeyFile);
if (keyFile == NULL) {
LogMsg(0, RS_RET_CERTKEY_MISSING, LOG_WARNING, "Warning: Key file is not set");
bHaveKey = 0;
} else {
dbgprintf("osslCtxInit: OSSL KEY file: '%s'\n", keyFile);
bHaveKey = 1;
}
extraCaFiles =
(char *)((pThis->pszExtraCAFiles == NULL) ? glbl.GetNetstrmDrvrCAExtraFiles(runConf) : pThis->pszExtraCAFiles);
if (extraCaFiles == NULL) {
bHaveExtraCAFiles = 0;
} else {
dbgprintf("osslCtxInit: OSSL EXTRA CA files: '%s'\n", extraCaFiles);
bHaveExtraCAFiles = 1;
}
/* Create main CTX Object based on method parameter */
if (pThis->ctx != NULL) {
SSL_CTX_free(pThis->ctx);
pThis->ctx = NULL;
}
pThis->ctx = SSL_CTX_new(method);
if (bHaveExtraCAFiles == 1) {
while ((extraCaFile = strsep(&extraCaFiles, ","))) {
if (SSL_CTX_load_verify_locations(pThis->ctx, extraCaFile, NULL) != 1) {
LogError(0, RS_RET_TLS_CERT_ERR,
"Error: Extra Certificate file could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "SSL_CTX_load_verify_locations");
ABORT_FINALIZE(RS_RET_TLS_CERT_ERR);
}
}
}
if (bHaveCA == 1 && SSL_CTX_load_verify_locations(pThis->ctx, caFile, NULL) != 1) {
LogError(0, RS_RET_TLS_CERT_ERR,
"Error: CA certificate could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "SSL_CTX_load_verify_locations");
ABORT_FINALIZE(RS_RET_TLS_CERT_ERR);
}
if (bHaveCRL == 1) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
// Get X509_STORE reference
X509_STORE *store = SSL_CTX_get_cert_store(pThis->ctx);
if (!X509_STORE_load_file(store, crlFile)) {
LogError(0, RS_RET_CRL_INVALID,
"Error: CRL could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "X509_STORE_load_file");
ABORT_FINALIZE(RS_RET_CRL_INVALID);
}
X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
#else
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
// Get X509_STORE reference
X509_STORE *store = SSL_CTX_get_cert_store(pThis->ctx);
// Load the CRL PEM file
FILE *fp = fopen(crlFile, "r");
if (fp == NULL) {
LogError(0, RS_RET_CRL_MISSING,
"Error: CRL could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "fopen");
ABORT_FINALIZE(RS_RET_CRL_MISSING);
}
X509_CRL *crl = PEM_read_X509_CRL(fp, NULL, NULL, NULL);
fclose(fp);
if (crl == NULL) {
LogError(0, RS_RET_CRL_INVALID,
"Error: Unable to read CRL."
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "PEM_read_X509_CRL");
ABORT_FINALIZE(RS_RET_CRL_INVALID);
}
// Add the CRL to the X509_STORE
if (!X509_STORE_add_crl(store, crl)) {
LogError(0, RS_RET_CRL_INVALID,
"Error: Unable to add CRL to store."
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "X509_STORE_add_crl");
X509_CRL_free(crl);
ABORT_FINALIZE(RS_RET_CRL_INVALID);
}
// Set the X509_STORE to the SSL_CTX
// SSL_CTX_set_cert_store(pThis->ctx, store);
// Enable CRL checking
X509_VERIFY_PARAM *param = X509_VERIFY_PARAM_new();
X509_VERIFY_PARAM_set_flags(param, X509_V_FLAG_CRL_CHECK);
SSL_CTX_set1_param(pThis->ctx, param);
X509_VERIFY_PARAM_free(param);
#else
LogError(0, RS_RET_SYS_ERR,
"Warning: TLS library does not support X509_STORE_load_file"
"(requires OpenSSL 3.x or higher). Cannot use Certificate revocation list (CRL) '%s'.",
crlFile);
#endif
#endif
}
if (bHaveCert == 1 && SSL_CTX_use_certificate_chain_file(pThis->ctx, certFile) != 1) {
LogError(0, RS_RET_TLS_CERT_ERR,
"Error: Certificate file could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "SSL_CTX_use_certificate_chain_file");
ABORT_FINALIZE(RS_RET_TLS_CERT_ERR);
}
if (bHaveKey == 1 && SSL_CTX_use_PrivateKey_file(pThis->ctx, keyFile, SSL_FILETYPE_PEM) != 1) {
LogError(0, RS_RET_TLS_KEY_ERR,
"Error: Key could not be accessed. "
"Check at least: 1) file path is correct, 2) file exist, "
"3) permissions are correct, 4) file content is correct. "
"OpenSSL error info may follow in next messages");
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "osslCtxInit", "SSL_CTX_use_PrivateKey_file");
ABORT_FINALIZE(RS_RET_TLS_KEY_ERR);
}
/* Set CTX Options */
SSL_CTX_set_options(pThis->ctx, SSL_OP_NO_SSLv2); /* Disable insecure SSLv2 Protocol */
SSL_CTX_set_options(pThis->ctx, SSL_OP_NO_SSLv3); /* Disable insecure SSLv3 Protocol */
SSL_CTX_sess_set_cache_size(pThis->ctx, 1024); /* TODO: make configurable? */
/* Set default VERIFY Options for OpenSSL CTX - and CALLBACK */
if (pThis->authMode == OSSL_AUTH_CERTANON) {
dbgprintf("osslCtxInit: SSL_VERIFY_NONE\n");
net_ossl_set_ctx_verify_callback(pThis->ctx, SSL_VERIFY_NONE);
} else {
dbgprintf("osslCtxInit: SSL_VERIFY_PEER\n");
net_ossl_set_ctx_verify_callback(pThis->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
}
SSL_CTX_set_timeout(pThis->ctx, 30); /* Default Session Timeout, TODO: Make configureable */
SSL_CTX_set_mode(pThis->ctx, SSL_MODE_AUTO_RETRY);
#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(ENABLE_WOLFSSL)
/* Enable Support for automatic ephemeral/temporary DH parameter selection. */
SSL_CTX_set_dh_auto(pThis->ctx, 1);
#endif
#ifndef ENABLE_WOLFSSL
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
#if OPENSSL_VERSION_NUMBER <= 0x101010FFL
/* Enable Support for automatic EC temporary key parameter selection. */
SSL_CTX_set_ecdh_auto(pThis->ctx, 1);
#else
/*
* SSL_CTX_set_ecdh_auto and SSL_CTX_set_tmp_ecdh are depreceated in higher
* OpenSSL Versions, so we no more need them - see for more:
* https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_ecdh_auto.html
*/
#endif
#else
dbgprintf(
"osslCtxInit: openssl to old, cannot use SSL_CTX_set_ecdh_auto."
"Using SSL_CTX_set_tmp_ecdh with NID_X9_62_prime256v1/() instead.\n");
SSL_CTX_set_tmp_ecdh(pThis->ctx, EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
#endif
#endif
finalize_it:
RETiRet;
}
/* Helper function to print usefull OpenSSL errors
*/
void net_ossl_lastOpenSSLErrorMsg(
uchar *fromHost, int ret, SSL *ssl, int severity, const char *pszCallSource, const char *pszOsslApi) {
unsigned long un_error = 0;
int iSSLErr = 0;
if (ssl == NULL) {
/* Output Error Info*/
DBGPRINTF("lastOpenSSLErrorMsg: Error in '%s' with ret=%d\n", pszCallSource, ret);
} else {
/* if object is set, get error code */
iSSLErr = SSL_get_error(ssl, ret);
/* Output Debug as well */
DBGPRINTF(
"lastOpenSSLErrorMsg: %s Error in '%s': '%s(%d)' with ret=%d, errno=%d(%s), sslapi='%s'\n",
(iSSLErr == SSL_ERROR_SSL ? "SSL_ERROR_SSL"
: (iSSLErr == SSL_ERROR_SYSCALL ? "SSL_ERROR_SYSCALL" : "SSL_ERROR_UNKNOWN")),
pszCallSource, ERR_error_string(iSSLErr, NULL), iSSLErr, ret, errno, strerror(errno), pszOsslApi);
/* Output error message */
LogMsg(0, RS_RET_NO_ERRCODE, severity, "%s Error in '%s': '%s(%d)' with ret=%d, errno=%d(%s), sslapi='%s'\n",
(iSSLErr == SSL_ERROR_SSL ? "SSL_ERROR_SSL"
: (iSSLErr == SSL_ERROR_SYSCALL ? "SSL_ERROR_SYSCALL" : "SSL_ERROR_UNKNOWN")),
pszCallSource, ERR_error_string(iSSLErr, NULL), iSSLErr, ret, errno, strerror(errno), pszOsslApi);
}
/* Loop through ERR_get_error */
while ((un_error = ERR_get_error()) > 0) {
LogMsg(0, RS_RET_NO_ERRCODE, severity, "net_ossl:remote '%s' OpenSSL Error Stack: %s", fromHost,
ERR_error_string(un_error, NULL));
}
}
#if OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(LIBRESSL_VERSION_NUMBER) && !defined(ENABLE_WOLFSSL)
/* initialize tls config commands in openssl context
*/
rsRetVal net_ossl_apply_tlscgfcmd(net_ossl_t *pThis, uchar *tlscfgcmd) {
DEFiRet;
char *pCurrentPos;
char *pNextPos;
char *pszCmd;
char *pszValue;
int iConfErr;
if (tlscfgcmd == NULL) {
FINALIZE;
}
dbgprintf("net_ossl_apply_tlscgfcmd: Apply tlscfgcmd: '%s'\n", tlscfgcmd);
/* Set working pointer */
pCurrentPos = (char *)tlscfgcmd;
if (pCurrentPos != NULL && strlen(pCurrentPos) > 0) {
// Create CTX Config Helper
SSL_CONF_CTX *cctx;
cctx = SSL_CONF_CTX_new();
if (pThis->sslState == osslServer) {
SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SERVER);
} else {
SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_CLIENT);
}
SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_FILE);
SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SHOW_ERRORS);
SSL_CONF_CTX_set_ssl_ctx(cctx, pThis->ctx);
do {
pNextPos = index(pCurrentPos, '=');
if (pNextPos != NULL) {
while (*pCurrentPos != '\0' && (*pCurrentPos == ' ' || *pCurrentPos == '\t')) pCurrentPos++;
pszCmd = strndup(pCurrentPos, pNextPos - pCurrentPos);
pCurrentPos = pNextPos + 1;
pNextPos = index(pCurrentPos, '\n');
pNextPos = (pNextPos == NULL ? index(pCurrentPos, ';') : pNextPos);
pszValue = (pNextPos == NULL ? strdup(pCurrentPos) : strndup(pCurrentPos, pNextPos - pCurrentPos));
pCurrentPos = (pNextPos == NULL ? NULL : pNextPos + 1);
/* Add SSL Conf Command */
iConfErr = SSL_CONF_cmd(cctx, pszCmd, pszValue);
if (iConfErr > 0) {
dbgprintf(
"net_ossl_apply_tlscgfcmd: Successfully added Command "
"'%s':'%s'\n",
pszCmd, pszValue);
} else {
LogError(0, RS_RET_SYS_ERR,
"Failed to add OpenSSL command %s:'%s' "
"in net_ossl_apply_tlscgfcmd with error '%d'. "
"The command or value may be unsupported by "
"this native OpenSSL build.",
pszCmd, pszValue, iConfErr);
}
free(pszCmd);
free(pszValue);
} else {
/* Abort further parsing */
pCurrentPos = NULL;
}
} while (pCurrentPos != NULL);
/* Finalize SSL Conf */
iConfErr = SSL_CONF_CTX_finish(cctx);
if (!iConfErr) {
LogError(0, RS_RET_SYS_ERR,
"Error: setting openssl command parameters: %s. "
"Requested settings may be unavailable on this native "
"OpenSSL build. "
"OpenSSL error info may follow in next messages",
tlscfgcmd);
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "net_ossl_apply_tlscgfcmd", "SSL_CONF_CTX_finish");
}
SSL_CONF_CTX_free(cctx);
}
finalize_it:
RETiRet;
}
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
/* Convert a fingerprint to printable data. The conversion is carried out
* according IETF I-D syslog-transport-tls-12. The fingerprint string is
* returned in a new cstr object. It is the caller's responsibility to
* destruct that object.
* rgerhards, 2008-05-08
*/
static rsRetVal net_ossl_genfingerprintstr(uchar *pFingerprint,
size_t sizeFingerprint,
cstr_t **ppStr,
const char *prefix) {
cstr_t *pStr = NULL;
uchar buf[4];
size_t i;
DEFiRet;
CHKiRet(rsCStrConstruct(&pStr));
CHKiRet(rsCStrAppendStrWithLen(pStr, (uchar *)prefix, strlen(prefix)));
for (i = 0; i < sizeFingerprint; ++i) {
snprintf((char *)buf, sizeof(buf), ":%2.2X", pFingerprint[i]);
CHKiRet(rsCStrAppendStrWithLen(pStr, buf, 3));
}
cstrFinalize(pStr);
*ppStr = pStr;
finalize_it:
if (iRet != RS_RET_OK) {
if (pStr != NULL) rsCStrDestruct(&pStr);
}
RETiRet;
}
/* Perform a match on ONE peer name obtained from the certificate. This name
* is checked against the set of configured credentials. *pbFoundPositiveMatch is
* set to 1 if the ID matches. *pbFoundPositiveMatch must have been initialized
* to 0 by the caller (this is a performance enhancement as we expect to be
* called multiple times).
* TODO: implemet wildcards?
* rgerhards, 2008-05-26
*/
static rsRetVal net_ossl_chkonepeername(net_ossl_t *pThis,
X509 *certpeer,
uchar *pszPeerID,
int *pbFoundPositiveMatch) {
permittedPeers_t *pPeer;
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
int osslRet;
unsigned int x509flags = 0;
#endif
char *x509name = NULL;
DEFiRet;
if (certpeer == NULL) {
ABORT_FINALIZE(RS_RET_TLS_NO_CERT);
}
ISOBJ_TYPE_assert(pThis, net_ossl);
assert(pszPeerID != NULL);
assert(pbFoundPositiveMatch != NULL);
/* Obtain Namex509 name from subject */
x509name = X509_NAME_oneline(RSYSLOG_X509_NAME_oneline(certpeer), NULL, 0);
if (pThis->pPermPeers) { /* do we have configured peer IDs? */
pPeer = pThis->pPermPeers;
while (pPeer != NULL) {
CHKiRet(net.PermittedPeerWildcardMatch(pPeer, pszPeerID, pbFoundPositiveMatch));
if (*pbFoundPositiveMatch) break;
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
/* if we did not succeed so far, try ossl X509_check_host
* ( Includes check against SubjectAlternativeName )
* if prioritizeSAN set, only check against SAN
*/
if (pThis->bSANpriority == 1) {
#if OPENSSL_VERSION_NUMBER >= 0x10100004L && !defined(LIBRESSL_VERSION_NUMBER)
x509flags = X509_CHECK_FLAG_NEVER_CHECK_SUBJECT;
#else
dbgprintf("net_ossl_chkonepeername: PrioritizeSAN not supported before OpenSSL 1.1.0\n");
#endif // OPENSSL_VERSION_NUMBER >= 0x10100004L
}
osslRet = X509_check_host(certpeer, (const char *)pPeer->pszID, strlen((const char *)pPeer->pszID),
x509flags, NULL);
if (osslRet == 1) {
/* Found Peer cert in allowed Peerslist */
dbgprintf("net_ossl_chkonepeername: Client ('%s') is allowed (X509_check_host)\n", x509name);
*pbFoundPositiveMatch = 1;
break;
} else if (osslRet < 0) {
net_ossl_lastOpenSSLErrorMsg(NULL, osslRet, NULL, LOG_ERR, "net_ossl_chkonepeername",
"X509_check_host");
ABORT_FINALIZE(RS_RET_NO_ERRCODE);
}
#endif
/* Check next peer */
pPeer = pPeer->pNext;
}
} else {
LogMsg(0, RS_RET_TLS_NO_CERT, LOG_WARNING,
"net_ossl_chkonepeername: Peername check could not be done: "
"no peernames configured.");
}
finalize_it:
if (x509name != NULL) {
OPENSSL_free(x509name);
}
RETiRet;
}
/* Check the peer's ID in fingerprint auth mode.
* rgerhards, 2008-05-22
*/
rsRetVal net_ossl_peerfingerprint(net_ossl_t *pThis, X509 *certpeer, uchar *fromHostIP) {
DEFiRet;
unsigned int n;
uchar fingerprint[20 /*EVP_MAX_MD_SIZE**/];
uchar fingerprintSha256[32 /*EVP_MAX_MD_SIZE**/];
size_t size;
size_t sizeSha256;
cstr_t *pstrFingerprint = NULL;
cstr_t *pstrFingerprintSha256 = NULL;
int bFoundPositiveMatch;
permittedPeers_t *pPeer;
const EVP_MD *fdig = EVP_sha1();
const EVP_MD *fdigSha256 = EVP_sha256();
ISOBJ_TYPE_assert(pThis, net_ossl);
if (certpeer == NULL) {
ABORT_FINALIZE(RS_RET_TLS_NO_CERT);
}
/* obtain the SHA1 fingerprint */
size = sizeof(fingerprint);
if (!X509_digest(certpeer, fdig, fingerprint, &n)) {
dbgprintf("net_ossl_peerfingerprint: error X509cert is not valid!\n");
ABORT_FINALIZE(RS_RET_INVALID_FINGERPRINT);
}
sizeSha256 = sizeof(fingerprintSha256);
if (!X509_digest(certpeer, fdigSha256, fingerprintSha256, &n)) {
dbgprintf("net_ossl_peerfingerprint: error X509cert is not valid!\n");
ABORT_FINALIZE(RS_RET_INVALID_FINGERPRINT);
}
CHKiRet(net_ossl_genfingerprintstr(fingerprint, size, &pstrFingerprint, "SHA1"));
dbgprintf("net_ossl_peerfingerprint: peer's certificate SHA1 fingerprint: %s\n",
cstrGetSzStrNoNULL(pstrFingerprint));
CHKiRet(net_ossl_genfingerprintstr(fingerprintSha256, sizeSha256, &pstrFingerprintSha256, "SHA256"));
dbgprintf("net_ossl_peerfingerprint: peer's certificate SHA256 fingerprint: %s\n",
cstrGetSzStrNoNULL(pstrFingerprintSha256));
/* now search through the permitted peers to see if we can find a permitted one */
bFoundPositiveMatch = 0;
pPeer = pThis->pPermPeers;
while (pPeer != NULL && !bFoundPositiveMatch) {
if (!rsCStrSzStrCmp(pstrFingerprint, pPeer->pszID, strlen((char *)pPeer->pszID))) {
dbgprintf("net_ossl_peerfingerprint: peer's certificate SHA1 MATCH found: %s\n", pPeer->pszID);
bFoundPositiveMatch = 1;
} else if (!rsCStrSzStrCmp(pstrFingerprintSha256, pPeer->pszID, strlen((char *)pPeer->pszID))) {
dbgprintf("net_ossl_peerfingerprint: peer's certificate SHA256 MATCH found: %s\n", pPeer->pszID);
bFoundPositiveMatch = 1;
} else {
dbgprintf("net_ossl_peerfingerprint: NOMATCH peer certificate: %s\n", pPeer->pszID);
pPeer = pPeer->pNext;
}
}
if (!bFoundPositiveMatch) {
dbgprintf("net_ossl_peerfingerprint: invalid peer fingerprint, not permitted to talk to it\n");
if (pThis->bReportAuthErr == 1) {
errno = 0;
LogMsg(0, RS_RET_INVALID_FINGERPRINT, LOG_WARNING,
"net_ossl:TLS session terminated with remote syslog server '%s': "
"Fingerprint check failed, not permitted to talk to %s",
fromHostIP, cstrGetSzStrNoNULL(pstrFingerprint));
pThis->bReportAuthErr = 0;
}
ABORT_FINALIZE(RS_RET_INVALID_FINGERPRINT);
}
finalize_it:
if (pstrFingerprint != NULL) cstrDestruct(&pstrFingerprint);
if (pstrFingerprintSha256 != NULL) cstrDestruct(&pstrFingerprintSha256);
RETiRet;
}
/* Check the peer's ID in name auth mode.
*/
rsRetVal net_ossl_chkpeername(net_ossl_t *pThis, X509 *certpeer, uchar *fromHostIP) {
DEFiRet;
uchar lnBuf[256];
int bFoundPositiveMatch;
cstr_t *pStr = NULL;
char *x509name = NULL;
ISOBJ_TYPE_assert(pThis, net_ossl);
bFoundPositiveMatch = 0;
CHKiRet(rsCStrConstruct(&pStr));
/* Obtain Namex509 name from subject */
x509name = X509_NAME_oneline(RSYSLOG_X509_NAME_oneline(certpeer), NULL, 0);
dbgprintf("net_ossl_chkpeername: checking - peername '%s' on server '%s'\n", x509name, fromHostIP);
snprintf((char *)lnBuf, sizeof(lnBuf), "name: %s; ", x509name);
CHKiRet(rsCStrAppendStr(pStr, lnBuf));
CHKiRet(net_ossl_chkonepeername(pThis, certpeer, (uchar *)x509name, &bFoundPositiveMatch));
if (!bFoundPositiveMatch) {
dbgprintf("net_ossl_chkpeername: invalid peername, not permitted to talk to it\n");
if (pThis->bReportAuthErr == 1) {
cstrFinalize(pStr);
errno = 0;
LogMsg(0, RS_RET_INVALID_FINGERPRINT, LOG_WARNING,
"net_ossl:TLS session terminated with remote syslog server: "
"peer name not authorized, not permitted to talk to %s",
cstrGetSzStrNoNULL(pStr));
pThis->bReportAuthErr = 0;
}
ABORT_FINALIZE(RS_RET_INVALID_FINGERPRINT);
} else {
dbgprintf("net_ossl_chkpeername: permitted to talk!\n");
}
finalize_it:
if (x509name != NULL) {
OPENSSL_free(x509name);
}
if (pStr != NULL) rsCStrDestruct(&pStr);
RETiRet;
}
/* check the ID of the remote peer - used for both fingerprint and
* name authentication.
*/
X509 *net_ossl_getpeercert(net_ossl_t *pThis, SSL *ssl, uchar *fromHostIP) {
X509 *certpeer;
ISOBJ_TYPE_assert(pThis, net_ossl);
/* Get peer certificate from SSL */
certpeer = SSL_get_peer_certificate(ssl);
if (certpeer == NULL) {
if (pThis->bReportAuthErr == 1 && 1) {
errno = 0;
pThis->bReportAuthErr = 0;
LogMsg(0, RS_RET_TLS_NO_CERT, LOG_WARNING,
"net_ossl:TLS session terminated with remote syslog server '%s': "
"Peer check failed, peer did not provide a certificate.",
fromHostIP);
}
}
return certpeer;
}
/* Verify the validity of the remote peer's certificate.
*/
rsRetVal net_ossl_chkpeercertvalidity(net_ossl_t __attribute__((unused)) * pThis, SSL *ssl, uchar *fromHostIP) {
DEFiRet;
int iVerErr = X509_V_OK;
ISOBJ_TYPE_assert(pThis, net_ossl);
PermitExpiredCerts *pPermitExpiredCerts = (PermitExpiredCerts *)SSL_get_ex_data(ssl, 1);
#ifdef ENABLE_WOLFSSL
iVerErr = wolfSSL_get_verify_result(ssl);
#else
iVerErr = SSL_get_verify_result(ssl);
#endif
if (iVerErr != X509_V_OK) {
if (iVerErr == X509_V_ERR_CERT_HAS_EXPIRED) {
if (pPermitExpiredCerts != NULL && *pPermitExpiredCerts == OSSL_EXPIRED_DENY) {
LogMsg(0, RS_RET_CERT_EXPIRED, LOG_INFO,
"net_ossl:TLS session terminated with remote syslog server '%s': "
"not permitted to talk to peer, certificate invalid: "
"Certificate expired: %s",
fromHostIP, X509_verify_cert_error_string(iVerErr));
ABORT_FINALIZE(RS_RET_CERT_EXPIRED);
} else if (pPermitExpiredCerts != NULL && *pPermitExpiredCerts == OSSL_EXPIRED_WARN) {
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"net_ossl:CertValidity check - warning talking to peer '%s': "
"certificate expired: %s",
fromHostIP, X509_verify_cert_error_string(iVerErr));
} else {
dbgprintf(
"net_ossl_chkpeercertvalidity: talking to peer '%s': "
"certificate expired: %s\n",
fromHostIP, X509_verify_cert_error_string(iVerErr));
} /* Else do nothing */
} else if (iVerErr == X509_V_ERR_CERT_REVOKED) {
LogMsg(0, RS_RET_CERT_REVOKED, LOG_INFO,
"net_ossl:TLS session terminated with remote syslog server '%s': "
"not permitted to talk to peer, certificate invalid: "
"certificate revoked '%s'",
fromHostIP, X509_verify_cert_error_string(iVerErr));
ABORT_FINALIZE(RS_RET_CERT_EXPIRED);
} else {
LogMsg(0, RS_RET_CERT_INVALID, LOG_INFO,
"net_ossl:TLS session terminated with remote syslog server '%s': "
"not permitted to talk to peer, certificate validation failed: %s",
fromHostIP, X509_verify_cert_error_string(iVerErr));
ABORT_FINALIZE(RS_RET_CERT_INVALID);
}
} else {
dbgprintf("net_ossl_chkpeercertvalidity: client certificate validation success: %s\n",
X509_verify_cert_error_string(iVerErr));
}
finalize_it:
RETiRet;
}
/*--------------------------------------OCSP Support ------------------------------------------*/
#ifndef ENABLE_WOLFSSL
/* OCSP is not available in WolfSSL builds */
/* OCSP Response Cache
*
* Concurrency & Locking:
* - Cache is shared across all workers (global state)
* - Protected by ocsp_cache_mutex (pthread_mutex_t)
* - All cache operations (lookup/store/cleanup) acquire mutex before access
* - Cache entries store OCSP responses with expiration times
*
* Non-blocking I/O:
* - Socket operations use non-blocking mode during connect with timeout
* - Once connected, sockets use SO_RCVTIMEO/SO_SNDTIMEO for bounded I/O
* - Maximum timeout per OCSP responder: OCSP_TIMEOUT (5 seconds)
*
* Cache Strategy:
* - Key: certificate serial number + issuer name hash (SHA-256)
* - TTL: Based on OCSP response nextUpdate field, or OCSP_CACHE_DEFAULT_TTL (1 hour)
* - Eviction: Prefers expired entries, then FIFO from tail when cache reaches MAX_ENTRIES
* - Expired entries removed during lookup to maintain cache efficiency
* - Thread-safe: All cache operations protected by mutex
*/
typedef struct ocsp_cache_entry_s {
char *cache_key; /* cert serial + issuer hash */
int cert_status; /* V_OCSP_CERTSTATUS_* */
time_t expires_at; /* when this entry expires */
struct ocsp_cache_entry_s *next;
} ocsp_cache_entry_t;
static ocsp_cache_entry_t *ocsp_cache_head = NULL;
static int ocsp_cache_count = 0;
static pthread_mutex_t ocsp_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
static char *ocsp_make_cache_key(X509 *cert, X509 *issuer) {
DEFiRet;
ASN1_INTEGER *serial = X509_get_serialNumber(cert);
unsigned char md[EVP_MAX_MD_SIZE];
unsigned int md_len = 0;
unsigned char key_md[EVP_MAX_MD_SIZE];
unsigned int key_md_len = 0;
char *key = NULL;
int key_len;
BIGNUM *bn = NULL;
char *serial_hex = NULL;
if (!serial || !issuer) ABORT_FINALIZE(RS_RET_ERR);
/* Hash issuer name for compact key */
X509_NAME *issuer_name = X509_get_subject_name(issuer);
if (!X509_NAME_digest(issuer_name, EVP_sha256(), md, &md_len)) {
ABORT_FINALIZE(RS_RET_ERR);
}
/* Hash issuer public key to avoid collisions across key rollovers */
if (!X509_pubkey_digest(issuer, EVP_sha256(), key_md, &key_md_len)) {
ABORT_FINALIZE(RS_RET_ERR);
}
/* Format: serial-number:issuer-name-hash:issuer-key-hash */
bn = ASN1_INTEGER_to_BN(serial, NULL);
if (!bn) ABORT_FINALIZE(RS_RET_ERR);
serial_hex = BN_bn2hex(bn);
if (!serial_hex) ABORT_FINALIZE(RS_RET_ERR);
key_len = strlen(serial_hex) + (md_len * 2) + (key_md_len * 2) + 3;
CHKmalloc(key = malloc(key_len));
char *p = key;
int written = snprintf(p, key_len, "%s:", serial_hex);
if (written < 0 || written >= key_len) {
ABORT_FINALIZE(RS_RET_ERR);
}
p += written;
size_t remaining = (size_t)key_len - (size_t)written;
for (unsigned int i = 0; i < md_len; i++) {
if (remaining < 3) {
ABORT_FINALIZE(RS_RET_ERR);
}
written = snprintf(p, remaining, "%02x", md[i]);
if (written != 2) {
ABORT_FINALIZE(RS_RET_ERR);
}
p += written;
remaining -= (size_t)written;
}
if (remaining < 2) {
ABORT_FINALIZE(RS_RET_ERR);
}
*p++ = ':';
*p = '\0';
remaining--;
for (unsigned int i = 0; i < key_md_len; i++) {
if (remaining < 3) {
ABORT_FINALIZE(RS_RET_ERR);
}
written = snprintf(p, remaining, "%02x", key_md[i]);
if (written != 2) {
ABORT_FINALIZE(RS_RET_ERR);
}
p += written;
remaining -= (size_t)written;
}
finalize_it:
if (iRet != RS_RET_OK) {
free(key);
key = NULL;
}
if (bn) BN_free(bn);
if (serial_hex) OPENSSL_free(serial_hex);
return key;
}
static int ocsp_cache_lookup(const char *cache_key, int *cert_status) {
int found = 0;
time_t now = time(NULL);
pthread_mutex_lock(&ocsp_cache_mutex);
ocsp_cache_entry_t **pp = &ocsp_cache_head;
while (*pp) {
ocsp_cache_entry_t *entry = *pp;
if (strcmp(entry->cache_key, cache_key) == 0) {
if (entry->expires_at > now) {
*cert_status = entry->cert_status;
found = 1;
dbgprintf("OCSP: Cache hit for key %s, status=%d\n", cache_key, *cert_status);
} else {
/* Remove expired entry */
dbgprintf("OCSP: Removing expired cache entry for key %s\n", cache_key);
*pp = entry->next;
free(entry->cache_key);
free(entry);
ocsp_cache_count--;
}
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&ocsp_cache_mutex);
return found;
}
static void ocsp_cache_store(const char *cache_key, int cert_status, time_t ttl) {
DEFiRet;
pthread_mutex_lock(&ocsp_cache_mutex);
/* Check if already exists and update */
ocsp_cache_entry_t *entry = ocsp_cache_head;
while (entry) {
if (strcmp(entry->cache_key, cache_key) == 0) {
entry->cert_status = cert_status;
entry->expires_at = time(NULL) + ttl;
dbgprintf("OCSP: Cache updated for key %s\n", cache_key);
pthread_mutex_unlock(&ocsp_cache_mutex);
return;
}
entry = entry->next;
}
/* Try to evict expired entries first before removing valid ones */
if (ocsp_cache_count >= OCSP_CACHE_MAX_ENTRIES) {
time_t now = time(NULL);
ocsp_cache_entry_t **pp = &ocsp_cache_head;
ocsp_cache_entry_t *expired_entry = NULL;
/* Find first expired entry */
while (*pp) {
if ((*pp)->expires_at <= now) {
expired_entry = *pp;
*pp = expired_entry->next;
free(expired_entry->cache_key);
free(expired_entry);
ocsp_cache_count--;
break;
}
pp = &(*pp)->next;
}
/* If no expired entries found, evict from tail (true FIFO) */
if (!expired_entry && ocsp_cache_head) {
pp = &ocsp_cache_head;
while ((*pp)->next) {
pp = &(*pp)->next;
}
ocsp_cache_entry_t *to_remove = *pp;
*pp = NULL;
free(to_remove->cache_key);
free(to_remove);
ocsp_cache_count--;
}
}
/* Add new entry */
ocsp_cache_entry_t *new_entry = NULL;
CHKmalloc(new_entry = malloc(sizeof(ocsp_cache_entry_t)));
CHKmalloc(new_entry->cache_key = strdup(cache_key));
new_entry->cert_status = cert_status;
new_entry->expires_at = time(NULL) + ttl;
new_entry->next = ocsp_cache_head;
ocsp_cache_head = new_entry;
ocsp_cache_count++;
dbgprintf("OCSP: Cache stored for key %s, count=%d\n", cache_key, ocsp_cache_count);
finalize_it:
if (iRet == RS_RET_OUT_OF_MEMORY) {
LogError(0, RS_RET_OUT_OF_MEMORY, "OCSP: Failed to allocate cache entry\n");
}
if (iRet != RS_RET_OK) {
if (new_entry) {
free(new_entry->cache_key);
free(new_entry);
}
}
pthread_mutex_unlock(&ocsp_cache_mutex);
}
void ocsp_cache_cleanup(void) {
pthread_mutex_lock(&ocsp_cache_mutex);
ocsp_cache_entry_t *entry = ocsp_cache_head;
while (entry) {
ocsp_cache_entry_t *next = entry->next;
free(entry->cache_key);
free(entry);
entry = next;
}
ocsp_cache_head = NULL;
ocsp_cache_count = 0;
pthread_mutex_unlock(&ocsp_cache_mutex);
}
/*
* CRL is not implemented!
*
* This is just a sanity-check stub, to fail on CRL-only certificates.
* CRL-only certificate means: Certificate CRL DP, but not OCSP/AuthorityInfoAccess information.
*
* Returns:
* 0: if CRL check failed (Error, or Revoked when is_revoked is set)
* 1: if the certificate status is "GOOD"
* 2: if the certificate holds no CRL URL
*/
static int crl_check(X509 *current_cert, int *is_revoked) {
int ret = 0;
STACK_OF(DIST_POINT) * crldp;
STACK_OF(OPENSSL_STRING) * ocsp_uris;
/* Reset revoked status. Needs to be set in case CRL gets implemented. */
if (is_revoked) *is_revoked = 0;
crldp = X509_get_ext_d2i(current_cert, NID_crl_distribution_points, NULL, NULL);
ocsp_uris = X509_get1_ocsp(current_cert);
if (crldp && ocsp_uris == NULL) {
LogError(0, RS_RET_NO_ERRCODE,
"CRL support is not implemented. "
"CRL-only certificates can't be validated!\n");
ret = 0; /* abort verification. CRL-only not supported. */
} else if (!crldp) {
ret = 2;
}
if (ocsp_uris) X509_email_free(ocsp_uris);
if (crldp) sk_DIST_POINT_pop_free(crldp, DIST_POINT_free);
return ret;
}
/*
* Find the issuer certificate, which is mandatory to generate
* a OCSP request.
*
* Query first locally configured and trusted certificates (e.g CAs).
*
* Last resort: Use the certificate chain of the peer, to get hold of
* untrusted certificates (e.g. intermediate certificates).
*
* The resulting issuer certificate might be an untrusted certificate,
* which should be used to generate OCSP requests.
*/
static X509 *ocsp_find_issuer(X509 *target_cert,
const char *cert_name,
SSL_CTX *ctx,
STACK_OF(X509) * untrusted_peer_certs) {
X509 *issuer = NULL;
X509_STORE *store = SSL_CTX_get_cert_store(ctx);
STACK_OF(X509_OBJECT) * objs;
/* find issuer among local trusted issuers */
if (store != NULL) {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
objs = X509_STORE_get0_objects(store);
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
X509 *cert = X509_OBJECT_get0_X509(sk_X509_OBJECT_value(objs, i));
if (cert && X509_check_issued(cert, target_cert) == X509_V_OK) {
issuer = cert;
break;
}
}
#else
/* OpenSSL 1.0.2 compatibility: direct access to store fields */
objs = store->objs;
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
X509_OBJECT *x509obj = sk_X509_OBJECT_value(objs, i);
X509 *cert = x509obj->type == X509_LU_X509 ? x509obj->data.x509 : NULL;
if (cert && X509_check_issued(cert, target_cert) == X509_V_OK) {
issuer = cert;
break;
}
}
#endif
}
if (!issuer) {
/* Look for intermediate certificates in the remote cert-chain */
for (int i = 0; i < sk_X509_num(untrusted_peer_certs); i++) {
X509 *cert = sk_X509_value(untrusted_peer_certs, i);
if (X509_check_issued(cert, target_cert) == X509_V_OK) {
issuer = cert;
break;
}
}
}
/* No issuer, no revocation check possible */
if (!issuer) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Could not find any issuer for: \"%s\"\n", cert_name);
}
return issuer;
}
static const char *ocsp_get_response_status_err(int c) {
switch (c) {
case OCSP_RESPONSE_STATUS_SUCCESSFUL:
return "Successful";
case OCSP_RESPONSE_STATUS_MALFORMEDREQUEST:
return "Malformed request";
case OCSP_RESPONSE_STATUS_INTERNALERROR:
return "Internal error in OCSP responder";
case OCSP_RESPONSE_STATUS_TRYLATER:
return "Try again later";
case OCSP_RESPONSE_STATUS_SIGREQUIRED:
return "Must sign the request";
case OCSP_RESPONSE_STATUS_UNAUTHORIZED:
return "Request unauthorized";
default:
return "Unknown Response Status";
}
}
/*
* Returns 1 if the cert status is "GOOD"
* Stores result in cache if successful
*/
static int ocsp_check_validate_response_and_cert(OCSP_RESPONSE *rsp,
OCSP_REQUEST *req,
STACK_OF(X509) * untrusted_peer_certs,
OCSP_CERTID *id,
SSL_CTX *ctx,
const char *cert_name,
X509 *cert,
X509 *issuer,
int *is_revoked) {
int s, ret = 0;
long leeway_sec = 300; /* 5 Minutes */
long maxage = -1; /* No maximum age for ThisUpdate response */
int status, reason;
ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd;
OCSP_BASICRESP *bs = NULL;
X509_STORE *store = SSL_CTX_get_cert_store(ctx);
bs = OCSP_response_get1_basic(rsp);
if (bs == NULL) {
LogError(0, RS_RET_NO_ERRCODE, "Failed to decode the basic OCSP response.\n");
goto err;
}
s = OCSP_check_nonce(req, bs);
if (s == -1) {
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"Warning: no Nonce in OCSP response from peer. "
"(No replay attack protection available)\n");
} else if (s == 0) {
LogError(0, RS_RET_NO_ERRCODE, "Nonce mismatch in OCSP response.\n");
goto err;
}
s = OCSP_basic_verify(bs, untrusted_peer_certs, store, 0);
if (s <= 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP response verification failed.\n");
goto err;
}
if (!OCSP_resp_find_status(bs, id, &status, &reason, &rev, &thisupd, &nextupd)) {
LogError(0, RS_RET_NO_ERRCODE, "Failed to get OCSP response status for: %s\n", cert_name);
goto err;
}
s = OCSP_check_validity(thisupd, nextupd, leeway_sec, maxage);
if (s == 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP response is not valid (either expired or not yet valid).\n");
goto err;
}
/* Check certificate status */
switch (status) {
case V_OCSP_CERTSTATUS_GOOD:
dbgprintf("OCSP: Certificate status is GOOD for: %s\n", cert_name);
ret = 1;
break;
case V_OCSP_CERTSTATUS_REVOKED:
LogMsg(0, RS_RET_CERT_REVOKED, LOG_ERR, "OCSP: Certificate status is REVOKED for: %s (reason: %s)\n",
cert_name, OCSP_crl_reason_str(reason));
if (is_revoked) *is_revoked = 1;
ret = 0;
break;
case V_OCSP_CERTSTATUS_UNKNOWN:
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Certificate status is UNKNOWN for: %s\n", cert_name);
ret = 0;
break;
default:
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Unknown certificate status for: %s\n", cert_name);
ret = 0;
break;
}
/* Store result in cache */
char *cache_key = ocsp_make_cache_key(cert, issuer);
if (cache_key) {
time_t cache_ttl = OCSP_CACHE_DEFAULT_TTL;
/* Use nextUpdate if available for more accurate cache expiry */
if (nextupd) {
/* Use ASN1_TIME_diff() which properly handles UTC times without timezone issues */
int pday, psec;
if (ASN1_TIME_diff(&pday, &psec, NULL, nextupd) == 1) {
/* nextupd is in the future */
time_t seconds_until_expiry = (pday * 86400) + psec;
if (seconds_until_expiry > 0) {
cache_ttl = seconds_until_expiry;
}
} else {
dbgprintf("OCSP: ASN1_TIME_diff() failed, using default TTL\n");
}
}
ocsp_cache_store(cache_key, status, cache_ttl);
free(cache_key);
}
err:
if (bs) OCSP_BASICRESP_free(bs);
return ret;
}
static BIO *ocsp_connect(const char *host, const char *port, const char *device) {
BIO *bio = NULL;
int sock = -1;
struct addrinfo hints, *res = NULL, *rp;
int s;
int flags;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
/* TODO: DNS resolution is blocking with no timeout. A malicious certificate
* with OCSP responder URLs pointing to slow/unresponsive DNS names can cause
* the TLS handshake to block indefinitely. This is called from the TLS verify
* callback. Consider using async DNS resolution or making OCSP optional.
*/
s = getaddrinfo(host, port, &hints, &res);
if (s != 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: getaddrinfo failed for %s:%s: %s\n", host, port, gai_strerror(s));
goto err;
}
for (rp = res; rp != NULL; rp = rp->ai_next) {
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sock == -1) continue;
if (device && *device) {
#ifdef SO_BINDTODEVICE
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)) < 0) {
LogError(errno, RS_RET_NO_ERRCODE, "OCSP: Failed to bind socket to device %s\n", device);
close(sock);
sock = -1;
continue;
}
#else
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"OCSP: SO_BINDTODEVICE not supported, ignoring device parameter\n");
#endif
}
/* Set socket to non-blocking for timeout support */
flags = fcntl(sock, F_GETFL, 0);
if (flags != -1) {
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
}
if (connect(sock, rp->ai_addr, rp->ai_addrlen) == 0) {
break; /* Success */
}
if (errno == EINPROGRESS) {
fd_set wfds;
struct timeval tv;
FD_ZERO(&wfds);
FD_SET(sock, &wfds);
tv.tv_sec = OCSP_TIMEOUT;
tv.tv_usec = 0;
s = select(sock + 1, NULL, &wfds, NULL, &tv);
if (s > 0) {
int error = 0;
socklen_t len = sizeof(error);
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
break; /* Success */
}
}
}
close(sock);
sock = -1;
}
if (sock == -1 || rp == NULL) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to connect to %s:%s\n", host, port);
goto err;
}
/* Set socket back to blocking mode with read/write timeout */
flags = fcntl(sock, F_GETFL, 0);
if (flags != -1) {
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
}
/* Set socket read and write timeouts to prevent indefinite blocking during OCSP I/O */
struct timeval timeout;
timeout.tv_sec = OCSP_TIMEOUT;
timeout.tv_usec = 0;
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"OCSP: Failed to set socket read timeout (errno %d), continuing without timeout\n", errno);
}
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) {
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"OCSP: Failed to set socket write timeout (errno %d), continuing without timeout\n", errno);
}
bio = BIO_new_socket(sock, BIO_CLOSE);
if (!bio) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to create BIO\n");
close(sock);
goto err;
}
err:
if (res) freeaddrinfo(res);
return bio;
}
static OCSP_RESPONSE *ocsp_send_and_receive(BIO *bio, const char *host, const char *path, OCSP_REQUEST *req) {
OCSP_RESPONSE *rsp = NULL;
char *req_data = NULL;
int req_len;
int rv;
/* Encode OCSP request */
req_len = i2d_OCSP_REQUEST(req, (unsigned char **)&req_data);
if (req_len <= 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to encode OCSP request\n");
goto err;
}
/* Send HTTP POST request */
rv = BIO_printf(bio,
"POST %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Content-Type: application/ocsp-request\r\n"
"Content-Length: %d\r\n"
"\r\n",
path, host, req_len);
if (rv <= 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to send HTTP header\n");
goto err;
}
/* Send request data */
if (BIO_write(bio, req_data, req_len) != req_len) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to send OCSP request data\n");
goto err;
}
if (BIO_flush(bio) != 1) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to flush BIO\n");
goto err;
}
/* Read HTTP response - skip headers */
char buf[1024];
int in_headers = 1;
long content_length = -1;
const long MAX_OCSP_RESPONSE_SIZE = 1024 * 1024; /* 1MB limit */
while (in_headers) {
rv = BIO_gets(bio, buf, sizeof(buf));
if (rv <= 0) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to read HTTP response headers\n");
goto err;
}
/* Parse Content-Length header */
if (strncasecmp(buf, "Content-Length:", 15) == 0) {
content_length = atol(buf + 15);
if (content_length > MAX_OCSP_RESPONSE_SIZE) {
LogError(0, RS_RET_NO_ERRCODE,
"OCSP: Response Content-Length (%ld) exceeds maximum allowed size (%ld)\n", content_length,
MAX_OCSP_RESPONSE_SIZE);
goto err;
}
}
/* Empty line marks end of headers */
if (strcmp(buf, "\r\n") == 0 || strcmp(buf, "\n") == 0) {
in_headers = 0;
}
}
/* Read OCSP response with size limit */
if (content_length > 0 && content_length <= MAX_OCSP_RESPONSE_SIZE) {
/* Size is within limits, proceed with reading */
rsp = d2i_OCSP_RESPONSE_bio(bio, NULL);
} else if (content_length == -1) {
/* No Content-Length header - read with caution */
LogMsg(0, RS_RET_NO_ERRCODE, LOG_WARNING,
"OCSP: No Content-Length header in response, reading without size validation\n");
rsp = d2i_OCSP_RESPONSE_bio(bio, NULL);
}
if (!rsp) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to parse OCSP response\n");
goto err;
}
err:
if (req_data) OPENSSL_free(req_data);
return rsp;
}
static int ocsp_is_supported_protocol(const char *url) {
return (strncmp(url, "http://", 7) == 0);
}
static int ocsp_request_per_responder(const char *url,
X509 *cert,
X509 *issuer,
STACK_OF(X509) * untrusted_peer_certs,
SSL_CTX *ctx,
const char *device,
int *is_revoked) {
int ret = 0;
char *host = NULL, *port = NULL, *path = NULL;
int use_ssl = 0;
BIO *bio = NULL;
OCSP_REQUEST *req = NULL;
OCSP_RESPONSE *rsp = NULL;
OCSP_CERTID *id = NULL;
char cert_name[256];
X509_NAME_oneline(X509_get_subject_name(cert), cert_name, sizeof(cert_name));
/* Parse URL */
if (!OCSP_parse_url((char *)url, &host, &port, &path, &use_ssl)) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to parse URL: %s\n", url);
goto err;
}
/* Only HTTP is supported */
if (use_ssl) {
dbgprintf("OCSP: HTTPS not supported, skipping %s\n", url);
goto err;
}
dbgprintf("OCSP: Connecting to %s:%s%s\n", host, port, path);
/* Connect to OCSP responder */
bio = ocsp_connect(host, port, device);
if (!bio) {
goto err;
}
/* Create OCSP request */
req = OCSP_REQUEST_new();
if (!req) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to create OCSP request\n");
goto err;
}
id = OCSP_cert_to_id(NULL, cert, issuer);
if (!id) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to create OCSP certificate ID\n");
goto err;
}
if (!OCSP_request_add0_id(req, id)) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Failed to add certificate ID to OCSP request\n");
OCSP_CERTID_free(id);
goto err;
}
/* Add nonce extension for replay attack protection */
OCSP_request_add1_nonce(req, NULL, -1);
/* Send request and receive response */
rsp = ocsp_send_and_receive(bio, host, path, req);
if (!rsp) {
goto err;
}
/* Check response status */
int response_status = OCSP_response_status(rsp);
if (response_status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
LogError(0, RS_RET_NO_ERRCODE, "OCSP: Response status: %s\n", ocsp_get_response_status_err(response_status));
goto err;
}
/* Validate response and check certificate status */
ret = ocsp_check_validate_response_and_cert(rsp, req, untrusted_peer_certs, id, ctx, cert_name, cert, issuer,
is_revoked);
err:
if (rsp) OCSP_RESPONSE_free(rsp);
if (req) OCSP_REQUEST_free(req);
if (bio) BIO_free_all(bio);
if (host) OPENSSL_free(host);
if (port) OPENSSL_free(port);
if (path) OPENSSL_free(path);
return ret;
}
static int ocsp_check(
X509 *current_cert, STACK_OF(X509) * untrusted_peer_certs, SSL_CTX *ctx, const char *device, int *is_revoked) {
int ret = 0;
char cert_name[256];
X509 *issuer = NULL;
STACK_OF(OPENSSL_STRING) *ocsp_responders = NULL;
const char *url = NULL;
int at_least_one_responder = 0;
char *cache_key = NULL;
int cached_status;
X509_NAME_oneline(X509_get_subject_name(current_cert), cert_name, sizeof(cert_name));
/* Reset revoked status */
if (is_revoked) *is_revoked = 0;
/*
* 1. Lookup the issuer cert of the current certificate, required to marshal a OCSP request.
*/
if (!(issuer = ocsp_find_issuer(current_cert, cert_name, ctx, untrusted_peer_certs))) goto err;
/*
* 2. Check cache first to avoid network I/O
*/
cache_key = ocsp_make_cache_key(current_cert, issuer);
if (cache_key && ocsp_cache_lookup(cache_key, &cached_status)) {
if (cached_status == V_OCSP_CERTSTATUS_GOOD) {
dbgprintf("OCSP: Cache indicates certificate is GOOD for: %s\n", cert_name);
ret = 1;
goto err;
} else if (cached_status == V_OCSP_CERTSTATUS_REVOKED) {
LogMsg(0, RS_RET_CERT_REVOKED, LOG_ERR, "OCSP: Cached status shows certificate is REVOKED for: %s\n",
cert_name);
if (is_revoked) *is_revoked = 1;
goto err;
}
/* UNKNOWN status: fall through to network check */
}
/*
* 3. Sanity check prior generating OCSP request.
*
* - No OCSP URL, no OCSP revocation verification required, skip the current cert.
* - Only HTTP protocol is supported. If no OCSP responder with a supported protocol
* is available, this MUST fail the OCSP verification and abort the TLS handshake.
*/
ocsp_responders = X509_get1_ocsp(current_cert);
if (ocsp_responders == NULL) {
ret = 2;
goto err; /* continue with cert path validation */
}
for (int i = 0; i < sk_OPENSSL_STRING_num(ocsp_responders); i++) {
url = sk_OPENSSL_STRING_value(ocsp_responders, i);
if (ocsp_is_supported_protocol(url)) {
at_least_one_responder = 1;
break;
}
}
if (!at_least_one_responder) {
LogError(0, RS_RET_NO_ERRCODE,
"None of the OCSP responders are supported by "
"this implementation:\n");
for (int i = 0; i < sk_OPENSSL_STRING_num(ocsp_responders); i++) {
url = sk_OPENSSL_STRING_value(ocsp_responders, i);
LogError(0, RS_RET_NO_ERRCODE, "\t%s\n", url);
}
goto err;
}
/*
* Try all supported OCSP responders one by one.
*
* One successful OCSP status response is enough. Not all responders
* need to be queried.
*/
for (int i = 0; i < sk_OPENSSL_STRING_num(ocsp_responders); i++) {
url = sk_OPENSSL_STRING_value(ocsp_responders, i);
if (ocsp_request_per_responder(url, current_cert, issuer, untrusted_peer_certs, ctx, device, is_revoked)) {
ret = 1;
goto err;
}
/* When revoked don't try other sources/OCSP responders */
if (is_revoked && *is_revoked) goto err;
/* If the status is unknown, try the next available responder */
}
err:
if (ocsp_responders) X509_email_free(ocsp_responders);
if (cache_key) free(cache_key);
return ret;
}
#endif /* !ENABLE_WOLFSSL */
/*--------------------------------------End OCSP Support ------------------------------------------*/
/* Verify Callback for X509 Certificate validation. Force visibility as this function is not called anywhere but
* only used as callback!
*/
int net_ossl_verify_callback(int status, X509_STORE_CTX *store) {
char szdbgdata1[256];
char szdbgdata2[256];
dbgprintf("verify_callback: status %d\n", status);
if (status == 0) {
/* Retrieve all needed pointers */
X509 *cert = X509_STORE_CTX_get_current_cert(store);
int depth = X509_STORE_CTX_get_error_depth(store);
SSL *ssl = X509_STORE_CTX_get_ex_data(store, SSL_get_ex_data_X509_STORE_CTX_idx());
int err = X509_STORE_CTX_get_error(store);
int iVerifyMode = SSL_get_verify_mode(ssl);
nsd_t *pNsdTcp = (nsd_t *)SSL_get_ex_data(ssl, 0);
PermitExpiredCerts *pPermitExpiredCerts = (PermitExpiredCerts *)SSL_get_ex_data(ssl, 1);
dbgprintf("verify_callback: Certificate validation failed, Mode (%d)!\n", iVerifyMode);
X509_NAME_oneline(X509_get_issuer_name(cert), szdbgdata1, sizeof(szdbgdata1));
X509_NAME_oneline(RSYSLOG_X509_NAME_oneline(cert), szdbgdata2, sizeof(szdbgdata2));
uchar *fromHost = NULL;
if (pNsdTcp != NULL) {
nsd_ptcp.GetRemoteHName(pNsdTcp, &fromHost);
}
if (iVerifyMode != SSL_VERIFY_NONE) {
/* Handle expired Certificates **/
if (err == X509_V_OK || err == X509_V_ERR_CERT_HAS_EXPIRED) {
if (pPermitExpiredCerts != NULL && *pPermitExpiredCerts == OSSL_EXPIRED_PERMIT) {
dbgprintf(
"verify_callback: EXPIRED cert but PERMITTED at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s\n",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err));
/* Set Status to OK*/
status = 1;
} else if (pPermitExpiredCerts != NULL && *pPermitExpiredCerts == OSSL_EXPIRED_WARN) {
LogMsg(0, RS_RET_CERT_EXPIRED, LOG_WARNING,
"Certificate EXPIRED warning at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s peer '%s'",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err), fromHost);
/* Set Status to OK*/
status = 1;
} else /* also default - if (pPermitExpiredCerts == OSSL_EXPIRED_DENY)*/ {
LogMsg(0, RS_RET_CERT_EXPIRED, LOG_ERR,
"Certificate EXPIRED at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s\n\t"
"not permitted to talk to peer '%s', certificate invalid: "
"certificate expired",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err), fromHost);
}
} else if (err == X509_V_ERR_CERT_REVOKED) {
LogMsg(0, RS_RET_CERT_REVOKED, LOG_ERR,
"Certificate REVOKED at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s\n\t"
"not permitted to talk to peer '%s', certificate invalid: "
"certificate revoked",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err), fromHost);
} else {
/* all other error codes cause failure */
LogMsg(0, RS_RET_NO_ERRCODE, LOG_ERR,
"Certificate error at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s - peer '%s'",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err), fromHost);
}
} else {
/* do not verify certs in ANON mode, just log into debug */
dbgprintf(
"verify_callback: Certificate validation DISABLED but Error at depth: %d \n\t"
"issuer = %s\n\t"
"subject = %s\n\t"
"err %d:%s\n",
depth, szdbgdata1, szdbgdata2, err, X509_verify_cert_error_string(err));
/* Set Status to OK*/
status = 1;
}
free(fromHost);
}
if (status == 0) return 0; /* Verification failed */
#ifndef ENABLE_WOLFSSL
/* OCSP revocation checking is not available in WolfSSL builds */
/*
* Certificate revocation checks (only if enabled via StreamDriver.TlsRevocationCheck)
*/
X509 *cert = X509_STORE_CTX_get_current_cert(store);
SSL *ssl = X509_STORE_CTX_get_ex_data(store, SSL_get_ex_data_X509_STORE_CTX_idx());
/* Check if revocation checking is enabled
* Note: Using index 3 to avoid collision with imdtls which uses index 2
* Index allocation: 0=pTcp, 1=permitExpiredCerts, 2=imdtls instance, 3=revocationCheck
*/
int *pTlsRevocationCheck = (int *)SSL_get_ex_data(ssl, 3);
int tlsRevocationCheck = (pTlsRevocationCheck != NULL) ? *pTlsRevocationCheck : 0;
if (tlsRevocationCheck == 0) {
/* Revocation checking is disabled, skip OCSP/CRL checks */
return status;
}
STACK_OF(X509) *untrusted_peer_certs = X509_STORE_CTX_get1_chain(store);
SSL_CTX *ctx = SSL_get_SSL_CTX(ssl);
int ret, is_revoked = 0;
const char *device = NULL;
/* Try to get device name from nsd object if available */
/* Note: device binding is not currently supported in the refactored code */
/* This is left here for future enhancement */
/* 1. OCSP with caching and non-blocking I/O */
/* Note: OCSP checks use non-blocking sockets with OCSP_TIMEOUT (5 sec) per responder.
* Results are cached to minimize network I/O on subsequent TLS handshakes.
* See: https://github.com/rsyslog/rsyslog/issues/6469
*/
ret = ocsp_check(cert, untrusted_peer_certs, ctx, device, &is_revoked);
if (ret == 1) {
/* Status is OK */
status = 1;
goto done;
} else if (ret == 2) {
/* No OCSP URL, give CRL a chance */
status = 1;
} else if (is_revoked) {
/* Cert is revoked, fail verification */
status = 0;
goto done;
} else {
/* If OCSP failed, but cert was not revoked, then the Status might be still OK.
* Try alternative sources (e.g. CRL), in compliance with RFC 6960, Chapter 2.2, Page 7-8.
*/
status = 0;
}
/* 2. CRL */
/* CRL support is not implemented.
* This stub will fail if the cert holds a CRL Distribution Point. */
ret = crl_check(cert, &is_revoked);
if (ret == 1) {
/* Status is OK */
status = 1;
} else if (ret == 2) {
/* No CRL, keep the existing status */
;
} else {
/* Cert is revoked, or check failed -> fail verification */
status = 0;
}
done:
if (untrusted_peer_certs) sk_X509_pop_free(untrusted_peer_certs, X509_free);
#endif /* !ENABLE_WOLFSSL */
return status;
}
#ifndef ENABLE_WOLFSSL
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
static long RSYSLOG_BIO_debug_callback_ex(BIO *bio,
int cmd,
const char __attribute__((unused)) * argp,
size_t __attribute__((unused)) len,
int argi,
long __attribute__((unused)) argl,
int ret,
size_t __attribute__((unused)) * processed)
#else
static long RSYSLOG_BIO_debug_callback(
BIO *bio, int cmd, const char __attribute__((unused)) * argp, int argi, long __attribute__((unused)) argl, long ret)
#endif
{
long ret2 = ret;
long r = 1;
if (BIO_CB_RETURN & cmd) r = ret;
dbgprintf("openssl debugmsg: BIO[%p]: ", (void *)bio);
switch (cmd) {
case BIO_CB_FREE:
dbgprintf("Free - %s\n", RSYSLOG_BIO_method_name(bio));
break;
/* Disabled due API changes for OpenSSL 1.1.0+ */
#if OPENSSL_VERSION_NUMBER < 0x10100000L
case BIO_CB_READ:
if (bio->method->type & BIO_TYPE_DESCRIPTOR)
dbgprintf("read(%d,%lu) - %s fd=%d\n", RSYSLOG_BIO_number_read(bio), (unsigned long)argi,
RSYSLOG_BIO_method_name(bio), RSYSLOG_BIO_number_read(bio));
else
dbgprintf("read(%d,%lu) - %s\n", RSYSLOG_BIO_number_read(bio), (unsigned long)argi,
RSYSLOG_BIO_method_name(bio));
break;
case BIO_CB_WRITE:
if (bio->method->type & BIO_TYPE_DESCRIPTOR)
dbgprintf("write(%d,%lu) - %s fd=%d\n", RSYSLOG_BIO_number_written(bio), (unsigned long)argi,
RSYSLOG_BIO_method_name(bio), RSYSLOG_BIO_number_written(bio));
else
dbgprintf("write(%d,%lu) - %s\n", RSYSLOG_BIO_number_written(bio), (unsigned long)argi,
RSYSLOG_BIO_method_name(bio));
break;
#else
case BIO_CB_READ:
dbgprintf("read %s\n", RSYSLOG_BIO_method_name(bio));
break;
case BIO_CB_WRITE:
dbgprintf("write %s\n", RSYSLOG_BIO_method_name(bio));
break;
#endif
case BIO_CB_PUTS:
dbgprintf("puts() - %s\n", RSYSLOG_BIO_method_name(bio));
break;
case BIO_CB_GETS:
dbgprintf("gets(%lu) - %s\n", (unsigned long)argi, RSYSLOG_BIO_method_name(bio));
break;
case BIO_CB_CTRL:
dbgprintf("ctrl(%lu) - %s\n", (unsigned long)argi, RSYSLOG_BIO_method_name(bio));
break;
case BIO_CB_RETURN | BIO_CB_READ:
dbgprintf("read return %ld\n", ret2);
break;
case BIO_CB_RETURN | BIO_CB_WRITE:
dbgprintf("write return %ld\n", ret2);
break;
case BIO_CB_RETURN | BIO_CB_GETS:
dbgprintf("gets return %ld\n", ret2);
break;
case BIO_CB_RETURN | BIO_CB_PUTS:
dbgprintf("puts return %ld\n", ret2);
break;
case BIO_CB_RETURN | BIO_CB_CTRL:
dbgprintf("ctrl return %ld\n", ret2);
break;
default:
dbgprintf("bio callback - unknown type (%d)\n", cmd);
break;
}
return (r);
}
#endif /* !ENABLE_WOLFSSL */
#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(ENABLE_WOLFSSL)
// Requires at least OpenSSL v1.1.1
PRAGMA_DIAGNOSTIC_PUSH
PRAGMA_IGNORE_Wunused_parameter static int net_ossl_generate_cookie(SSL *ssl,
unsigned char *cookie,
unsigned int *cookie_len) {
unsigned char result[EVP_MAX_MD_SIZE];
unsigned int resultlength;
unsigned char *sslHello;
unsigned int length;
sslHello = (unsigned char *)"rsyslog";
length = strlen((char *)sslHello);
// Generate the cookie using SHA256 hash
if (!EVP_Digest(sslHello, length, result, &resultlength, EVP_sha256(), NULL)) {
return 0;
}
memcpy(cookie, result, resultlength);
*cookie_len = resultlength;
dbgprintf("net_ossl_generate_cookie: generate cookie SUCCESS\n");
return 1;
}
PRAGMA_DIAGNOSTIC_POP
static int net_ossl_verify_cookie(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len) {
unsigned char cookie_gen[EVP_MAX_MD_SIZE];
unsigned int cookie_gen_len;
// Generate a cookie using the same method as in net_ossl_generate_cookie
if (!net_ossl_generate_cookie(ssl, cookie_gen, &cookie_gen_len)) {
dbgprintf("net_ossl_verify_cookie: generate cookie FAILED\n");
return 0;
}
// Check if the generated cookie matches the cookie received
if (cookie_len == cookie_gen_len && memcmp(cookie, cookie_gen, cookie_len) == 0) {
dbgprintf("net_ossl_verify_cookie: compare cookie SUCCESS\n");
return 1;
}
dbgprintf("net_ossl_verify_cookie: compare cookie FAILED\n");
return 0;
}
static rsRetVal net_ossl_init_engine(__attribute__((unused)) net_ossl_t *pThis) {
// OpenSSL Engine Support feature relies on an outdated version of OpenSSL and is
// strictly experimental. No support or guarantees are provided. Use at your own risk.
DEFiRet;
#ifndef OPENSSL_NO_ENGINE
const char *engine_id = NULL;
const char *engine_name = NULL;
PRAGMA_DIAGNOSTIC_PUSH
PRAGMA_IGNORE_Wdeprecated_declarations
// Get the default RSA engine
ENGINE *default_engine = ENGINE_get_default_RSA();
if (default_engine) {
engine_id = ENGINE_get_id(default_engine);
engine_name = ENGINE_get_name(default_engine);
DBGPRINTF("net_ossl_init_engine: Default RSA Engine: ID = %s, Name = %s\n", engine_id, engine_name);
// Free the engine reference when done
ENGINE_free(default_engine);
} else {
DBGPRINTF("net_ossl_init_engine: No default RSA Engine set.\n");
}
/* Setting specific Engine */
if (runConf != NULL && glbl.GetDfltOpensslEngine(runConf) != NULL) {
default_engine = ENGINE_by_id((char *)glbl.GetDfltOpensslEngine(runConf));
if (default_engine && ENGINE_init(default_engine)) {
/* engine initialised */
ENGINE_set_default_DSA(default_engine);
ENGINE_set_default_ciphers(default_engine);
/* Switch to Engine */
DBGPRINTF("net_ossl_init_engine: Changed default Engine to %s\n", glbl.GetDfltOpensslEngine(runConf));
/* Release the functional reference from ENGINE_init() */
ENGINE_finish(default_engine);
} else {
LogError(0, RS_RET_VALUE_NOT_SUPPORTED,
"error: ENGINE_init failed to load Engine '%s'"
"ossl netstream driver",
glbl.GetDfltOpensslEngine(runConf));
net_ossl_lastOpenSSLErrorMsg(NULL, 0, NULL, LOG_ERR, "net_ossl_init_engine", "ENGINE_init");
}
// Free the engine reference when done
ENGINE_free(default_engine);
} else {
DBGPRINTF("net_ossl_init_engine: use openssl default Engine");
}
PRAGMA_DIAGNOSTIC_POP
#else
DBGPRINTF("net_ossl_init_engine: OpenSSL compiled without ENGINE support - ENGINE support disabled\n");
#endif /* OPENSSL_NO_ENGINE */
RETiRet;
}
static rsRetVal net_ossl_ctx_init_cookie(net_ossl_t *pThis) {
DEFiRet;
// Set our cookie generation and verification callbacks
SSL_CTX_set_options(pThis->ctx, SSL_OP_COOKIE_EXCHANGE);
SSL_CTX_set_cookie_generate_cb(pThis->ctx, net_ossl_generate_cookie);
SSL_CTX_set_cookie_verify_cb(pThis->ctx, net_ossl_verify_cookie);
RETiRet;
}
#endif // OPENSSL_VERSION_NUMBER >= 0x10100000L
/* ------------------------------ end OpenSSL helpers ------------------------------------------*/
/* ------------------------------ OpenSSL Callback set helpers ---------------------------------*/
void net_ossl_set_ssl_verify_callback(SSL *pSsl, int flags) {
/* Enable certificate valid checking */
SSL_set_verify(pSsl, flags, net_ossl_verify_callback);
}
void net_ossl_set_ctx_verify_callback(SSL_CTX *pCtx, int flags) {
/* Enable certificate valid checking */
SSL_CTX_set_verify(pCtx, flags, net_ossl_verify_callback);
}
void net_ossl_set_bio_callback(BIO *conn) {
#ifndef ENABLE_WOLFSSL
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
BIO_set_callback_ex(conn, RSYSLOG_BIO_debug_callback_ex);
#else
BIO_set_callback(conn, RSYSLOG_BIO_debug_callback);
#endif // OPENSSL_VERSION_NUMBER >= 0x10100000L
#else
(void)conn;
#endif
}
/* ------------------------------ End OpenSSL Callback set helpers -----------------------------*/
/* Standard-Constructor */
BEGINobjConstruct(net_ossl) /* be sure to specify the object type also in END macro! */
DBGPRINTF("net_ossl_construct: [%p]\n", pThis);
pThis->bReportAuthErr = 1;
pThis->bSANpriority = 0;
#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(ENABLE_WOLFSSL)
CHKiRet(net_ossl_init_engine(pThis));
finalize_it:
#endif
ENDobjConstruct(net_ossl)
/* destructor for the net_ossl object */
BEGINobjDestruct(net_ossl) /* be sure to specify the object type also in END and CODESTART macros! */
CODESTARTobjDestruct(net_ossl);
DBGPRINTF("net_ossl_destruct: [%p]\n", pThis);
/* Free SSL obj also if we do not have a session - or are NOT in TLS mode! */
if (pThis->ssl != NULL) {
DBGPRINTF("net_ossl_destruct: [%p] FREE pThis->ssl \n", pThis);
SSL_free(pThis->ssl);
pThis->ssl = NULL;
}
if (pThis->ctx != NULL && !pThis->ctx_is_copy) {
SSL_CTX_free(pThis->ctx);
pThis->ctx = NULL;
}
free((void *)pThis->pszCAFile);
free((void *)pThis->pszCRLFile);
free((void *)pThis->pszKeyFile);
free((void *)pThis->pszCertFile);
free((void *)pThis->pszExtraCAFiles);
ENDobjDestruct(net_ossl)
/* queryInterface function */
BEGINobjQueryInterface(net_ossl)
CODESTARTobjQueryInterface(net_ossl);
DBGPRINTF("netosslQueryInterface\n");
if (pIf->ifVersion != net_osslCURR_IF_VERSION) { /* check for current version, increment on each change */
ABORT_FINALIZE(RS_RET_INTERFACE_NOT_SUPPORTED);
}
pIf->Construct = (rsRetVal(*)(net_ossl_t **))net_osslConstruct;
pIf->Destruct = (rsRetVal(*)(net_ossl_t **))net_osslDestruct;
pIf->osslCtxInit = net_ossl_osslCtxInit;
pIf->osslChkpeername = net_ossl_chkpeername;
pIf->osslPeerfingerprint = net_ossl_peerfingerprint;
pIf->osslGetpeercert = net_ossl_getpeercert;
pIf->osslChkpeercertvalidity = net_ossl_chkpeercertvalidity;
#if OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(LIBRESSL_VERSION_NUMBER) && !defined(ENABLE_WOLFSSL)
pIf->osslApplyTlscgfcmd = net_ossl_apply_tlscgfcmd;
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
pIf->osslSetBioCallback = net_ossl_set_bio_callback;
pIf->osslSetCtxVerifyCallback = net_ossl_set_ctx_verify_callback;
pIf->osslSetSslVerifyCallback = net_ossl_set_ssl_verify_callback;
pIf->osslLastOpenSSLErrorMsg = net_ossl_lastOpenSSLErrorMsg;
#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(ENABLE_WOLFSSL)
pIf->osslCtxInitCookie = net_ossl_ctx_init_cookie;
pIf->osslInitEngine = net_ossl_init_engine;
#endif
finalize_it:
ENDobjQueryInterface(net_ossl)
/* exit our class
*/
BEGINObjClassExit(net_ossl, OBJ_IS_CORE_MODULE) /* CHANGE class also in END MACRO! */
CODESTARTObjClassExit(net_ossl);
DBGPRINTF("netosslClassExit\n");
/* release objects we no longer need */
objRelease(nsd_ptcp, LM_NSD_PTCP_FILENAME);
objRelease(net, LM_NET_FILENAME);
objRelease(glbl, CORE_COMPONENT);
/* shut down OpenSSL */
osslGlblExit();
ENDObjClassExit(net_ossl)
/* Initialize the net_ossl class. Must be called as the very first method
* before anything else is called inside this class.
*/
BEGINObjClassInit(net_ossl, 1, OBJ_IS_CORE_MODULE) /* class, version */
DBGPRINTF("net_osslClassInit\n");
// request objects we use
CHKiRet(objUse(glbl, CORE_COMPONENT));
CHKiRet(objUse(net, LM_NET_FILENAME));
CHKiRet(objUse(nsd_ptcp, LM_NSD_PTCP_FILENAME));
// Do global TLS init stuff
osslGlblInit();
ENDObjClassInit(net_ossl)
/* --------------- here now comes the plumbing that makes as a library module --------------- */
/* vi:set ai:
*/