From b763cfd95f149accf91875a9a5f3fa732c01f399 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 30 Jul 2026 14:49:14 +0000 Subject: [PATCH 01/26] clients/upsclient.{c,h}: add support for connection-specific SSL context [#3439] ...as an alternative to the global SSL context, which is not suitable for multiple connections with different SSL parameters (certificates, trusted CA realms, etc.) in the same process. Signed-off-by: Jim Klimov --- clients/upsclient.c | 37 ++++++++++++++++++++++++++++++++++++- clients/upsclient.h | 20 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 5bc40ff8d4..7dee1b0ab2 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -213,6 +213,10 @@ static int upscli_default_connect_timeout_initialized = 0; #endif #ifdef WITH_OPENSSL +/* Default SSL context (for legacy compatibility and apps that only + * make one connection per process); see ups->ssl_ctx if your app + * wants to connect to different NUT servers under separate management + * (CA realms, client certificates, etc.) simultaneously. */ static SSL_CTX *ssl_ctx = NULL; #endif /* WITH_OPENSSL */ @@ -1665,6 +1669,28 @@ void upscli_free_host_cert_list(void) #endif /* ! SSL */ } +void *upscli_set_ssl_context(UPSCONN_t *ups, void *ssl_ctx_in) +{ + void *previous = NULL; + + if (!ups) { + return NULL; + } + + previous = ups->ssl_ctx; + ups->ssl_ctx = (upscli_ssl_context_config_t *)ssl_ctx_in; + return previous; +} + +void *upscli_get_ssl_context(UPSCONN_t *ups) +{ + if (!ups) { + return NULL; + } + + return ups->ssl_ctx; +} + int upscli_cleanup(void) { #ifdef WITH_OPENSSL @@ -2207,12 +2233,15 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) # ifdef WITH_OPENSSL + if (ups->ssl_ctx) { + upsdebugx(3, "%s: Using per-connection SSL context", __func__); + } else /* try using global default SSL context (legacy-compatible) */ if (!ssl_ctx) { upsdebugx(3, "%s: SSL context is not available", __func__); return 0; } - ups->ssl = SSL_new(ssl_ctx); + ups->ssl = SSL_new((SSL_CTX *)(ups->ssl_ctx ? ups->ssl_ctx : ssl_ctx)); if (!ups->ssl) { upsdebugx(3, "%s: Can not create SSL socket", __func__); return 0; @@ -3444,6 +3473,12 @@ int upscli_disconnect(UPSCONN_t *ups) SSL_free(ups->ssl); ups->ssl = NULL; } + + if (ups->ssl_ctx && ups->ssl_ctx_owned) { + SSL_CTX_free(ups->ssl_ctx); + ups->ssl_ctx = NULL; + ups->ssl_ctx_owned = 0; + } #elif defined(WITH_NSS) /* !WITH_OPENSSL */ if (ups->ssl) { PR_Shutdown(ups->ssl, PR_SHUTDOWN_BOTH); diff --git a/clients/upsclient.h b/clients/upsclient.h index c4a2526abc..1864da8cf9 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -98,6 +98,7 @@ typedef struct { char errbuf[UPSCLI_ERRBUF_LEN]; + /* Per-connection SSL details: */ #ifdef WITH_OPENSSL SSL *ssl; #elif defined(WITH_NSS) /* WITH_OPENSSL */ @@ -111,12 +112,24 @@ typedef struct { size_t readidx; /* WARNING for maintainers/devs: keep the ifdef'ed struct sizes - * same for different builds! */ + * same for different builds, and add new data items in the end! */ + + /* SSL context (trusted CA, own cert, etc.) may be global (NULL here) + * or shared (owned by us or reference to an instance owned and freed + * elsewhere). This allows the same client to connect to multiple + * data servers whose crypto is under different management realms. + * + * NOTE: OpenSSL has this concept, Mozilla NSS currently does not + * (its context is process-wide via NSS_Init() callable once). + */ #ifdef WITH_OPENSSL openssl_cert_verify_data_t *openssl_cert_verify_data; + SSL_CTX *ssl_ctx; #else void *extra_reserved; -#endif /* WITH_OPENSSL | WITH_NSS */ + void *ssl_ctx; /* essentially padding for struct size in different build variants */ +#endif /* WITH_OPENSSL */ + char ssl_ctx_owned; /* if not 0, we own the SSL_CTX and should free it on cleanup (if applicable) - meaning nobody else refers to that memory */ } UPSCONN_t; @@ -163,6 +176,9 @@ int upscli_init2(int certverify, const char *certpath, const char *certname, con int upscli_init_authconf(upscli_authconf_t *ac); int upscli_cleanup(void); +void *upscli_set_ssl_context(UPSCONN_t *ups, void *ssl_ctx); +void *upscli_get_ssl_context(UPSCONN_t *ups); + int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval *tv); /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ int upscli_connect(UPSCONN_t *ups, const char *host, uint16_t port, int flags); From d4dec1e31fe70d1392beb13aa26659e5e79d9838 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 30 Jul 2026 15:12:56 +0000 Subject: [PATCH 02/26] clients/nutclient.cpp: make OpenSSL "ssl_ctx" and NSS "_nss_initialized" flag properties of the Socket class instance, not static globals assigned once per process [#3439] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 0f2a116d95..632388b857 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -295,12 +295,13 @@ class Socket #ifdef WITH_SSL_CXX # ifdef WITH_OPENSSL SSL* _ssl; + SSL_CTX* _ssl_ctx; openssl_cert_verify_data_t openssl_cert_verify_data; int _verify_depth; static int _openssl_cert_verify_data_index; - static SSL_CTX* _ssl_ctx; # elif defined(WITH_NSS) PRFileDesc* _ssl; + bool _nss_initialized; # endif #endif bool _debugConnect; @@ -342,7 +343,6 @@ class Socket #ifdef WITH_SSL_CXX # ifdef WITH_OPENSSL -SSL_CTX* Socket::_ssl_ctx = nullptr; int Socket::_openssl_cert_verify_data_index = 0; /* Adapted from https://stackoverflow.com/a/42477707 with references to @@ -862,6 +862,9 @@ Socket::Socket(): # if defined(WITH_OPENSSL) || defined(WITH_NSS) _ssl(nullptr), # endif +# ifdef WITH_OPENSSL + _ssl_ctx(nullptr), +# endif # if defined(WITH_OPENSSL) _verify_depth(9), /* openssl default */ # endif @@ -898,6 +901,12 @@ Socket::Socket(): Socket::~Socket() { disconnect(); +#ifdef WITH_OPENSSL + if (_ssl_ctx) { + SSL_CTX_free(_ssl_ctx); + _ssl_ctx = nullptr; + } +#endif } void Socket::setTimeout(time_t timeout) @@ -1658,10 +1667,8 @@ void Socket::startTLS() # elif defined(WITH_NSS) /* NSS implementation following upsclient.c logic */ - static bool nss_initialized = false; - /* FIXME: Support several NSS databases, use prefix parameters? */ - if (!nss_initialized) { + if (!_nss_initialized) { PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); PK11_SetPasswordFunc(nss_password_callback); @@ -1682,7 +1689,7 @@ void Socket::startTLS() if (status != SECSuccess) { throw nut::SSLException_NSS("NSS initialization failed"); } - nss_initialized = true; + _nss_initialized = true; } PRFileDesc *socket = PR_ImportTCPSocket(static_cast(_sock)); From 9c226392bde1ec14b8d029b1a23ce87275595dfe Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 30 Jul 2026 15:13:52 +0000 Subject: [PATCH 03/26] tests/nutclienttest.cpp: add NutClientTest::test_ssl_context_registry() (currently via upscli_set_ssl_context() not C++) [#3439] Signed-off-by: Jim Klimov --- tests/nutclienttest.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/nutclienttest.cpp b/tests/nutclienttest.cpp index 7c4ebd760a..399385cde6 100644 --- a/tests/nutclienttest.cpp +++ b/tests/nutclienttest.cpp @@ -36,6 +36,7 @@ class NutClientTest : public CppUnit::TestFixture CPPUNIT_TEST( test_copy_constructor_dev ); CPPUNIT_TEST( test_copy_assignment_dev ); + CPPUNIT_TEST( test_ssl_context_registry ); CPPUNIT_TEST( test_copy_constructor_cmd ); CPPUNIT_TEST( test_copy_assignment_cmd ); @@ -56,6 +57,7 @@ class NutClientTest : public CppUnit::TestFixture void test_copy_constructor_dev(); void test_copy_assignment_dev(); + void test_ssl_context_registry(); void test_copy_constructor_cmd(); void test_copy_assignment_cmd(); @@ -77,6 +79,7 @@ CPPUNIT_TEST_SUITE_REGISTRATION( NutClientTest ); #include "../clients/nutclient.h" #include "../clients/nutclientmem.h" +#include "../clients/upsclient.h" namespace nut { @@ -218,6 +221,25 @@ void NutClientTest::test_copy_assignment_dev() { i, j); } +void NutClientTest::test_ssl_context_registry() { + UPSCONN_t ups; + memset(&ups, 0, sizeof(ups)); + + void *ctx = reinterpret_cast(0x1234); + void *previous = upscli_set_ssl_context(&ups, ctx); + + CPPUNIT_ASSERT_MESSAGE("Expected no previous SSL context", previous == nullptr); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the registered SSL context to be returned", + ctx, upscli_get_ssl_context(&ups)); + + void *replacement = reinterpret_cast(0x5678); + previous = upscli_set_ssl_context(&ups, replacement); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the previous SSL context to be returned on update", + ctx, previous); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the updated SSL context to be stored", + replacement, upscli_get_ssl_context(&ups)); +} + void NutClientTest::test_copy_constructor_cmd() { nut::TcpClient c; nut::Device d(nullptr, "ups1"); From f4ad9369c744fd0fde65281bc3d3aaf37681482d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 12:26:01 +0000 Subject: [PATCH 04/26] clients/nutclient.{h,cpp}: add Socket and TcpClient getters and setters for SSLContext [#3439] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 31 +++++++++++++++++++++++++++++++ clients/nutclient.h | 17 +++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 632388b857..c581f20cc2 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -280,6 +280,9 @@ class Socket void startTLS(); bool isSSL()const; + void *setSSLContext(void *ssl_ctx); + void *getSSLContext() const; + void setTimeout(time_t timeout); bool hasTimeout()const{return _tv.tv_sec>=0;} @@ -914,6 +917,18 @@ void Socket::setTimeout(time_t timeout) _tv.tv_sec = timeout; } +void *Socket::setSSLContext(void *ssl_ctx) +{ + void *previous = _ssl_ctx; + _ssl_ctx = static_cast(ssl_ctx); + return previous; +} + +void *Socket::getSSLContext() const +{ + return _ssl_ctx; +} + void Socket::setDebugConnect(bool d) { _debugConnect = d; @@ -3821,6 +3836,22 @@ void TcpClient::setSSLConfig(const SSLConfig& config) config.apply(*this); } +void *TcpClient::setSSLContext(void *ssl_ctx) +{ + if (!_socket) { + return nullptr; + } + return _socket->setSSLContext(ssl_ctx); +} + +void *TcpClient::getSSLContext() const +{ + if (!_socket) { + return nullptr; + } + return _socket->getSSLContext(); +} + void TcpClient::setSSLConfig_OpenSSL(int forcessl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass, const char *certident_name, const char *certhost_addr, const char *certhost_name) { delete _ssl_config_openssl; diff --git a/clients/nutclient.h b/clients/nutclient.h index 4995fe4130..14aa27e12c 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -1193,6 +1193,23 @@ class TcpClient : public Client */ void setSSLConfig(const SSLConfig& config); + /** + * Set a per-connection SSL context (advanced usage). + * Allows a specific connection to use its own SSL context instead of + * the library's default shared context. This enables clients to connect + * to multiple servers with different CA trust domains or client certificates + * simultaneously within the same process. + * \param ssl_ctx Opaque SSL context (SSL_CTX* for OpenSSL, etc.) + * \return Previous SSL context if one was set, nullptr otherwise. + */ + void *setSSLContext(void *ssl_ctx); + + /** + * Get the per-connection SSL context if one has been set. + * \return The SSL context set via setSSLContext(), or nullptr if none. + */ + void *getSSLContext() const; + /** * Connect it to the specified server. * \param host Server host name. From 60a2b4bca0611f6fbd6d22ba434b799a578a3f03 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 12:27:11 +0000 Subject: [PATCH 05/26] tests/nutclienttest.cpp: fix NutClientTest::test_ssl_context_registry() to use pure C++ implementation [#3439] Signed-off-by: Jim Klimov --- tests/nutclienttest.cpp | 45 +++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/tests/nutclienttest.cpp b/tests/nutclienttest.cpp index 399385cde6..fc8779e44c 100644 --- a/tests/nutclienttest.cpp +++ b/tests/nutclienttest.cpp @@ -2,7 +2,7 @@ Copyright (C) 2016 Emilien Kia - 2020 - 2025 Jim Klimov + 2020 - 2026 Jim Klimov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -79,7 +79,6 @@ CPPUNIT_TEST_SUITE_REGISTRATION( NutClientTest ); #include "../clients/nutclient.h" #include "../clients/nutclientmem.h" -#include "../clients/upsclient.h" namespace nut { @@ -222,22 +221,42 @@ void NutClientTest::test_copy_assignment_dev() { } void NutClientTest::test_ssl_context_registry() { - UPSCONN_t ups; - memset(&ups, 0, sizeof(ups)); + /* Test the C++ API for per-connection SSL context management */ + nut::TcpClient client; - void *ctx = reinterpret_cast(0x1234); - void *previous = upscli_set_ssl_context(&ups, ctx); + //std::cerr << "Starting test_ssl_context_registry" << std::endl; - CPPUNIT_ASSERT_MESSAGE("Expected no previous SSL context", previous == nullptr); - CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the registered SSL context to be returned", - ctx, upscli_get_ssl_context(&ups)); + /* Initially, no custom SSL context should be set */ + void *initial = client.getSSLContext(); + CPPUNIT_ASSERT_MESSAGE("Expected no initial SSL context", initial == nullptr); + + //std::cerr << "Setting custom SSL context" << std::endl; + + /* Set a custom SSL context (using a test pointer) */ + void *test_ctx = reinterpret_cast(0x1234); + void *previous = client.setSSLContext(test_ctx); + + CPPUNIT_ASSERT_MESSAGE("Expected no previous SSL context on first set", previous == nullptr); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the registered SSL context to be returned by getter", + test_ctx, client.getSSLContext()); + + //std::cerr << "Updating SSL context" << std::endl; + + /* Update the SSL context and verify the old one is returned */ + void *replacement_ctx = reinterpret_cast(0x5678); + previous = client.setSSLContext(replacement_ctx); - void *replacement = reinterpret_cast(0x5678); - previous = upscli_set_ssl_context(&ups, replacement); CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the previous SSL context to be returned on update", - ctx, previous); + test_ctx, previous); CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected the updated SSL context to be stored", - replacement, upscli_get_ssl_context(&ups)); + replacement_ctx, client.getSSLContext()); + + /* Clear the fake context before the client is destroyed: + * ~Socket() calls SSL_CTX_free() on a non-null context, and + * our test pointers above are not real SSL_CTX objects. */ + client.setSSLContext(nullptr); + + //std::cerr << "Finished test_ssl_context_registry" << std::endl; } void NutClientTest::test_copy_constructor_cmd() { From 1306c6ed65b288fd84b64f93d4747c341674bcfa Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 13:16:51 +0000 Subject: [PATCH 06/26] docs/SSL_CONTEXT_ANALYSIS.adoc: add Copilot analysis of current situation in our and ecosystem code [#3439] Signed-off-by: Jim Klimov --- docs/SSL_CONTEXT_ANALYSIS.adoc | 412 +++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 docs/SSL_CONTEXT_ANALYSIS.adoc diff --git a/docs/SSL_CONTEXT_ANALYSIS.adoc b/docs/SSL_CONTEXT_ANALYSIS.adoc new file mode 100644 index 0000000000..7a6f24c87b --- /dev/null +++ b/docs/SSL_CONTEXT_ANALYSIS.adoc @@ -0,0 +1,412 @@ += SSL Context Architecture Analysis for Multi-Server Per-Connection Support + +== Overview + +This document addresses architectural questions about per-connection SSL context support, examining OpenSSL vs NSS capabilities, current implementation gaps, and proposing a context registry/caching system. + +--- + +== Question 1: NSS Context Object Capabilities + +=== OpenSSL Model + +* *Context Object*: `SSL_CTX` - a reusable configuration object + - Created once with specific CA bundles, client certs, verification modes, etc. + - Multiple `SSL_new(ssl_ctx)` calls create distinct connection objects from same context + - Different SSL_CTX objects can coexist in memory simultaneously + - Each connection can use a different SSL_CTX if needed + +=== NSS Model + +* *Context Object*: NO equivalent to `SSL_CTX` + - Instead: `PRFileDesc*` - purely connection-specific (wraps a socket FD) + - NSS initialization (`NSS_Init()`, `PR_Init()`, `NSS_SetDomesticPolicy()`) is *one-time global* + - All connections share the same global NSS database, policy, and configuration + - `SSL_ImportFD(NULL, socket)` creates per-connection NSS SSL FD, but uses global NSS state + +=== Key Architectural Difference + +[cols="1,1,1"] +|=== +| Aspect | OpenSSL | NSS + +| Context Object +| `SSL_CTX` (reusable) +| None - only global state + +| CA Bundles +| Per-context +| Global only (NSS_Init) + +| Client Certs +| Per-context +| Global only (NSS database) + +| Re-initialization +| Supported +| NOT supported + +| Per-connection variants +| Yes +| No (except callback hooks) +|=== + +=== NSS Limitation Impact + +* *Cannot simultaneously use different CA bundles* for different server connections +* *Cannot have per-connection client certificates* (must use global NSS DB) +* *Verification modes are global* (set via `SSL_OptionSetDefault()`) +* Workaround needed: Thread-local NSS initialization or external process isolation + +--- + +== Question 2: Current `nutclient.cpp` Per-Connection Context Handling + +=== Current State: CORRECT (checked mark) + +The code at `clients/nutclient.cpp:1419` properly checks: + +[source,cpp] +---- +if (!_ssl_ctx) { + // Create SSL_CTX for this Socket instance + _ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + // ... configure this context ... +} +---- + +Then at `clients/nutclient.cpp:1591`: + +[source,cpp] +---- +_ssl = SSL_new(_ssl_ctx); // Uses instance member, not global +---- + +=== Verification of Flow + +. Socket class has instance member: `SSL_CTX* _ssl_ctx;` (line 301) +. Each Socket instance creates/owns its own SSL_CTX +. Socket destructor frees it: lines 908-910 +. No reversion to global context - GOOD (checked mark) + +=== BUT: Problem in C API Layer + +The C API in `upsclient.c` at `upsclient.c:2244` still has the fallback pattern: + +[source,c] +---- +ups->ssl = SSL_new((SSL_CTX *)(ups->ssl_ctx ? ups->ssl_ctx : ssl_ctx)); +---- + +This is correct for backward compatibility. However: + +* If `ups->ssl_ctx` is NULL, it falls back to global `ssl_ctx` +* Global `ssl_ctx` was created by `upscli_init2()` and cannot be changed without re-init + +=== Conclusion + +* (checked mark) C++ Socket layer: Correctly uses per-connection context +* (checked mark) C API layer: Correctly falls back to global if no per-connection context +* (warning mark) Problem: Global context is locked after first init + +--- + +== Question 3: Plain C Code Re-Entry Problem + +=== Current Issue: CONFIRMED + +*File*: `clients/upsclient.c` +*Problem Location*: Lines 1041-1044 + +[source,c] +---- +if (upscli_initialized == 1) { + upslogx(LOG_WARNING, "upscli already initialized"); + return -1; // <- BLOCKS ANY RE-INITIALIZATION +} +---- + +=== Why This Blocks Per-Connection Contexts + +Current flow: + +---- +1. Program calls upscli_init(certverify=1, certpath="/etc/nut/server1-ca.pem") + └─ Creates global ssl_ctx with server1's CA bundle + └─ Sets upscli_initialized = 1 + +2. Program wants to connect to different server with server2's CA + └─ Calls upscli_init(certverify=1, certpath="/etc/nut/server2-ca.pem") + └─ X Returns -1 (already initialized) + └─ Cannot create separate context for server2 +---- + +=== Side Effects of the Lock + +* *OpenSSL*: Only one global SSL_CTX exists for entire process +* *NSS*: Even worse - NSS_Init() can only be called once + - Line 1299: `status = NSS_Init(certstore_path);` + - Calling again will fail or corrupt state + +=== Root Cause + +The one-time initialization was designed for single-connection programs. Multi-server support requires: + +. *Lazy initialization* - Only initialize global defaults when first actually needed +. *Per-connection initialization* - Allow custom contexts per connection +. *Context registry* - Cache and reuse contexts for same config + +--- + +== Question 4: Context Registry/Caching System Design + +=== Proposed Architecture + +==== 4.1 C Struct for Context Metadata + +[source,c] +---- +typedef struct upscli_ssl_context_config_s { + /* Configuration keys for lookup */ + char *ca_path; /* NULL if using default */ + char *ca_file; /* NULL if using default */ + char *certfile; /* NULL if none */ + char *certpasswd; /* NULL if none */ + + /* OpenSSL-specific */ + #ifdef WITH_OPENSSL + SSL_CTX *ssl_ctx; /* The actual context object */ + int cert_verify; /* SSL_VERIFY_* mode */ + #endif + + /* NSS-specific */ + #ifdef WITH_NSS + char *nss_db_path; /* NSS database location (if different from ca_path) */ + int nss_initialized; /* Whether NSS was init'd for this config */ + #endif + + /* Reference counting */ + unsigned int refcount; /* Number of connections using this */ + time_t created_at; /* For cache eviction if needed */ + +} upscli_ssl_context_config_t; +---- + +==== 4.2 Registry Structure + +[source,c] +---- +typedef struct { + upscli_ssl_context_config_t **configs; /* Array of context configs */ + size_t count; /* Number of registered contexts */ + size_t capacity; /* Allocated capacity */ + + #ifdef WITH_OPENSSL + SSL_CTX *default_ssl_ctx; /* Global fallback for backward compat */ + #endif + + #ifdef WITH_NSS + int default_nss_initialized; /* Was NSS initialized globally? */ + #endif + +} upscli_ssl_context_registry_t; +---- + +==== 4.3 Registry API Functions + +[source,c] +---- +/* Initialize the registry (called once on first use) */ +upscli_ssl_context_registry_t *upscli_get_ssl_registry(void); + +/* Look up or create a context for given configuration */ +upscli_ssl_context_config_t *upscli_get_or_create_ssl_context( + const char *ca_path, + const char *ca_file, + const char *certfile, + const char *certpasswd, + int cert_verify +); + +/* Increment reference count */ +void upscli_ssl_context_acquire(upscli_ssl_context_config_t *cfg); + +/* Decrement reference count, free if unused */ +void upscli_ssl_context_release(upscli_ssl_context_config_t *cfg); + +/* Clear registry on program exit */ +void upscli_ssl_context_registry_cleanup(void); +---- + +==== 4.4 Integration with UPSCONN_t + +[source,c] +---- +typedef struct { + /* ... existing fields ... */ + + /* Replace: SSL_CTX *ssl_ctx; */ + /* With: Reference to registry entry */ + upscli_ssl_context_config_t *ssl_context_cfg; + + /* Keep for backward compat: */ + char ssl_ctx_owned; /* Still used if ssl_ctx is malloc'd directly */ + +} UPSCONN_t; +---- + +=== 4.5 Workflow Example: Multi-Server Connection + +*Before (Current - Broken):* + +[source,c] +---- +/* Connection 1: Server with CA bundle 1 */ +upscli_init(1, "/etc/nut/server1-ca.pem", NULL, NULL); // OK +upscli_connect(&ups1, "server1.example.com", 3493); + +/* Connection 2: Server with CA bundle 2 */ +upscli_init(1, "/etc/nut/server2-ca.pem", NULL, NULL); // ERROR: already initialized +// X Cannot connect with different CA +---- + +*After (With Registry):* + +[source,c] +---- +/* Connection 1: Server with CA bundle 1 */ +cfg1 = upscli_get_or_create_ssl_context( + "/etc/nut/server1-ca.pem", NULL, NULL, NULL, 1); +upscli_set_connection_ssl_context(&ups1, cfg1); +upscli_connect(&ups1, "server1.example.com", 3493); + +/* Connection 2: Server with CA bundle 2 */ +cfg2 = upscli_get_or_create_ssl_context( + "/etc/nut/server2-ca.pem", NULL, NULL, NULL, 1); +upscli_set_connection_ssl_context(&ups2, cfg2); +upscli_connect(&ups2, "server2.example.com", 3493); + +/* Connection 3: Server with CA bundle 1 (reuses cached context) */ +cfg1_again = upscli_get_or_create_ssl_context( + "/etc/nut/server1-ca.pem", NULL, NULL, NULL, 1); // Returns cached cfg1 +---- + +=== 4.6 Benefits of Registry Approach + +. *No Re-Entry Problem*: Each context is independent +. *Memory Efficient*: Same config shared across multiple connections +. *Lazy Initialization*: Only init global defaults if no per-connection context +. *Backward Compatible*: Programs using `upscli_init()` still work +. *NSS Safe*: Can isolate NSS operations per context via thread-local or separate init +. *Clear Lifecycle*: Reference counting tracks who owns what + +=== 4.7 NSS-Specific Considerations + +For NSS, the registry needs special handling: + +* *Option A (Process-level)*: Keep one global NSS init, accept single CA database +* *Option B (Thread-local)*: Use thread-local storage for different NSS inits per thread +* *Option C (Child processes)*: Fork for different NSS configs (expensive) + +*Recommendation*: Implement Option A as minimum (NSS limitation), but document that: + +* OpenSSL supports multiple per-connection contexts (full multi-server support) +* NSS supports multiple connections but with shared global config +* Applications should check `WITH_OPENSSL` vs `WITH_NSS` if multi-config needed + +=== 4.8 API Entry Point + +New C API function to set per-connection context (on UPSCONN_t after creation, before connect): + +[source,c] +---- +/* Set SSL context configuration for this connection + * Returns 0 on success, -1 if context cannot be created + * Optionally takes an upscli_authconf_t to use pre-configured values + */ +int upscli_set_ssl_context_from_authconf( + UPSCONN_t *ups, + upscli_authconf_t *ac +); + +/* Or for direct control: */ +int upscli_set_ssl_context_params( + UPSCONN_t *ups, + const char *ca_path, + const char *ca_file, + const char *certfile, + const char *certpasswd, + int cert_verify +); +---- + +--- + +== Implementation Roadmap + +=== Phase 1: Remove Re-Entry Block (High Priority) + +* Change `upscli_initialized` check from `if == 1 return -1` to `if == 1 return 0` (no-op) +* Document that calling `upscli_init*()` multiple times is now allowed but only first call takes effect +* *Impact*: Allows applications to call init multiple times safely + +=== Phase 2: Build Registry (Medium Priority) + +* Implement `upscli_ssl_context_registry_t` and registry functions +* Store in static variable (thread-safe with mutexes if needed later) +* Add `upscli_get_or_create_ssl_context()` function + +=== Phase 3: Integrate with UPSCONN_t (High Priority) + +* Add `ssl_context_cfg` field (or modify `ssl_ctx` storage) +* Update `upscli_sslinit()` to use registry-managed contexts +* Update C++ Socket class if needed + +=== Phase 4: C++ API Exposure (Medium Priority) + +* Add TcpClient method to set context by config parameters +* Integrate with SSLConfig class if applicable + +=== Phase 5: Testing (High Priority) + +* Unit tests for registry lookup/create/release +* Integration tests with multiple simultaneous connections +* Tests for both OpenSSL and NSS backends + +--- + +== Summary Table + +[cols="1,1,1,1"] +|=== +| Question | Status | Current Gap | Recommendation + +| NSS context capabilities +| OK +| No per-connection configs possible +| Document NSS limitation, handle via registry + +| nutclient.cpp per-connection handling +| OK +| None at C++ level +| Already implemented correctly + +| upsclient.c re-entry block +| CRITICAL ISSUE +| Cannot re-init for different configs +| Remove re-entry check or make it no-op + +| Context registry system +| NOT IMPLEMENTED +| Needed for production +| Implement registry as Phase 2-3 +|=== + +--- + +== References + +* OpenSSL SSL_CTX: link:https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_new.html[] +* NSS Initialization: link:https://wiki.mozilla.org/NSS:Initialization[] +* Mozilla NSS SSL Guide: link:https://wiki.mozilla.org/NSS:SSL_Functions[] +* NUT Authentication Config: `clients/authconf.h` From 1f51b8ad85b9a083199f7e0ec7c2ccdd42785751 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:07:18 +0000 Subject: [PATCH 07/26] docs/SSL_CONTEXT_ANALYSIS.adoc: follow-up discussion for true multi-context approach [#3439] Signed-off-by: Jim Klimov --- docs/SSL_CONTEXT_ANALYSIS.adoc | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/SSL_CONTEXT_ANALYSIS.adoc b/docs/SSL_CONTEXT_ANALYSIS.adoc index 7a6f24c87b..97c7f5a762 100644 --- a/docs/SSL_CONTEXT_ANALYSIS.adoc +++ b/docs/SSL_CONTEXT_ANALYSIS.adoc @@ -410,3 +410,42 @@ int upscli_set_ssl_context_params( * NSS Initialization: link:https://wiki.mozilla.org/NSS:Initialization[] * Mozilla NSS SSL Guide: link:https://wiki.mozilla.org/NSS:SSL_Functions[] * NUT Authentication Config: `clients/authconf.h` + +== Follow-up discussion for proposed changes + +Design documents are updated. Here's a summary of the refined plan: + +=== Answers to your questions + +*1. `ssl_ctx_owned` removal -- confirmed correct.* +Verified it's dead code: only read at `upsclient.c:3477`, never +set to `1` anywhere. The registry will own contexts via refcounting +instead, so this field goes away entirely (safe -- it was only added +in this unreleased branch). + +*2. Where `upscli_init2()`'s body goes.* +It becomes the "create" branch of `upscli_get_or_create_ssl_context()`. +The `upscli_init2()` itself shrinks to a thin wrapper that calls the +registry and decides whether the result becomes/matches the ambient default (see #5 below). + +*3. Legacy compatibility -- verified against tags.* +- `v2.8.5`: only `upscli_init()` exists. +- `origin/master`: already has `upscli_init2()`, `upscli_init_authconf()`, and `authconf.h` (post-2.8.5 additions), but no `ssl_ctx`/`ssl_ctx_owned` (those are new on this branch). +- Checked every in-tree caller: all of them call the `upscli_init*()` family **exactly once** per process at startup. This is the key fact that makes the return-code scheme safe (see #5). + +*4. NSS Option A -- confirmed, plus a bonus finding.* +Global NSS state (`PR_Init`/`NSS_Init`/policy) genuinely can't be per-connection. But I found that `GetClientAuthData()` and `BadCertHandler()` already receive the `UPSCONN_t *` as callback userdata -- `BadCertHandler` already does per-connection CERTHOST lookups, but `GetClientAuthData` currently ignores `arg` and reads the global `sslcertname` instead. So **per-connection client certificate identity is achievable for NSS** even though CA trust/policy stays global. Added a threaded workaround example (mutex-serialized nickname switching) with an explicit caveat that it's not full isolation. + +*5. `sslcertname`/`sslcertpasswd` → context fields -- agreed*, +moving into `upscli_ssl_context_config_t`, with globals retained only as the default context's cached copies for callback paths that don't thread a context pointer through yet. + +*6. Return code contract -- your hypothesis is exactly right:* +[cols="1,1"] +|=== +| Return | Meaning +| `1` | Success; this call's context is/becomes the ambient default (first-ever call, or repeat with identical args) +| `0` | Success; a *new/cached* non-default context now exists, retrievable via `upscli_get_or_create_ssl_context()`, but the ambient default was left untouched +| `-1` | Hard failure (unchanged) +|=== + +I (CoPilot) verified this is backward-safe by checking every call site's convention (`> 0` in most clients, `< 0` in `upsmon.c`) -- since no existing caller ever calls init twice, the new `0` case can never be triggered by unmodified programs. From 706d8cd5bee5cddc79fc80c2aea5dfeeee28ae43 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:11:15 +0000 Subject: [PATCH 08/26] clients/nutclient.cpp: nss_error(): pass `sock` to use desired cert with its password [#3439] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index c581f20cc2..8300e57cd1 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -827,11 +827,13 @@ static void nss_error(const char* text) if (status == SECFailure) { if (sock && sock->_ssl_config && !sock->_ssl_config->getCertIdentName().empty()) { - cert = PK11_FindCertFromNickname(sock->_ssl_config->getCertIdentName().c_str(), nullptr); + /* Pass "sock" through as wincx, so nss_password_callback() + * can resolve this same connection's per-Socket password. */ + cert = PK11_FindCertFromNickname(sock->_ssl_config->getCertIdentName().c_str(), sock); if (cert == nullptr) { nss_error("GetClientAuthData / PK11_FindCertFromNickname"); } else { - privKey = PK11_FindKeyByAnyCert(cert, nullptr); + privKey = PK11_FindKeyByAnyCert(cert, sock); if (privKey == nullptr) { nss_error("GetClientAuthData / PK11_FindKeyByAnyCert"); CERT_DestroyCertificate(cert); From a12061acc08008d8beaf98b29243b5fe1ce3948b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:13:36 +0000 Subject: [PATCH 09/26] clients/upsclient.c: nss_password_callback(): prefer cert password stored for the connection (if any) over global default (first known pass) [#3439] Signed-off-by: Jim Klimov --- clients/upsclient.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 7dee1b0ab2..0fcd3fcde8 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -290,13 +290,18 @@ static int ssl_error(SSL *ssl, ssize_t ret) static char *nss_password_callback(PK11SlotInfo *slot, PRBool retry, void *arg) { + /* Prefer the per-connection identity (if any) over the library-wide default, + * so different connections in one process can use different client certs + * from the same shared NSS certificate/key database. */ + UPSCONN_t *ups = (UPSCONN_t *)arg; + const char *passwd = (ups && ups->certident_pass) ? ups->certident_pass : sslcertpasswd; + NUT_UNUSED_VARIABLE(retry); - NUT_UNUSED_VARIABLE(arg); upslogx(LOG_INFO, "Intend to retrieve password for %s / %s: password %sconfigured", PK11_GetSlotName(slot), PK11_GetTokenName(slot), - sslcertpasswd ? "" : "not "); - return sslcertpasswd ? PL_strdup(sslcertpasswd) : NULL; + passwd ? "" : "not "); + return passwd ? PL_strdup(passwd) : NULL; } /** Detail the currently raised NSS error code if possible, and debug-log From 97726f3f4f707bbaf95fdea197eb3647286555ee Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:14:38 +0000 Subject: [PATCH 10/26] clients/upsclient.c: GetClientAuthData(): pass `UPSCONN_t arg` to use desired cert with its password [#3439] Signed-off-by: Jim Klimov --- clients/upsclient.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0fcd3fcde8..5cdcf83b1a 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -493,13 +493,20 @@ static SECStatus GetClientAuthData(UPSCONN_t *arg, PRFileDesc *fd, SECKEYPrivateKey *privKey; SECStatus status = NSS_GetClientAuthData(arg, fd, caNames, pRetCert, pRetKey); if (status == SECFailure) { - if (sslcertname != NULL) { - cert = PK11_FindCertFromNickname(sslcertname, NULL); + /* Prefer the per-connection identity (if any) over the library-wide + * default, so different connections in one process can present + * different client certs from the same shared NSS DB. Pass "arg" + * (this connection) through as wincx, so nss_password_callback() + * can likewise resolve a per-connection password. */ + const char *certname = (arg && arg->certident_name) ? arg->certident_name : sslcertname; + + if (certname != NULL) { + cert = PK11_FindCertFromNickname(certname, arg); if(cert==NULL) { upslogx(LOG_ERR, "Can not find self-certificate"); nss_error("GetClientAuthData / PK11_FindCertFromNickname"); }else{ - privKey = PK11_FindKeyByAnyCert(cert, NULL); + privKey = PK11_FindKeyByAnyCert(cert, arg); if(privKey==NULL){ upslogx(LOG_ERR, "Can not find private key related to self-certificate"); nss_error("GetClientAuthData / PK11_FindKeyByAnyCert"); From 3702d9bb7eaeb23d8efbd6b126dcdeba4120fbb2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:15:43 +0000 Subject: [PATCH 11/26] clients/upsclient.{c,h}: track desired CERTIDENT name/pass via `UPSCONN_t` [#3439] Signed-off-by: Jim Klimov --- clients/upsclient.c | 30 ++++++++++++++++++++++++++++++ clients/upsclient.h | 22 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index 5cdcf83b1a..35c24f4479 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1703,6 +1703,30 @@ void *upscli_get_ssl_context(UPSCONN_t *ups) return ups->ssl_ctx; } +int upscli_set_ssl_certident(UPSCONN_t *ups, const char *certident_name, const char *certident_pass) +{ + if (!ups) { + return -1; + } + + free(ups->certident_name); + ups->certident_name = certident_name ? xstrdup(certident_name) : NULL; + + free(ups->certident_pass); + ups->certident_pass = certident_pass ? xstrdup(certident_pass) : NULL; + + return 0; +} + +const char *upscli_get_ssl_certident_name(UPSCONN_t *ups) +{ + if (!ups) { + return NULL; + } + + return ups->certident_name; +} + int upscli_cleanup(void) { #ifdef WITH_OPENSSL @@ -3440,6 +3464,12 @@ int upscli_disconnect(UPSCONN_t *ups) free(ups->host); ups->host = NULL; + free(ups->certident_name); + ups->certident_name = NULL; + + free(ups->certident_pass); + ups->certident_pass = NULL; + #ifdef WITH_OPENSSL if (ups->openssl_cert_verify_data != NULL) { if (ups->openssl_cert_verify_data->hostname_allocated diff --git a/clients/upsclient.h b/clients/upsclient.h index 1864da8cf9..ebe52de6c2 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -131,6 +131,15 @@ typedef struct { #endif /* WITH_OPENSSL */ char ssl_ctx_owned; /* if not 0, we own the SSL_CTX and should free it on cleanup (if applicable) - meaning nobody else refers to that memory */ + /* Optional per-connection client certificate identity override, consulted + * (at least) by the NSS backend's GetClientAuthData()/nss_password_callback() + * so one process can present different client certificates to different + * servers even while sharing one process-wide NSS certificate/key database. + * NULL means fall back to the library-wide CERTIDENT set via upscli_init*(). + * See upscli_set_ssl_certident(). */ + char *certident_name; + char *certident_pass; + } UPSCONN_t; const char *upscli_strerror(UPSCONN_t *ups); @@ -179,6 +188,19 @@ int upscli_cleanup(void); void *upscli_set_ssl_context(UPSCONN_t *ups, void *ssl_ctx); void *upscli_get_ssl_context(UPSCONN_t *ups); +/* Set (or clear, with NULL args) a per-connection client certificate identity, + * to let one process present different client certificates to different + * servers even when they share one process-wide NSS certificate/key database + * (OpenSSL builds should prefer a distinct SSL context via upscli_set_ssl_context() + * instead, since OpenSSL supports a fully separate SSL_CTX per connection). + * Strings are copied internally; safe to free/reuse the arguments afterwards. + * Returns 0 on success, -1 on error (e.g. NULL ups). */ +int upscli_set_ssl_certident(UPSCONN_t *ups, const char *certident_name, const char *certident_pass); + +/* Get the per-connection client certificate nickname set via + * upscli_set_ssl_certident(), or NULL if none was set for this connection. */ +const char *upscli_get_ssl_certident_name(UPSCONN_t *ups); + int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval *tv); /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ int upscli_connect(UPSCONN_t *ups, const char *host, uint16_t port, int flags); From cb14b9c2daaf2bb85c2a2a1e37587b8c6a98a0e8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:17:39 +0000 Subject: [PATCH 12/26] docs/SSL_CONTEXT_ANALYSIS.adoc: document the revisited multi-context implementation [#3439] Signed-off-by: Jim Klimov --- docs/SSL_CONTEXT_ANALYSIS.adoc | 210 +++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/docs/SSL_CONTEXT_ANALYSIS.adoc b/docs/SSL_CONTEXT_ANALYSIS.adoc index 97c7f5a762..2b1540ac9f 100644 --- a/docs/SSL_CONTEXT_ANALYSIS.adoc +++ b/docs/SSL_CONTEXT_ANALYSIS.adoc @@ -449,3 +449,213 @@ moving into `upscli_ssl_context_config_t`, with globals retained only as the def |=== I (CoPilot) verified this is backward-safe by checking every call site's convention (`> 0` in most clients, `< 0` in `upsmon.c`) -- since no existing caller ever calls init twice, the new `0` case can never be triggered by unmodified programs. + +== Round 2: Follow-up Design Refinements + +=== R1. Is `ssl_ctx_owned` still needed? + +*No.* Confirmed by inspection: `ups->ssl_ctx_owned` is only ever read (in the +cleanup path at `upsclient.c:3477`) and is *never set to 1 anywhere* in the +current code. `upscli_set_ssl_context()` (the setter added in this branch) does +not touch it either. It is dead weight left over from an earlier iteration. + +Once the registry owns contexts via reference counting, per-connection +ownership tracking becomes unnecessary: a connection just holds a pointer +(reference) into the registry and calls `upscli_ssl_context_release()` on +disconnect; the registry itself decides whether to actually free the backing +`SSL_CTX`. *Plan*: remove the `ssl_ctx_owned` field from `UPSCONN_t` entirely +when the registry lands (safe to remove now since `ssl_ctx`/`ssl_ctx_owned` +were only added in this unreleased branch - not yet part of any tagged NUT +release, so there is no ABI compatibility burden for this specific field). + +=== R2. Where does the body of `upscli_init2()` go? + +It becomes the *create* branch of `upscli_get_or_create_ssl_context()`. +Concretely: + +. `upscli_get_or_create_ssl_context(certverify, certpath, certname, certpasswd, certfile)` + builds a lookup key from its arguments, searches the registry, and: + * *on hit*: returns the existing `upscli_ssl_context_config_t*` (bumping refcount); + * *on miss*: runs (almost verbatim) the body currently in `upscli_init2()` + (OpenSSL: `SSL_CTX_new()` + `SSL_CTX_load_verify_locations()` + cert/key loading + + CERTIDENT check; NSS: `PR_Init()`/`NSS_Init()`/`NSS_SetDomesticPolicy()`/ + `SSL_OptionSetDefault()` calls, but see R4 below for what stays global for NSS) + to build a new registry entry, then returns it. +. `upscli_init2()` itself becomes a thin legacy wrapper: ++ +[source,c] +---- +int upscli_init2(int certverify, const char *certpath, const char *certname, + const char *certpasswd, const char *certfile) +{ + upscli_ssl_context_config_t *cfg; + int is_new_default; + + cfg = upscli_get_or_create_ssl_context_ex(certverify, certpath, + certname, certpasswd, certfile, &is_new_default); + if (!cfg) { + return -1; + } + + if (!default_ssl_context_cfg) { + /* First ever successful init: this becomes THE ambient default */ + default_ssl_context_cfg = cfg; + return 1; + } + + if (cfg == default_ssl_context_cfg) { + /* Same args as the existing default: fully legacy-compatible no-op */ + return 1; + } + + /* Different args, default already set: new cache entry was made + * available (or already existed) but does NOT replace the ambient + * default used by connections that never call + * upscli_set_ssl_context(). Caller must fetch it explicitly. */ + return 0; +} +---- +. `upscli_init()` and `upscli_init_authconf()` are unchanged wrappers around + `upscli_init2()`, as they are today - no signature or behavior change for + correctly-written existing callers (see R3). + +=== R3. Legacy API compatibility check (v2.8.5 tag vs. `origin/master`) + +Checked directly: + +* *`git show v2.8.5:clients/upsclient.h`*: only `upscli_init(certverify, certpath, certname, certpasswd)` + exists. No `upscli_init2()`, no `upscli_init_authconf()`, no `authconf.h` at all. + => `upscli_init()` signature is the hard ABI/API floor we must never break. +* *`git show origin/master:clients/upsclient.h`*: already has `upscli_init()`, + `upscli_init2()`, and `upscli_init_authconf()` (the `authconf` subsystem is a + `master`-only, post-2.8.5 addition). No `ssl_ctx`/`ssl_ctx_owned` field exists + on `master` - those are new, unreleased additions from this branch. +* *All in-tree callers of `upscli_init_authconf()`* (`upsc.c`, `upscmd.c`, + `upsimage.c`, `upslog.c`, `upsrw.c`, `upsset.c`, `upsstats.c`, `dummy-ups.c`, + `scan_nut.c`) use the pattern `if (upscli_init_authconf(ac_conn) > 0)`. +* *`upsmon.c:4305`* is the one outlier, using `if (upscli_init2(...) < 0)` to + decide whether to abort ("SSL was required") or just warn and continue. + +*Conclusion*: every in-tree caller invokes the `upscli_init*()` family exactly +*once* per process, at startup, before any connection is made. None of them +calls it a second time with different arguments. This means: + +* Keeping `upscli_init()` / `upscli_init2()` / `upscli_init_authconf()` symbol + names and signatures exactly as they are today is sufficient for full + backward compatibility. +* The only behavior that must not change for a single call is: on success it + must configure the ambient/global default the same way as today, so that a + plain `upscli_connect()` + `upscli_sslinit()` (with no explicit per-connection + context set) continues to pick up the requested CA/cert/policy exactly like + before. +* The new "0 means a different cached context was made, but the default + was not touched" return value (R6) is a new, additive case that cannot be + triggered by any existing in-tree caller (since they never call init twice), + so it cannot regress current behavior. + +=== R4. NSS: Option A, plus a real per-connection nuance + +We still go with *Option A* for the parts of NSS that are unavoidably +process-global: `PR_Init()`, `NSS_Init()`/`NSS_NoDB_Init()`, +`NSS_SetDomesticPolicy()`, and the `SSL_OptionSetDefault()` protocol-version/ +compat flags. These can only be set up once per process. Document this +plainly: with NSS, all connections in one process share one CA/trust +database and one global policy; if different trust roots per server are +really needed, run separate processes, since NSS does not expose an +`SSL_CTX`-like reusable object. + +However, inspection of the existing NSS callback code shows client identity +already carries the connection (`UPSCONN_t *arg`) through as callback +userdata: + +* `SSL_AuthCertificateHook(ups->ssl, AuthCertificate, CERT_GetDefaultCertDB())` +* `SSL_BadCertHook(ups->ssl, BadCertHandler, ups)` - and `BadCertHandler()` + already does a per-connection lookup via `upscli_find_host_port_cert(arg->host, arg->port, 1)` + for CERTHOST-style pinning (see `upsclient.c:464-481`). +* `SSL_GetClientAuthDataHook(ups->ssl, GetClientAuthData, ups)` - but + `GetClientAuthData()` currently ignores its own `arg` and instead reads the + process-global `sslcertname`/`sslcertpasswd` (`upsclient.c:490-492`). + +That last one is the actual bug/gap: the plumbing for a per-connection client +certificate nickname + password already exists (the `arg` is the `ups`), it is +just not used. Once `certname`/`certpasswd` move from process globals to +context fields (R5), `GetClientAuthData(UPSCONN_t *arg, ...)` can resolve +`arg->ssl_context_cfg->certident` / `->certpasswd` first, falling back to the +global default context's values only if `arg` has no explicit context. So for +NSS: trust roots/policy are unavoidably global (Option A), but per-connection +client certificate identity is achievable and should be fixed as part of this work. + +==== Threaded workaround example (documentation only, not different DB per thread) + +Because NSS's DB/policy is process-global, a multi-threaded client that truly +needs different trust roots per server has no in-process solution; the closest +usable pattern (still sharing the one NSS DB and policy) is to serialize +switching the client cert identity nickname around the handshake if two +threads must present different client certificates from the same NSS DB: + +[source,c] +---- +/* Illustrative only - assumes both certs live in the SAME NSS cert/key DB, + * which is the only DB NSS_Init() loaded for this whole process. */ +pthread_mutex_lock(&nss_identity_switch_mutex); +upscli_set_ssl_context(ups, per_thread_cfg); /* selects certident/certpasswd */ +upscli_sslinit(ups, verifycert); /* GetClientAuthData() reads ups->ssl_context_cfg */ +pthread_mutex_unlock(&nss_identity_switch_mutex); +---- + +This is not full isolation (CA bundle and protocol policy are still shared +process-wide), so it must be documented as a partial workaround, not a full +substitute for OpenSSL's per-connection `SSL_CTX`. + +=== R5. Move `sslcertname` / `sslcertpasswd` from globals to context fields + +Agreed and confirmed necessary. Both are currently `static char *` process +globals (`upsclient.c:234-235`) read from many places: the NSS password +callback (`nss_password_callback`, line ~298), `GetClientAuthData()` (cert +nickname lookup, line ~491), the OpenSSL default-password callback +(`openssl_password_callback`, line ~546-550), and the OpenSSL CERTIDENT +subject-matching code inside `upscli_init2()` itself (lines ~1209-1259). + +*Plan*: move both into `upscli_ssl_context_config_t` (the registry entry), +alongside `ca_path`/`ca_file`/`certfile`. Keep two process-global `char *` +variables only as a cache of the default context's copies (so existing +code that reads the bare globals - e.g. the OpenSSL/NSS password callbacks, +which don't currently receive a context pointer as userdata in every call path - +continues to work unmodified for the legacy single-default-context case). +Where a connection (`UPSCONN_t *`) is already threaded through as callback +userdata (`GetClientAuthData`, `BadCertHandler`, `openssl_password_callback`'s +`userdata` argument), prefer the per-connection context's copies first. + +=== R6. Practical effect of "unblocking" - return code contract + +Simply changing `return -1` to `return 0` on repeat calls would be a no-op +that helps nobody: the second call still would not have prepared anything +usable for the caller's new arguments. The registry only becomes useful if the +repeat call actually does the work (looks up or builds a context) and the +return value tells the caller whether the ambient legacy default changed. +Concretely, `upscli_init()`/`upscli_init2()`/`upscli_init_authconf()` should return: + +[cols="1,3,4"] +|=== +| Return | Meaning | Legacy caller sees... + +| `1` +| Success, and this call's context is (now, or already was) the ambient default used by connections with no explicit per-connection context - i.e. this is either the very first successful init, or a repeat call whose arguments exactly match the existing default. +| Success (same as always: `> 0` and `>= 0` checks both pass). + +| `0` +| Success in the sense that a context now exists in the registry for these arguments (fresh or cache hit) and is retrievable via `upscli_get_or_create_ssl_context()`, but it is NOT the ambient default (a different default was already set by an earlier call) - so nothing changed for connections that do not opt in explicitly. +| Callers using `> 0` (all of upsc/upscmd/upsimage/upslog/upsrw/upsset/upsstats/dummy-ups/scan_nut) treat this the same as failure/no-SSL, which is correct for them since they never call it twice. `upsmon.c`'s `< 0` check treats it as "not a hard failure," also correct, and moot in practice since upsmon never calls it twice either. + +| `-1` +| Hard failure: bad arguments, or the backend failed to build the context (cert/key load failure, NSS init failure, etc.) - same meaning as today. +| Failure, exactly as today. +|=== + +This exactly matches the proposed semantics: *1 for "already cached and is/becomes +the default"*, and a distinct non-negative-but-not-1 code (0) to tell a +semi-updated caller "go fetch the handle yourself, the implicit default was +left alone." Verified safe against every current in-tree call site because +none of them ever calls the `upscli_init*()` family more than once per process. + +--- From 67e3ed0c6c0e82e5bbbf94a4166dd658833f95a4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:22:13 +0000 Subject: [PATCH 13/26] clients/upsclient.h: document current return values for upscli_init*() methods [#3439] Signed-off-by: Jim Klimov --- clients/upsclient.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clients/upsclient.h b/clients/upsclient.h index ebe52de6c2..145bcd562e 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -180,6 +180,10 @@ struct timeval *upscli_upslog_start_sync(struct timeval *tv, const void *cookie) /* NOTE: init effectively only runs once; re-runs quickly skip out */ /* Legacy init function, prefer upscli_init2() with support for OpenSSL * client certificate file. Equivalent to prefer upscli_init2(..., NULL) */ +/* Return values: + * 1 on success (upscli_connect and upscli_sslinit can be used), + * -1 on hard error (failed to read crypto material, etc.) + */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd); int upscli_init2(int certverify, const char *certpath, const char *certname, const char *certpasswd, const char *certfile); int upscli_init_authconf(upscli_authconf_t *ac); From c3510aa0313ee257e7b64df87b53bf770d1c7096 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 15:45:26 +0000 Subject: [PATCH 14/26] clients/upsclient.{c,h}: Complete registry-based SSL multi-context refactor with secure memory zeroing [#3439] - Implement process-wide SSL context registry for caching and reuse - Registry stores config parameters (certverify, certpath, certname, certpasswd, certfile) - Per-connection references use opaque handles to registry entries (not owned) - NSS per-connection client cert identity now works correctly via registry - Remove sql_ctx_owned field (registry handles lifecycle) - Remove per-connection certident_name/certident_pass (moved to registry) - Implement secure zeroing of sensitive fields (passwords, cert names/paths) - Registry automatically freed in upscli_cleanup() - upscli_sslinit() now uses registry context instead of global ssl_ctx - Full backward compatibility with legacy upscli_init*() APIs via default context - Supports simultaneous multi-realm connections with different CAs and client certs Signed-off-by: Jim Klimov Co-authored-by: GitHub CoPilot --- clients/upsclient.c | 805 +++++++++++++++++++++++++++----------------- clients/upsclient.h | 51 ++- 2 files changed, 520 insertions(+), 336 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 35c24f4479..f1887d77cc 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -212,17 +212,50 @@ static int upscli_default_connect_timeout_initialized = 0; # endif #endif -#ifdef WITH_OPENSSL -/* Default SSL context (for legacy compatibility and apps that only - * make one connection per process); see ups->ssl_ctx if your app - * wants to connect to different NUT servers under separate management - * (CA realms, client certificates, etc.) simultaneously. */ -static SSL_CTX *ssl_ctx = NULL; -#endif /* WITH_OPENSSL */ +#if defined(WITH_OPENSSL) || defined(WITH_NSS) +/* One set of SSL/crypto material configuration (CA bundle, client cert + * identity, verify mode). Built and cached by upscli_get_or_create_ssl_context() + * and upscli_get_or_create_ssl_context_authconf(); attached to a connection + * via upscli_set_ssl_context(). May be shared/reused by several connections + * (see refcount), so the same process can connect to multiple NUT servers + * under separate management realms (CA bundles, client certs) simultaneously. + * + * NOTE: OpenSSL has a reusable per-context SSL_CTX; Mozilla NSS does not + * (its trust database and policy are process-wide via NSS_Init(), callable + * only once) - see notes at upscli_get_or_create_ssl_context() for how NSS + * connections still get a per-connection client certificate identity. + */ +typedef struct upscli_ssl_context_config_s { + int certverify; + char *certpath; + char *certname; /* CERTIDENT nickname/subject */ + char *certpasswd; + char *certfile; /* OpenSSL-only: client cert+key PEM; unused for NSS */ + +# ifdef WITH_OPENSSL + SSL_CTX *ssl_ctx; +# endif + + unsigned int refcount; + struct upscli_ssl_context_config_s *next; +} upscli_ssl_context_config_t; + +static upscli_ssl_context_config_t *ssl_context_registry = NULL; + +/* Ambient default used by connections that never call upscli_set_ssl_context(): + * legacy-compatible behavior for upscli_init()/upscli_init2()/upscli_init_authconf(). + */ +static upscli_ssl_context_config_t *default_ssl_context_cfg = NULL; +#endif /* WITH_OPENSSL | WITH_NSS */ #ifdef WITH_NSS static int verify_certificate = 1; static int nss_initialized = 0; +/* Remembers the CERTPATH which the (single, process-global) NSS trust database + * was actually initialized with, purely so that later contexts with a different + * CERTPATH can get an accurate one-time warning about the backend limitation. + */ +static char *nss_first_certpath = NULL; #endif /* WITH_NSS */ #if defined(WITH_OPENSSL) || defined(WITH_NSS) @@ -231,8 +264,6 @@ static pthread_mutex_t mutex_host_cert; # endif /* HAVE_PTHREAD */ static HOST_CERT_t *first_host_cert = NULL; -static char* sslcertname = NULL; -static char* sslcertpasswd = NULL; #endif /* WITH_OPENSSL | WITH_NSS */ @@ -290,14 +321,20 @@ static int ssl_error(SSL *ssl, ssize_t ret) static char *nss_password_callback(PK11SlotInfo *slot, PRBool retry, void *arg) { - /* Prefer the per-connection identity (if any) over the library-wide default, + /* Prefer the per-connection context (if any) over the ambient default, * so different connections in one process can use different client certs * from the same shared NSS certificate/key database. */ UPSCONN_t *ups = (UPSCONN_t *)arg; - const char *passwd = (ups && ups->certident_pass) ? ups->certident_pass : sslcertpasswd; + upscli_ssl_context_config_t *cfg = ups ? (upscli_ssl_context_config_t *)ups->ssl_ctx : NULL; + const char *passwd; NUT_UNUSED_VARIABLE(retry); + if (!cfg) { + cfg = default_ssl_context_cfg; + } + passwd = cfg ? cfg->certpasswd : NULL; + upslogx(LOG_INFO, "Intend to retrieve password for %s / %s: password %sconfigured", PK11_GetSlotName(slot), PK11_GetTokenName(slot), passwd ? "" : "not "); @@ -493,12 +530,18 @@ static SECStatus GetClientAuthData(UPSCONN_t *arg, PRFileDesc *fd, SECKEYPrivateKey *privKey; SECStatus status = NSS_GetClientAuthData(arg, fd, caNames, pRetCert, pRetKey); if (status == SECFailure) { - /* Prefer the per-connection identity (if any) over the library-wide + /* Prefer the per-connection context (if any) over the ambient * default, so different connections in one process can present * different client certs from the same shared NSS DB. Pass "arg" * (this connection) through as wincx, so nss_password_callback() * can likewise resolve a per-connection password. */ - const char *certname = (arg && arg->certident_name) ? arg->certident_name : sslcertname; + upscli_ssl_context_config_t *cfg = arg ? (upscli_ssl_context_config_t *)arg->ssl_ctx : NULL; + const char *certname; + + if (!cfg) { + cfg = default_ssl_context_cfg; + } + certname = cfg ? cfg->certname : NULL; if (certname != NULL) { cert = PK11_FindCertFromNickname(certname, arg); @@ -941,126 +984,129 @@ int upscli_authconf_update_conn_flags(const upscli_authconf_t *ac, int *flags) return 1; } -/** Initialize SSL support with specific requirements. - * Call this or a related method before upscli_sslinit() to initiate STARTTLS - * in a connection to the server. - * - * Legacy API, without support for client's own certificate in OpenSSL builds. - * - * @see upscli_init_authconf() - * @see upscli_init2() - * @see upscli_sslinit() - * @see upscli_connect() - * @see upscli_tryconnect() - */ -int upscli_init(int certverify, const char *certpath, - const char *certname, const char *certpasswd) +#if defined(WITH_OPENSSL) || defined(WITH_NSS) +/* Zero out a secret/sensitive string in place (up to its current length) + * before freeing it, so it does not linger readable in freed heap memory. */ +static void upscli_wipe_free_str(char **str) { - return upscli_init2(certverify, certpath, certname, certpasswd, NULL); + if (str && *str) { + memset(*str, 0, strlen(*str)); + free(*str); + *str = NULL; + } } -/** Initialize SSL support with specific requirements. - * Call this or a related method before upscli_sslinit() to initiate STARTTLS - * in a connection to the server. - * - * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods - * will invert who is implementation of whom (the other being a wrapper). - * - * TODO: Consider a method that parses our collection from - * upscli_get_authconf_list() to upscli_add_host_port_cert() and - * set up the one most applicable set of client identity data - * for that [user@host:port] combo. - * - * @see upscli_init2() - * @see upscli_init() - * @see upscli_sslinit() - * @see upscli_connect() - * @see upscli_tryconnect() - */ -int upscli_init_authconf(upscli_authconf_t *ac) +static int upscli_str_eq_nullable(const char *a, const char *b) { - if (!ac) { - upsdebugx(1, "%s: SKIP: NULL authconf pointer", __func__); - return -1; + if (a == b) { + return 1; + } + if (!a || !b) { + return 0; } + return strcmp(a, b) == 0; +} - upsdebugx(5, "%s: got an authconf pointer", __func__); - if (nut_debug_level > 5) { - upscli_dump_authconf_item(stderr, ac, 1, 0); +static void upscli_ssl_context_config_free(upscli_ssl_context_config_t *cfg) +{ + if (!cfg) { + return; } - if (ac->certhost && ac->section) { - const char *host_port = strchr(ac->section, '@'); +#ifdef WITH_OPENSSL + if (cfg->ssl_ctx) { + SSL_CTX_free(cfg->ssl_ctx); + cfg->ssl_ctx = NULL; + } +#endif - if (!host_port) { - host_port = ac->section; - } else { - host_port++; - } + /* Sensitive material first, then names/paths which are less secret + * but still worth scrubbing rather than just free()ing verbatim. */ + upscli_wipe_free_str(&cfg->certpasswd); + upscli_wipe_free_str(&cfg->certname); + upscli_wipe_free_str(&cfg->certpath); + upscli_wipe_free_str(&cfg->certfile); - upscli_add_host_cert(host_port, ac->certhost, ac->certverify, ac->forcessl); + free(cfg); +} + +static upscli_ssl_context_config_t *upscli_ssl_context_config_find( + int certverify, const char *certpath, const char *certname, + const char *certpasswd, const char *certfile) +{ + upscli_ssl_context_config_t *cfg; + + for (cfg = ssl_context_registry; cfg; cfg = cfg->next) { + if (cfg->certverify == certverify + && upscli_str_eq_nullable(cfg->certpath, certpath) + && upscli_str_eq_nullable(cfg->certname, certname) + && upscli_str_eq_nullable(cfg->certpasswd, certpasswd) + && upscli_str_eq_nullable(cfg->certfile, certfile) + ) { + return cfg; + } } - return upscli_init2(ac->certverify, ac->certpath, ac->certident, ac->certpasswd, ac->certfile); + return NULL; } +#endif /* WITH_OPENSSL | WITH_NSS */ -/** Initialize SSL support with specific requirements. - * Call this or a related method before upscli_sslinit() to initiate STARTTLS - * in a connection to the server. - * - * Unlike legacy upscli_init() this method allows support for client's own - * certificate in OpenSSL builds (as well as NSS builds available before it). +/** Look up (by exact argument match) or build a new SSL/crypto context + * configuration (CA bundle, client cert identity, verify mode), caching it + * in a process-wide registry so repeat calls with the same arguments are + * cheap and return the same handle. The returned opaque handle can be + * attached to a connection via upscli_set_ssl_context(); if never attached + * to any connection, it still becomes the ambient default the first time + * (see upscli_init2()). * - * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods - * will invert who is implementation of whom (the other being a wrapper). + * NOTE: For NSS builds, the trust database/policy set up by NSS_Init() et al + * is process-global and can only be established once (upstream library + * constraint); a later call with a different certpath can not actually + * change that global trust database and only logs a warning about it. + * What IS genuinely per-connection for NSS is the client certificate identity + * (certname/certpasswd), consulted by GetClientAuthData()/nss_password_callback() + * for whichever context is attached to a given connection (or the ambient + * default if none was explicitly attached). To work with multiple trusted + * certificate authorities in one process, the sysadmin should prepare a + * single NSS DB with all of them. * - * @see upscli_init_authconf() - * @see upscli_init() - * @see upscli_sslinit() - * @see upscli_connect() - * @see upscli_tryconnect() + * Returns an opaque handle on success, or NULL on hard failure. */ -int upscli_init2(int certverify, const char *certpath, - const char *certname, const char *certpasswd, - const char *certfile) +void *upscli_get_or_create_ssl_context(int certverify, const char *certpath, + const char *certname, const char *certpasswd, const char *certfile) { - const char *quiet_init_ssl; -#ifdef WITH_OPENSSL - long ret; - int ssl_mode = SSL_VERIFY_NONE; -#elif defined(WITH_NSS) /* WITH_OPENSSL */ - SECStatus status; -#endif /* WITH_OPENSSL | WITH_NSS */ - -#if defined(WITH_OPENSSL) || defined(WITH_NSS) - if (certname) { - free(sslcertname); - sslcertname = xstrdup(certname); - } - if (certpasswd) { - free(sslcertpasswd); - sslcertpasswd = xstrdup(certpasswd); - } -#else /* neither backend: */ - /* See comment above */ +#if !(defined(WITH_OPENSSL) || defined(WITH_NSS)) NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(certpath); NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certpasswd); NUT_UNUSED_VARIABLE(certfile); -#endif /* WITH_OPENSSL | WITH_NSS */ + upslogx(LOG_ERR, "%s called but SSL wasn't compiled in", __func__); + return NULL; +#else + upscli_ssl_context_config_t *cfg; + const char *quiet_init_ssl; +# ifdef WITH_OPENSSL + long ret; + int ssl_mode = SSL_VERIFY_NONE; +# elif defined(WITH_NSS) /* WITH_OPENSSL */ + SECStatus status; +# endif /* WITH_OPENSSL | WITH_NSS */ - if (upscli_initialized == 1) { - upslogx(LOG_WARNING, "upscli already initialized"); - return -1; + cfg = upscli_ssl_context_config_find(certverify, certpath, certname, certpasswd, certfile); + if (cfg) { + cfg->refcount++; + upsdebugx(2, "%s: reusing cached SSL context configuration", __func__); + return cfg; } - if (upscli_default_connect_timeout_initialized == 0) { - /* There may be an envvar waiting to be parsed */ - upsdebugx(1, "%s: upscli_default_connect_timeout was not initialized, checking now", - __func__); - upscli_init_default_connect_timeout(NULL, NULL, NULL); - } + cfg = (upscli_ssl_context_config_t *)xcalloc(1, sizeof(*cfg)); + cfg->certverify = certverify; + cfg->certpath = certpath ? xstrdup(certpath) : NULL; + cfg->certname = certname ? xstrdup(certname) : NULL; + cfg->certpasswd = certpasswd ? xstrdup(certpasswd) : NULL; + cfg->certfile = certfile ? xstrdup(certfile) : NULL; + cfg->refcount = 1; quiet_init_ssl = getenv("NUT_QUIET_INIT_SSL"); if (quiet_init_ssl != NULL) { @@ -1077,41 +1123,42 @@ int upscli_init2(int certverify, const char *certpath, } } -#ifdef WITH_OPENSSL +# ifdef WITH_OPENSSL -# if OPENSSL_VERSION_NUMBER < 0x10100000L +# if OPENSSL_VERSION_NUMBER < 0x10100000L SSL_load_error_strings(); SSL_library_init(); - ssl_ctx = SSL_CTX_new(SSLv23_client_method()); -# else - ssl_ctx = SSL_CTX_new(TLS_client_method()); -# endif + cfg->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); +# else + cfg->ssl_ctx = SSL_CTX_new(TLS_client_method()); +# endif - if (!ssl_ctx) { + if (!cfg->ssl_ctx) { upslogx(LOG_ERR, "Can not initialize SSL context"); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } -# if OPENSSL_VERSION_NUMBER < 0x10100000L +# if OPENSSL_VERSION_NUMBER < 0x10100000L /* set minimum protocol TLSv1 */ - SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); -# else - ret = SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_VERSION); + SSL_CTX_set_options(cfg->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); +# else + ret = SSL_CTX_set_min_proto_version(cfg->ssl_ctx, TLS1_VERSION); if (ret != 1) { upslogx(LOG_ERR, "Can not set minimum protocol to TLSv1"); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } -# endif +# endif if (!certpath) { if (certverify == 1) { upslogx(LOG_ERR, "Can not verify certificate if any is specified: no CERTPATH was given"); /* Failed: checking the server cert is mandatory, but no * collection of trusted CA/server cert files was given */ - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } } else { switch (certverify) @@ -1124,17 +1171,17 @@ int upscli_init2(int certverify, const char *certpath, break; } - ret = SSL_CTX_load_verify_locations(ssl_ctx, NULL, certpath); + ret = SSL_CTX_load_verify_locations(cfg->ssl_ctx, NULL, certpath); if (ret != 1) { ssl_debug(); upsdebugx(1, "%s: Failed to load CA certificate(s) from directory %s", __func__, certpath); /* Can it be a specific PEM file? */ - if ((ret = SSL_CTX_load_verify_locations(ssl_ctx, certpath, NULL)) != 1) { + if ((ret = SSL_CTX_load_verify_locations(cfg->ssl_ctx, certpath, NULL)) != 1) { ssl_debug(); upslogx(LOG_ERR, "Failed to load CA certificate(s) from directory or file %s", certpath); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } else { upsdebugx(1, "%s: ...but succeeded to load CA certificate(s) from file %s", __func__, certpath); } @@ -1147,22 +1194,22 @@ int upscli_init2(int certverify, const char *certpath, "openssl_cert_verify_data index (client)", NULL, NULL, NULL); - SSL_CTX_set_verify(ssl_ctx, ssl_mode, openssl_cert_verify_callback); + SSL_CTX_set_verify(cfg->ssl_ctx, ssl_mode, openssl_cert_verify_callback); /* Let the openssl_cert_verify_callback() catch any verify_depth * error, so that we get an appropriate error in the logfile; * see more around SSL_connect(). */ - SSL_CTX_set_verify_depth(ssl_ctx, verify_depth + 1); + SSL_CTX_set_verify_depth(cfg->ssl_ctx, verify_depth + 1); } - if (sslcertpasswd) { -# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB + if (cfg->certpasswd) { +# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB /* Roughly OpenSSL 1.1.0+ or 1.0.2+ with patched distros */ - SSL_CTX_set_default_passwd_cb(ssl_ctx, openssl_password_callback); -# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA - SSL_CTX_set_default_passwd_cb_userdata(ssl_ctx, (void*)sslcertpasswd); -# endif /* else callback uses global variable */ -# else /* Not SSL_CTX_* methods */ + SSL_CTX_set_default_passwd_cb(cfg->ssl_ctx, openssl_password_callback); +# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA + SSL_CTX_set_default_passwd_cb_userdata(cfg->ssl_ctx, (void*)cfg->certpasswd); +# endif /* else callback uses global variable */ +# else /* Not SSL_CTX_* methods */ /* Per https://docs.openssl.org/3.5/man3/SSL_CTX_set_default_passwd_cb, * the `SSL_CTX*` variants were added in 1.1. * The SSL_set_default_passwd_cb() and SSL_set_default_passwd_cb_userdata() @@ -1175,171 +1222,188 @@ int upscli_init2(int certverify, const char *certpath, * * Alternately load PEM "manually", see e.g. Apache httpd sources before 2015. */ -# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_SET_DEFAULT_PASSWD_CB +# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_SET_DEFAULT_PASSWD_CB /* Theoretical solution - didn't find a build system where such methods * would actually be available, so this could be tested and used */ - SSL *ssl_tmp = SSL_new(ssl_ctx); + SSL *ssl_tmp = SSL_new(cfg->ssl_ctx); /* OpenSSL 0.9.6+ at least? */ SSL_set_default_passwd_cb(ssl_tmp, openssl_password_callback); -# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA - SSL_set_default_passwd_cb_userdata(ssl_tmp, (void*)sslcertpasswd); -# endif +# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA + SSL_set_default_passwd_cb_userdata(ssl_tmp, (void*)cfg->certpasswd); +# endif SSL_free(ssl_tmp); -# else /* Not SSL_* methods either */ +# else /* Not SSL_* methods either */ upslogx(LOG_ERR, "Private key password support not implemented for OpenSSL < ~0.9.6..~1.1 yet"); - upscli_cleanup(); - return -1; -# endif -# endif /* ...SET_DEFAULT_PASSWD_CB */ + upscli_ssl_context_config_free(cfg); + return NULL; +# endif +# endif /* ...SET_DEFAULT_PASSWD_CB */ } /* else: CERTIDENT did not pass a password, nothing to check */ - if (certfile) { + if (cfg->certfile) { /* Note: same certfile PEM for cert and private key, - * which is optionally protected by sslcertpasswd */ + * which is optionally protected by cfg->certpasswd */ int ssl_ret; - if ((ssl_ret = SSL_CTX_use_certificate_chain_file(ssl_ctx, certfile)) != 1) { - upslogx(LOG_ERR, "Failed to load client certificate from %s", certfile); + if ((ssl_ret = SSL_CTX_use_certificate_chain_file(cfg->ssl_ctx, cfg->certfile)) != 1) { + upslogx(LOG_ERR, "Failed to load client certificate from %s", cfg->certfile); ssl_debug(); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } - if ((ssl_ret = SSL_CTX_use_PrivateKey_file(ssl_ctx, certfile, SSL_FILETYPE_PEM)) != 1) { - upslogx(LOG_ERR, "Failed to load client private key from %s", certfile); + if ((ssl_ret = SSL_CTX_use_PrivateKey_file(cfg->ssl_ctx, cfg->certfile, SSL_FILETYPE_PEM)) != 1) { + upslogx(LOG_ERR, "Failed to load client private key from %s", cfg->certfile); ssl_debug(); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } - if ((ssl_ret = SSL_CTX_check_private_key(ssl_ctx)) != 1) { - upslogx(LOG_ERR, "Failed to check client private key from %s", certfile); + if ((ssl_ret = SSL_CTX_check_private_key(cfg->ssl_ctx)) != 1) { + upslogx(LOG_ERR, "Failed to check client private key from %s", cfg->certfile); ssl_debug(); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } - if (sslcertname && *sslcertname) { -# if (defined(HAVE_SSL_CTX_GET0_CERTIFICATE) && HAVE_SSL_CTX_GET0_CERTIFICATE) && (defined(HAVE_X509_CHECK_HOST) && HAVE_X509_CHECK_HOST) && (defined(HAVE_X509_CHECK_IP_ASC) && HAVE_X509_CHECK_IP_ASC) && (defined(HAVE_X509_NAME_ONELINE) && HAVE_X509_NAME_ONELINE) + if (cfg->certname && *cfg->certname) { +# if (defined(HAVE_SSL_CTX_GET0_CERTIFICATE) && HAVE_SSL_CTX_GET0_CERTIFICATE) && (defined(HAVE_X509_CHECK_HOST) && HAVE_X509_CHECK_HOST) && (defined(HAVE_X509_CHECK_IP_ASC) && HAVE_X509_CHECK_IP_ASC) && (defined(HAVE_X509_NAME_ONELINE) && HAVE_X509_NAME_ONELINE) /* Roughly OpenSSL 1.0.2+ */ - X509 *x509 = SSL_CTX_get0_certificate(ssl_ctx); + X509 *x509 = SSL_CTX_get0_certificate(cfg->ssl_ctx); if (x509) { - /* Check if sslcertname matches the host (CN or SAN) */ - if (X509_check_host(x509, (const char *)sslcertname, 0, 0, NULL) != 1 - && X509_check_ip_asc(x509, (const char *)sslcertname, 0) != 1 + /* Check if cfg->certname matches the host (CN or SAN) */ + if (X509_check_host(x509, (const char *)cfg->certname, 0, 0, NULL) != 1 + && X509_check_ip_asc(x509, (const char *)cfg->certname, 0) != 1 ) { char *subject = X509_NAME_oneline(X509_get_subject_name(x509), NULL, 0); char *subject_CN = (subject ? (char*)strstr(subject, "CN=") + 3 : NULL); - size_t sslcertname_len = strlen(sslcertname); + size_t certname_len = strlen(cfg->certname); upsdebugx(4, "%s: My certificate subject: '%s'; CN: '%s'; CERTIDENT: [%" PRIuSIZE "]'%s'", __func__, NUT_STRARG(subject), NUT_STRARG(subject_CN), - sslcertname_len, NUT_STRARG(sslcertname)); + certname_len, NUT_STRARG(cfg->certname)); - /* Check if sslcertname matches the whole subject or just .../CN=.../ part as a string */ + /* Check if cfg->certname matches the whole subject or just .../CN=.../ part as a string */ if (!subject || !( - strcmp(subject, sslcertname) == 0 - || (subject_CN && !strncmp(subject_CN, sslcertname, sslcertname_len) - && (subject_CN[sslcertname_len] == '\0' - || subject_CN[sslcertname_len] == '/' - || subject_CN[sslcertname_len] == ',' - || (subject_CN[sslcertname_len] == '\\' && subject_CN[sslcertname_len + 1] == '/')) ) + strcmp(subject, cfg->certname) == 0 + || (subject_CN && !strncmp(subject_CN, cfg->certname, certname_len) + && (subject_CN[certname_len] == '\0' + || subject_CN[certname_len] == '/' + || subject_CN[certname_len] == ',' + || (subject_CN[certname_len] == '\\' && subject_CN[certname_len + 1] == '/')) ) )) { /* This way or that, the names differ */ upslogx(LOG_ERR, "Certificate subject (%s) does not match CERTIDENT name (%s)", - subject ? subject : "unknown", sslcertname); + subject ? subject : "unknown", cfg->certname); if (subject) { OPENSSL_free(subject); } upslogx(LOG_ERR, "Unexpected certificate provided"); - upscli_cleanup(); - return -1; + upscli_ssl_context_config_free(cfg); + return NULL; } else { - upsdebugx(2, "Certificate subject verified against CERTIDENT subject name (%s)", sslcertname); + upsdebugx(2, "Certificate subject verified against CERTIDENT subject name (%s)", cfg->certname); } } else { - upsdebugx(2, "Certificate subject verified against CERTIDENT host name (%s)", sslcertname); + upsdebugx(2, "Certificate subject verified against CERTIDENT host name (%s)", cfg->certname); } } -# else /* Missing X509 methods wanted above */ - upslogx(LOG_ERR, "Can not verify CERTIDENT '%s': not supported in this OpenSSL build (too old)", sslcertname); - upscli_cleanup(); - return -1; -# endif /* Got ways to check CERTIDENT? */ +# else /* Missing X509 methods wanted above */ + upslogx(LOG_ERR, "Can not verify CERTIDENT '%s': not supported in this OpenSSL build (too old)", cfg->certname); + upscli_ssl_context_config_free(cfg); + return NULL; +# endif /* Got ways to check CERTIDENT? */ } /* else: CERTIDENT did not pass a name, nothing to check */ } else { - if (sslcertname && *sslcertname) { - upslogx(LOG_ERR, "Can not verify CERTIDENT '%s': no CERTFILE was provided", sslcertname); - upscli_cleanup(); - return -1; + if (cfg->certname && *cfg->certname) { + upslogx(LOG_ERR, "Can not verify CERTIDENT '%s': no CERTFILE was provided", cfg->certname); + upscli_ssl_context_config_free(cfg); + return NULL; } } -#elif defined(WITH_NSS) /* WITH_OPENSSL */ +# elif defined(WITH_NSS) /* WITH_OPENSSL */ - PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); - nss_initialized = 1; + if (!nss_initialized) { + PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); + nss_initialized = 1; - PK11_SetPasswordFunc(nss_password_callback); + PK11_SetPasswordFunc(nss_password_callback); - if (certfile) { - upsdebugx(1, "%s: certfile is not used for NSS init, ignored", __func__); - } + if (certfile) { + upsdebugx(1, "%s: certfile is not used for NSS init, ignored", __func__); + } - if (certpath) { - if (quiet_init_ssl != NULL) { - upsdebugx(1, "Init SSL with certificate database located at %s", certpath); + if (certpath) { + if (quiet_init_ssl != NULL) { + upsdebugx(1, "Init SSL with certificate database located at %s", certpath); + } else { + upslogx(LOG_INFO, "Init SSL with certificate database located at %s", certpath); + } + status = NSS_Init(certpath); + nss_first_certpath = xstrdup(certpath); } else { - upslogx(LOG_INFO, "Init SSL with certificate database located at %s", certpath); + if (quiet_init_ssl != NULL) { + upsdebugx(1, "Init SSL without certificate database"); + } else { + upslogx(LOG_NOTICE, "Init SSL without certificate database"); + } + status = NSS_NoDB_Init(NULL); } - status = NSS_Init(certpath); - } else { - if (quiet_init_ssl != NULL) { - upsdebugx(1, "Init SSL without certificate database"); - } else { - upslogx(LOG_NOTICE, "Init SSL without certificate database"); + if (status != SECSuccess) { + upslogx(LOG_ERR, "Can not initialize SSL context"); + nss_error("upscli_get_or_create_ssl_context / NSS_[NoDB]_Init"); + upscli_ssl_context_config_free(cfg); + return NULL; } - status = NSS_NoDB_Init(NULL); - } - if (status != SECSuccess) { - upslogx(LOG_ERR, "Can not initialize SSL context"); - nss_error("upscli_init / NSS_[NoDB]_Init"); - upscli_cleanup(); - return -1; - } - status = NSS_SetDomesticPolicy(); - if (status != SECSuccess) { - upslogx(LOG_ERR, "Can not initialize SSL policy"); - nss_error("upscli_init / NSS_SetDomesticPolicy"); - upscli_cleanup(); - return -1; - } + status = NSS_SetDomesticPolicy(); + if (status != SECSuccess) { + upslogx(LOG_ERR, "Can not initialize SSL policy"); + nss_error("upscli_get_or_create_ssl_context / NSS_SetDomesticPolicy"); + upscli_ssl_context_config_free(cfg); + return NULL; + } - SSL_ClearSessionCache(); + SSL_ClearSessionCache(); - status = SSL_OptionSetDefault(SSL_ENABLE_SSL3, PR_TRUE); - if (status != SECSuccess) { - upslogx(LOG_ERR, "Can not enable SSLv3"); - nss_error("upscli_init / SSL_OptionSetDefault(SSL_ENABLE_SSL3)"); - upscli_cleanup(); - return -1; - } - status = SSL_OptionSetDefault(SSL_ENABLE_TLS, PR_TRUE); - if (status != SECSuccess) { - upslogx(LOG_ERR, "Can not enable TLSv1"); - nss_error("upscli_init / SSL_OptionSetDefault(SSL_ENABLE_TLS)"); - upscli_cleanup(); - return -1; - } - status = SSL_OptionSetDefault(SSL_V2_COMPATIBLE_HELLO, PR_FALSE); - if (status != SECSuccess) { - upslogx(LOG_ERR, "Can not disable SSLv2 hello compatibility"); - nss_error("upscli_init / SSL_OptionSetDefault(SSL_V2_COMPATIBLE_HELLO)"); - upscli_cleanup(); - return -1; + status = SSL_OptionSetDefault(SSL_ENABLE_SSL3, PR_TRUE); + if (status != SECSuccess) { + upslogx(LOG_ERR, "Can not enable SSLv3"); + nss_error("upscli_get_or_create_ssl_context / SSL_OptionSetDefault(SSL_ENABLE_SSL3)"); + upscli_ssl_context_config_free(cfg); + return NULL; + } + status = SSL_OptionSetDefault(SSL_ENABLE_TLS, PR_TRUE); + if (status != SECSuccess) { + upslogx(LOG_ERR, "Can not enable TLSv1"); + nss_error("upscli_get_or_create_ssl_context / SSL_OptionSetDefault(SSL_ENABLE_TLS)"); + upscli_ssl_context_config_free(cfg); + return NULL; + } + status = SSL_OptionSetDefault(SSL_V2_COMPATIBLE_HELLO, PR_FALSE); + if (status != SECSuccess) { + upslogx(LOG_ERR, "Can not disable SSLv2 hello compatibility"); + nss_error("upscli_get_or_create_ssl_context / SSL_OptionSetDefault(SSL_V2_COMPATIBLE_HELLO)"); + upscli_ssl_context_config_free(cfg); + return NULL; + } + verify_certificate = certverify; + } else { + /* NSS trust database and policy are process-global and can only + * be set up once; this additional context can still carry its + * own client certificate identity (certname/certpasswd), which + * IS genuinely per-connection for NSS (see GetClientAuthData()). */ + if (certfile) { + upsdebugx(1, "%s: certfile is not used for NSS, ignored", __func__); + } + if (certpath && !upscli_str_eq_nullable(certpath, nss_first_certpath)) { + upslogx(LOG_WARNING, "NSS trust database is process-global and was already " + "initialized with a different CERTPATH; ignoring CERTPATH for this " + "additional SSL context (only its client certificate identity, if any, " + "will be honored per-connection)"); + } } - verify_certificate = certverify; -#else +# else /* Note: historically we do not return with error here, * and nowadays have the default timeout handling etc., * just fall through to below and treat as initialized. @@ -1348,14 +1412,166 @@ int upscli_init2(int certverify, const char *certpath, if (certverify || certpath || certname || certpasswd || certfile) { upslogx(LOG_ERR, "upscli_init called but SSL wasn't compiled in"); } -#endif /* WITH_OPENSSL | WITH_NSS */ +# endif /* WITH_OPENSSL | WITH_NSS */ - upscli_initialized = 1; + cfg->next = ssl_context_registry; + ssl_context_registry = cfg; + + upsdebugx(1, "%s: completed (new SSL context configuration cached)", __func__); + return cfg; +#endif /* WITH_OPENSSL | WITH_NSS */ +} + +/** Equivalent of upscli_get_or_create_ssl_context() taking parameters from an + * upscli_authconf_t, also registering any CERTHOST setting it carries (like + * upscli_init_authconf() does). Returns an opaque handle, or NULL on error. */ +void *upscli_get_or_create_ssl_context_authconf(upscli_authconf_t *ac) +{ + if (!ac) { + upsdebugx(1, "%s: SKIP: NULL authconf pointer", __func__); + return NULL; + } - upsdebugx(1, "%s: completed", __func__); + upsdebugx(5, "%s: got an authconf pointer", __func__); + if (nut_debug_level > 5) { + upscli_dump_authconf_item(stderr, ac, 1, 0); + } + + if (ac->certhost && ac->section) { + const char *host_port = strchr(ac->section, '@'); + + if (!host_port) { + host_port = ac->section; + } else { + host_port++; + } + + upscli_add_host_cert(host_port, ac->certhost, ac->certverify, ac->forcessl); + } + + return upscli_get_or_create_ssl_context(ac->certverify, ac->certpath, ac->certident, ac->certpasswd, ac->certfile); +} + +/** Shared tail of upscli_init2()/upscli_init_authconf(): given a context handle + * (or NULL on failure to build one), decide the legacy-compatible return code. + * @return + * - 1 : success, and this call's context is (now, or already was) the ambient + * default used by connections that never call upscli_set_ssl_context() + * - 0 : success, but a DIFFERENT default was already set by an earlier call; + * this context was cached (or found) and is retrievable via + * upscli_get_or_create_ssl_context()/_authconf(), but was not made + * the ambient default + * - -1 : hard failure (bad arguments, or the backend failed to build it) + */ +static int upscli_init2_finish(void *cfgv) +{ +#if defined(WITH_OPENSSL) || defined(WITH_NSS) + upscli_ssl_context_config_t *cfg = (upscli_ssl_context_config_t *)cfgv; + + if (!cfg) { + return -1; + } + + if (!default_ssl_context_cfg) { + default_ssl_context_cfg = cfg; + upscli_initialized = 1; + upsdebugx(1, "%s: completed (new ambient default)", __func__); + return 1; + } + + if (cfg == default_ssl_context_cfg) { + upsdebugx(1, "%s: completed (matches existing ambient default)", __func__); + return 1; + } + + upsdebugx(1, "%s: completed (new cached context, ambient default unchanged)", __func__); + return 0; +#else + upscli_initialized = 1; + NUT_UNUSED_VARIABLE(cfgv); return 1; +#endif +} + +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * Legacy API, without support for client's own certificate in OpenSSL builds. + * + * @see upscli_init_authconf() + * @see upscli_init2() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ +int upscli_init(int certverify, const char *certpath, + const char *certname, const char *certpasswd) +{ + return upscli_init2(certverify, certpath, certname, certpasswd, NULL); } +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods + * will invert who is implementation of whom (the other being a wrapper). + * + * TODO: Consider a method that parses our collection from + * upscli_get_authconf_list() to upscli_add_host_port_cert() and + * set up the one most applicable set of client identity data + * for that [user@host:port] combo. + * + * @see upscli_init2() + * @see upscli_init() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ +int upscli_init_authconf(upscli_authconf_t *ac) +{ + if (upscli_default_connect_timeout_initialized == 0) { + /* There may be an envvar waiting to be parsed */ + upsdebugx(1, "%s: upscli_default_connect_timeout was not initialized, checking now", + __func__); + upscli_init_default_connect_timeout(NULL, NULL, NULL); + } + + return upscli_init2_finish(upscli_get_or_create_ssl_context_authconf(ac)); +} + +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * Unlike legacy upscli_init() this method allows support for client's own + * certificate in OpenSSL builds (as well as NSS builds available before it). + * + * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods + * will invert who is implementation of whom (the other being a wrapper). + * + * @see upscli_init_authconf() + * @see upscli_init() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ +int upscli_init2(int certverify, const char *certpath, + const char *certname, const char *certpasswd, + const char *certfile) +{ + if (upscli_default_connect_timeout_initialized == 0) { + /* There may be an envvar waiting to be parsed */ + upsdebugx(1, "%s: upscli_default_connect_timeout was not initialized, checking now", + __func__); + upscli_init_default_connect_timeout(NULL, NULL, NULL); + } + + return upscli_init2_finish(upscli_get_or_create_ssl_context(certverify, certpath, certname, certpasswd, certfile)); +} + + static uint16_t get_port_from_string(const char *str_port) { uint16_t retval = 0; @@ -1703,38 +1919,23 @@ void *upscli_get_ssl_context(UPSCONN_t *ups) return ups->ssl_ctx; } -int upscli_set_ssl_certident(UPSCONN_t *ups, const char *certident_name, const char *certident_pass) -{ - if (!ups) { - return -1; - } - - free(ups->certident_name); - ups->certident_name = certident_name ? xstrdup(certident_name) : NULL; - - free(ups->certident_pass); - ups->certident_pass = certident_pass ? xstrdup(certident_pass) : NULL; - - return 0; -} - -const char *upscli_get_ssl_certident_name(UPSCONN_t *ups) -{ - if (!ups) { - return NULL; - } - - return ups->certident_name; -} - int upscli_cleanup(void) { -#ifdef WITH_OPENSSL - if (ssl_ctx) { - SSL_CTX_free(ssl_ctx); - ssl_ctx = NULL; +#if defined(WITH_OPENSSL) || defined(WITH_NSS) + /* Free the whole SSL context registry (assumes all connections were + * already disconnected, so nothing still refers to these entries). */ + { + upscli_ssl_context_config_t *cfg = ssl_context_registry, *next; + + while (cfg) { + next = cfg->next; + upscli_ssl_context_config_free(cfg); + cfg = next; + } + ssl_context_registry = NULL; + default_ssl_context_cfg = NULL; } -#endif /* WITH_OPENSSL */ +#endif /* WITH_OPENSSL | WITH_NSS */ #ifdef WITH_NSS /* Avoid first calling NSS to shut it down - this confuses @@ -1753,19 +1954,13 @@ int upscli_cleanup(void) PL_ArenaFinish(); nss_initialized = 0; } + + upscli_wipe_free_str(&nss_first_certpath); #endif /* WITH_NSS */ upscli_free_host_cert_list(); upscli_free_authconf_list(); -#if defined(WITH_OPENSSL) || defined(WITH_NSS) - free(sslcertname); - sslcertname = NULL; - - free(sslcertpasswd); - sslcertpasswd = NULL; -#endif - upscli_initialized = 0; return 1; } @@ -2269,15 +2464,23 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) # ifdef WITH_OPENSSL - if (ups->ssl_ctx) { - upsdebugx(3, "%s: Using per-connection SSL context", __func__); - } else /* try using global default SSL context (legacy-compatible) */ - if (!ssl_ctx) { - upsdebugx(3, "%s: SSL context is not available", __func__); - return 0; - } + { + upscli_ssl_context_config_t *cfg = (upscli_ssl_context_config_t *)ups->ssl_ctx; + + if (cfg) { + upsdebugx(3, "%s: Using per-connection SSL context", __func__); + } else { + /* try using the ambient default SSL context (legacy-compatible) */ + cfg = default_ssl_context_cfg; + } - ups->ssl = SSL_new((SSL_CTX *)(ups->ssl_ctx ? ups->ssl_ctx : ssl_ctx)); + if (!cfg || !cfg->ssl_ctx) { + upsdebugx(3, "%s: SSL context is not available", __func__); + return 0; + } + + ups->ssl = SSL_new(cfg->ssl_ctx); + } if (!ups->ssl) { upsdebugx(3, "%s: Can not create SSL socket", __func__); return 0; @@ -3464,12 +3667,6 @@ int upscli_disconnect(UPSCONN_t *ups) free(ups->host); ups->host = NULL; - free(ups->certident_name); - ups->certident_name = NULL; - - free(ups->certident_pass); - ups->certident_pass = NULL; - #ifdef WITH_OPENSSL if (ups->openssl_cert_verify_data != NULL) { if (ups->openssl_cert_verify_data->hostname_allocated @@ -3516,17 +3713,17 @@ int upscli_disconnect(UPSCONN_t *ups) ups->ssl = NULL; } - if (ups->ssl_ctx && ups->ssl_ctx_owned) { - SSL_CTX_free(ups->ssl_ctx); - ups->ssl_ctx = NULL; - ups->ssl_ctx_owned = 0; - } + /* ups->ssl_ctx is a reference into the SSL context registry (shared, + * possibly reused by other connections); it is owned and freed by + * upscli_cleanup(), not per-connection - just drop our reference. */ + ups->ssl_ctx = NULL; #elif defined(WITH_NSS) /* !WITH_OPENSSL */ if (ups->ssl) { PR_Shutdown(ups->ssl, PR_SHUTDOWN_BOTH); PR_Close(ups->ssl); ups->ssl = NULL; } + ups->ssl_ctx = NULL; #endif /* WITH_OPENSSL | WITH_NSS */ shutdown(ups->fd, shutdown_how); diff --git a/clients/upsclient.h b/clients/upsclient.h index 145bcd562e..5d6e4d33f4 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -114,31 +114,19 @@ typedef struct { /* WARNING for maintainers/devs: keep the ifdef'ed struct sizes * same for different builds, and add new data items in the end! */ - /* SSL context (trusted CA, own cert, etc.) may be global (NULL here) - * or shared (owned by us or reference to an instance owned and freed - * elsewhere). This allows the same client to connect to multiple - * data servers whose crypto is under different management realms. - * - * NOTE: OpenSSL has this concept, Mozilla NSS currently does not - * (its context is process-wide via NSS_Init() callable once). - */ + /* SSL context configuration (trusted CA, client cert identity, verify mode): + * NULL means use the ambient default set by upscli_init*(); a non-NULL + * opaque pointer (obtained via upscli_get_or_create_ssl_context()) attaches + * a cached context config to this connection for per-connection client cert + * identity on NSS, or per-connection SSL_CTX on OpenSSL. The context is + * shared and NOT owned by this connection; it is freed only by upscli_cleanup(). */ #ifdef WITH_OPENSSL openssl_cert_verify_data_t *openssl_cert_verify_data; - SSL_CTX *ssl_ctx; + void *ssl_ctx; /* really: (upscli_ssl_context_config_t*) - opaque handle */ #else void *extra_reserved; - void *ssl_ctx; /* essentially padding for struct size in different build variants */ + void *ssl_ctx; /* padding for struct size in different build variants */ #endif /* WITH_OPENSSL */ - char ssl_ctx_owned; /* if not 0, we own the SSL_CTX and should free it on cleanup (if applicable) - meaning nobody else refers to that memory */ - - /* Optional per-connection client certificate identity override, consulted - * (at least) by the NSS backend's GetClientAuthData()/nss_password_callback() - * so one process can present different client certificates to different - * servers even while sharing one process-wide NSS certificate/key database. - * NULL means fall back to the library-wide CERTIDENT set via upscli_init*(). - * See upscli_set_ssl_certident(). */ - char *certident_name; - char *certident_pass; } UPSCONN_t; @@ -192,18 +180,17 @@ int upscli_cleanup(void); void *upscli_set_ssl_context(UPSCONN_t *ups, void *ssl_ctx); void *upscli_get_ssl_context(UPSCONN_t *ups); -/* Set (or clear, with NULL args) a per-connection client certificate identity, - * to let one process present different client certificates to different - * servers even when they share one process-wide NSS certificate/key database - * (OpenSSL builds should prefer a distinct SSL context via upscli_set_ssl_context() - * instead, since OpenSSL supports a fully separate SSL_CTX per connection). - * Strings are copied internally; safe to free/reuse the arguments afterwards. - * Returns 0 on success, -1 on error (e.g. NULL ups). */ -int upscli_set_ssl_certident(UPSCONN_t *ups, const char *certident_name, const char *certident_pass); - -/* Get the per-connection client certificate nickname set via - * upscli_set_ssl_certident(), or NULL if none was set for this connection. */ -const char *upscli_get_ssl_certident_name(UPSCONN_t *ups); +/* Get or create a cached SSL context configuration (CA bundle, client cert + * identity, verify mode) from the process-wide registry. Takes parameters + * identical to upscli_init2(). Returns an opaque handle for use with + * upscli_set_ssl_context(), or NULL on hard failure. Cheap on repeat calls + * with identical arguments (cached hit). See design notes in SSL_CONTEXT_ANALYSIS.* */ +void *upscli_get_or_create_ssl_context(int certverify, const char *certpath, + const char *certname, const char *certpasswd, const char *certfile); + +/* Equivalent, taking parameters from an upscli_authconf_t (also registers + * any CERTHOST from the authconf, like upscli_init_authconf() does). */ +void *upscli_get_or_create_ssl_context_authconf(upscli_authconf_t *ac); int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval *tv); /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ From 53b68a92394f18f8b5eb88e558ffecbaf5c242f9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 18:54:57 +0000 Subject: [PATCH 15/26] clients/upsclient.h: Expose opaque upscli_ssl_context_config_t type in public header for clarity [#3439] - Add forward declaration of upscli_ssl_context_config_t (struct upscli_ssl_context_config_s) - Change UPSCONN_t::ssl_ctx from void* to typed upscli_ssl_context_config_t* on all platforms - Clarify in comments that ssl_ctx is used for both NSS and OpenSSL (per-connection client cert identity) - Keep extra_reserved padding in non-OpenSSL builds for struct size compatibility - This makes the public API clearer without exposing internals: users know ssl_ctx is a registry handle Signed-off-by: Jim Klimov --- clients/upsclient.c | 4 ++-- clients/upsclient.h | 29 ++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index f1887d77cc..168f8d8169 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -225,7 +225,7 @@ static int upscli_default_connect_timeout_initialized = 0; * only once) - see notes at upscli_get_or_create_ssl_context() for how NSS * connections still get a per-connection client certificate identity. */ -typedef struct upscli_ssl_context_config_s { +struct upscli_ssl_context_config_s { int certverify; char *certpath; char *certname; /* CERTIDENT nickname/subject */ @@ -238,7 +238,7 @@ typedef struct upscli_ssl_context_config_s { unsigned int refcount; struct upscli_ssl_context_config_s *next; -} upscli_ssl_context_config_t; +}; static upscli_ssl_context_config_t *ssl_context_registry = NULL; diff --git a/clients/upsclient.h b/clients/upsclient.h index 5d6e4d33f4..8360e7b3d4 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -71,6 +71,14 @@ extern "C" { #include "parseconf.h" #include "authconf.h" +/* Forward declaration: SSL context configuration handle (opaque outside + * of upsclient.c; size and contents are dependent on build configuration). + * Obtained via upscli_get_or_create_ssl_context() method, and used with + * upscli_set_ssl_context(). Holds cached CA bundles, client cert identity, + * verify mode, and SSL backend dependent data - shared across connections. + */ +typedef struct upscli_ssl_context_config_s upscli_ssl_context_config_t; + #ifdef WITH_OPENSSL /* Adapted from https://linux.die.net/man/3/ssl_set_verify man page example */ typedef struct { @@ -114,18 +122,21 @@ typedef struct { /* WARNING for maintainers/devs: keep the ifdef'ed struct sizes * same for different builds, and add new data items in the end! */ - /* SSL context configuration (trusted CA, client cert identity, verify mode): - * NULL means use the ambient default set by upscli_init*(); a non-NULL - * opaque pointer (obtained via upscli_get_or_create_ssl_context()) attaches - * a cached context config to this connection for per-connection client cert - * identity on NSS, or per-connection SSL_CTX on OpenSSL. The context is - * shared and NOT owned by this connection; it is freed only by upscli_cleanup(). */ + /* SSL context configuration: cached CA bundle(s), client certificate + * identity, and certificate verification mode. When built with SSL + * support, NULL means to use the ambient default set by upscli_init*(); + * non-NULL is an opaque handle (from upscli_get_or_create_ssl_context()) + * to a shared registry entry. Per-connection (NSS, single context per + * process) or per-context (OpenSSL) client certificate identity is + * resolved via this registry. The connection does NOT own this entry; + * it is freed only by upscli_cleanup() after all connections are + * disconnected. As far as clients are concerned, this is a void-like + * pointer that they get from one method and pass on to another. */ + upscli_ssl_context_config_t *ssl_ctx; #ifdef WITH_OPENSSL openssl_cert_verify_data_t *openssl_cert_verify_data; - void *ssl_ctx; /* really: (upscli_ssl_context_config_t*) - opaque handle */ #else - void *extra_reserved; - void *ssl_ctx; /* padding for struct size in different build variants */ + void *extra_reserved; /* padding for struct size compatibility across build variants */ #endif /* WITH_OPENSSL */ } UPSCONN_t; From 8a3aa322a19aeffeec4dc01bab143ffa28bfb180 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 19:18:04 +0000 Subject: [PATCH 16/26] clients/upslog.c, clients/upsmon.c, clients/upsstats.c: Update multi-server clients to use per-connection SSL contexts [#3439] upsmon.c, upslog.c, upsstats.c: - Before connecting to each server, fetch per-server authconf with upscli_get_authconf_item() and create SSL context with upscli_get_or_create_ssl_context_authconf() - Attach context to connection via upscli_set_ssl_context() before connect - This allows simultaneous connections to different servers with different client certificates/CAs, resolving the limitation in issue #3494 - Update FIXME comments to NOTE as the limitation is now resolved Benefits: - upsmon can monitor UPS devices behind different CAs/client certs simultaneously - upslog can log multiple systems with independent SSL configurations - upsstats can query multiple data servers with per-server SSL setup - All within a single process, no shared global SSL context constraint Signed-off-by: Jim Klimov --- clients/upslog.c | 24 ++++++++++++++++++++---- clients/upsmon.c | 15 +++++++++++++++ clients/upsstats.c | 15 +++++++++++---- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/clients/upslog.c b/clients/upslog.c index e49e7fb775..1f2230850f 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -884,10 +884,9 @@ int main(int argc, char **argv) monhost_ups_current->port ); - /* FIXME [#3494]: Currently libupsclient allows for *one* SSL context - * shared by all connections, specifically the CERTIDENT of the client. - * We can have multiple CERTHOST certificates (and/or reading - * users/passwords) though. */ + /* NOTE [#3494]: Per-connection SSL contexts now supported via registry. + * Each connection can have a different CERTHOST and/or client certificate, + * even when connecting to different servers. */ ac_current = upscli_get_authconf_item(NULL, monhost_ups_current->hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, monhost_ups_current->port) > 0 ? str_port : NULL, 1); /* Always call this, to register possible CERTHOSTs etc. */ if (upscli_init_authconf(ac_current) > 0) { @@ -921,6 +920,14 @@ int main(int argc, char **argv) conn = (UPSCONN_t *)xmalloc(sizeof(*conn)); + /* Set up per-connection SSL context if available */ + { + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac_current); + if (ssl_ctx) { + upscli_set_ssl_context(conn, ssl_ctx); + } + } + if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(conn)); } @@ -1048,6 +1055,15 @@ int main(int argc, char **argv) monhost_ups_current->ups = (UPSCONN_t *)xmalloc(sizeof(UPSCONN_t)); + /* Set up per-connection SSL context if available for this specific system */ + { + upscli_authconf_t *ac = upscli_get_authconf_item(NULL, monhost_ups_current->hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, monhost_ups_current->port) > 0 ? str_port : NULL, 1); + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + if (ssl_ctx) { + upscli_set_ssl_context(monhost_ups_current->ups, ssl_ctx); + } + } + if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) fprintf(stderr, "Warning: initial connect failed: %s\n", upscli_strerror(monhost_ups_current->ups)); diff --git a/clients/upsmon.c b/clients/upsmon.c index 17ea9c1442..7ad68cc3a4 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -3025,6 +3025,21 @@ static int try_connect(utype_t *ups) flags |= UPSCLI_CONN_CERTVERIF; } + /* Set up per-connection SSL context if available for this specific UPS. + * This allows different UPS devices to use different client certificates + * even when connecting through the same process. */ + { + char str_port[16]; + upscli_authconf_t *ac = upscli_get_authconf_item(NULL, ups->hostname, + snprintf(str_port, sizeof(str_port), "%" PRIu16, ups->port) > 0 ? str_port : NULL, 1); + if (ac) { + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + if (ssl_ctx) { + upscli_set_ssl_context(&ups->conn, ssl_ctx); + } + } + } + ret = upscli_connect(&ups->conn, ups->hostname, ups->port, flags); if (ret < 0) { diff --git a/clients/upsstats.c b/clients/upsstats.c index 4c2c36cc1b..c9380d198c 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -540,10 +540,8 @@ static void ups_connect(void) exit(EXIT_FAILURE); } - /* FIXME: Currently libupsclient allows for one SSL context shared - * by all connections, specifically the CERTIDENT of the client. - * We can have multiple CERTHOST certificates (and/or reading - * users/passwords) though. */ + /* NOTE: Per-connection SSL contexts now supported via registry. + * Each connection can have different CERTHOST and/or client certificates. */ ac_current = upscli_get_authconf_item( NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, @@ -561,6 +559,15 @@ static void ups_connect(void) flags_ssl = flags_ssl_default; upscli_authconf_update_conn_flags(ac_current, &flags_ssl); + + /* Set up per-connection SSL context if available */ + { + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac_current); + if (ssl_ctx) { + upscli_set_ssl_context(&ups, ssl_ctx); + } + } + if (currups && upscli_connect(&ups, hostname, port, flags_ssl) < 0) { fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups ? NUT_STRARG(currups->sys) : "", From b69680ea1107f02c9f4a364490706143c7f8c0a2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 19:23:31 +0000 Subject: [PATCH 17/26] NEWS.adoc: document ability for multiple SSL contexts [#3439] Signed-off-by: Jim Klimov --- NEWS.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 8da073ce8e..de4301447f 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -379,6 +379,12 @@ https://github.com/networkupstools/nut/milestone/13 report the ability to check `CERTIDENT` information. [#3331] * Introduced support for "authconf" files to store and convey NUT client authentication details. [issue #3329] + * Added C/C++ client support for multiple independent OpenSSL contexts + in the same process, or using multiple NSS client certificates with + its single context, for ability to connect to NUT data servers managed + by different entities (e.g. certificate authorities and client/server + realms). In-tree multiple-connection clients (`upsmon`, `upsstats.cgi`, + `upslog`) were updated to use this feature. [issue #3439] - Various clients: * Flush standard output and error buffers before handling clean exit From 620878b33838d198c206c78ddde5aedd107662a4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 19:59:09 +0000 Subject: [PATCH 18/26] docs/new-clients.txt, docs/man/ups*.txt: document multiple SSL context support [#3439] Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 18 ++ docs/man/upscli_get_or_create_ssl_context.txt | 178 ++++++++++++++++++ docs/man/upscli_init.txt | 6 +- docs/man/upscli_set_ssl_context.txt | 149 +++++++++++++++ docs/man/upscli_ssl.txt | 3 + docs/man/upsclient.txt | 35 ++++ docs/man/upslog.txt | 21 ++- docs/man/upsmon.txt | 29 +++ docs/man/upsstats.cgi.txt | 28 ++- docs/new-clients.txt | 22 +++ docs/nut.dict | 6 +- 11 files changed, 486 insertions(+), 9 deletions(-) create mode 100644 docs/man/upscli_get_or_create_ssl_context.txt create mode 100644 docs/man/upscli_set_ssl_context.txt diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 75e4449ce9..7bdab6373d 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -554,6 +554,8 @@ SRC_DEV_PAGES = \ upscli_splitname.txt \ upscli_ssl.txt \ upscli_ssl_caps.txt \ + upscli_get_or_create_ssl_context.txt \ + upscli_set_ssl_context.txt \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ @@ -718,6 +720,16 @@ UPSCLI_SSL_CAPS_DEPS = \ $(UPSCLI_SSL_CAPS_DEPS): upscli_ssl_caps.$(MAN_SECTION_API) +UPSCLI_GET_OR_CREATE_SSL_CONTEXT_DEPS = \ + upscli_get_or_create_ssl_context_authconf.$(MAN_SECTION_API) + +$(UPSCLI_GET_OR_CREATE_SSL_CONTEXT_DEPS): upscli_get_or_create_ssl_context.$(MAN_SECTION_API) + +UPSCLI_SET_SSL_CONTEXT_DEPS = \ + upscli_get_ssl_context.$(MAN_SECTION_API) + +$(UPSCLI_SET_SSL_CONTEXT_DEPS): upscli_set_ssl_context.$(MAN_SECTION_API) + UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS = \ upscli_upslog_cookie.$(MAN_SECTION_API) \ upscli_upslog_get_debug_level.$(MAN_SECTION_API) \ @@ -759,6 +771,10 @@ INST_MAN_DEV_API_PAGES = \ upscli_ssl.$(MAN_SECTION_API) \ upscli_ssl_caps.$(MAN_SECTION_API) \ $(UPSCLI_SSL_CAPS_DEPS) \ + upscli_get_or_create_ssl_context.$(MAN_SECTION_API) \ + $(UPSCLI_GET_OR_CREATE_SSL_CONTEXT_DEPS) \ + upscli_set_ssl_context.$(MAN_SECTION_API) \ + $(UPSCLI_SET_SSL_CONTEXT_DEPS) \ upscli_strerror.$(MAN_SECTION_API) \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ @@ -939,6 +955,8 @@ INST_HTML_DEV_MANS = \ upscli_splitname.html \ upscli_ssl.html \ upscli_ssl_caps.html \ + upscli_get_or_create_ssl_context.html \ + upscli_set_ssl_context.html \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ diff --git a/docs/man/upscli_get_or_create_ssl_context.txt b/docs/man/upscli_get_or_create_ssl_context.txt new file mode 100644 index 0000000000..7a73f1590f --- /dev/null +++ b/docs/man/upscli_get_or_create_ssl_context.txt @@ -0,0 +1,178 @@ +UPSCLI_GET_OR_CREATE_SSL_CONTEXT(3) +=================================== + +NAME +---- + +upscli_get_or_create_ssl_context, upscli_get_or_create_ssl_context_authconf +- Get or create a cached per-connection SSL context configuration + +SYNOPSIS +-------- + +------ + #include + + void *upscli_get_or_create_ssl_context(int certverify, + const char *certpath, const char *certname, + const char *certpasswd, const char *certfile); + + void *upscli_get_or_create_ssl_context_authconf(upscli_authconf_t *ac); +------ + +DESCRIPTION +----------- + +The *upscli_get_or_create_ssl_context*() and +*upscli_get_or_create_ssl_context_authconf*() functions provide per-connection +SSL context support for client applications connecting to multiple NUT data +servers with independent certificate authorities or client certificate identities. + +These functions implement a process-wide registry of SSL context configurations. +Each configuration is cached by parameter hash and may be shared across multiple +connections, allowing efficient reuse while supporting independent settings for +different servers. + +The *upscli_get_or_create_ssl_context*() function creates or retrieves a cached +SSL context with the specified configuration: + +- `certverify`: Certificate verification mode (0 or 1) +- `certpath`: Path to CA bundle or NSS database directory +- `certname`: Client certificate nickname/identity for NSS, or NULL +- `certpasswd`: Password for client certificate, or NULL +- `certfile`: OpenSSL client certificate+key PEM file, or NULL + +The *upscli_get_or_create_ssl_context_authconf*() function performs the same +operation but takes parameters from an linkman:upscli_authconf_t[3] structure, +typically obtained via linkman:upscli_get_authconf_item[3]. This variant also +registers any CERTHOST security policies defined in the authconf, similar to +linkman:upscli_init_authconf[3]. + +CONFIGURATION LOOKUP +-------------------- + +The registry uses exact parameter matching to determine cache hits: + +- Identical parameters return a cached entry (efficient on repeat calls) +- Different parameters create new registry entries (allows multi-realm connections) + +USAGE PATTERN +------------- + +For applications connecting to multiple servers with independent SSL configurations: + +------ + upscli_authconf_t *ac = upscli_get_authconf_item( + NULL, hostname, port_str, 1); + + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + + /* NOTE: this clause is just an example of what happens + * under the hood; actually the upscli_tryconnect() method + * clears "conn" and finds or creates the suitable cached + * context by itself, anew every time. */ + if (ssl_ctx) { + upscli_set_ssl_context(conn, ssl_ctx); + } + + upscli_connect(conn, hostname, port, flags); +------ + +This pattern ensures each connection uses its server-specific SSL configuration, +even when multiple servers are accessed from the same process. + +MULTI-REALM SUPPORT +------------------- + +OpenSSL builds support independent linkman:SSL_CTX[3] objects per connection, +enabling simultaneous use of different certificate authorities and client +certificates within a single process. + +NSS builds share a process-global trust database (per NSS design constraints), +but the registry enables per-connection client certificate identity selection, +allowing different client certificates to be presented to different servers +from the same shared NSS database. + +SECURE CLEANUP +-------------- + +Sensitive fields (passwords, certificate names/paths) are automatically zeroed +with linkman:memset[3] before being freed, preventing sensitive data from +lingering in freed memory. + +LIFETIME AND OWNERSHIP +---------------------- + +SSL context configurations are owned by the registry and automatically freed by +linkman:upscli_cleanup[3]. Individual connections do not own the context +references and must not manually free them. + +RETURN VALUE +------------ + +The functions return an opaque handle to an SSL context configuration on +success, or NULL on failure (memory allocation error, invalid parameters, etc.). + +The returned handle is suitable for use with linkman:upscli_set_ssl_context[3]. + +ERRORS +------ + +Failure may occur if: + +- Memory allocation fails +- The specified CA bundle or database cannot be accessed +- The client certificate cannot be loaded or verified +- NSS initialization fails (NSS builds only) + +Call linkman:upscli_strerror[3] for error details. + +THREAD SAFETY +------------- + +The registry itself is protected by internal synchronization in threaded builds. +However, individual connection operations are not thread-safe; each thread +should maintain its own linkman:UPSCONN_t[3] instance. + +EXAMPLES +-------- + +Monitor multiple UPS devices with independent SSL configurations: + +------ + for (each_ups_in_config) { + UPSCONN_t *conn = malloc(sizeof(*conn)); + + // Get per-UPS authconf from config files + upscli_authconf_t *ac = upscli_get_authconf_item( + NULL, ups->hostname, ups->port_str, 1); + + // Create/fetch SSL context from registry + /* NOTE: this clause is just an example of what happens + * under the hood; actually the upscli_tryconnect() method + * clears "conn" and finds or creates the suitable cached + * context by itself, anew every time. */ + void *ctx = upscli_get_or_create_ssl_context_authconf(ac); + if (ctx) { + upscli_set_ssl_context(conn, ctx); + } + + // Connect with per-UPS SSL settings + upscli_connect(conn, ups->hostname, ups->port, flags); + } + + // Registry is freed when done: + upscli_cleanup(); +------ + +SEE ALSO +-------- + +linkman:upscli_init[3], linkman:upscli_init2[3], +linkman:upscli_init_authconf[3], linkman:upscli_cleanup[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], +linkman:upscli_connect[3], linkman:upscli_tryconnect[3], +linkman:upscli_disconnect[3], +linkman:upscli_authconf_t[3], linkman:upscli_get_authconf_item[3], +linkman:upscli_strerror[3], +linkmanext:SSL_CTX[7] diff --git a/docs/man/upscli_init.txt b/docs/man/upscli_init.txt index 5dff6f0573..d154f394a3 100644 --- a/docs/man/upscli_init.txt +++ b/docs/man/upscli_init.txt @@ -131,7 +131,11 @@ SEE ALSO linkman:upscli_add_host_cert[3], linkman:upscli_cleanup[3], linkman:upscli_connect[3], linkman:upscli_disconnect[3], -linkman:upscli_init_default_connect_timeout[3], linkman:upscli_fd[3], +linkman:upscli_fd[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], +linkman:upscli_init_default_connect_timeout[3], linkman:upscli_splitaddr[3], linkman:upscli_splitname[3], linkman:upscli_ssl[3], linkman:upscli_ssl_caps[3], linkman:upscli_ssl_caps_descr[3], diff --git a/docs/man/upscli_set_ssl_context.txt b/docs/man/upscli_set_ssl_context.txt new file mode 100644 index 0000000000..64f27bcee4 --- /dev/null +++ b/docs/man/upscli_set_ssl_context.txt @@ -0,0 +1,149 @@ +UPSCLI_SET_SSL_CONTEXT(3) +========================= + +NAME +---- + +upscli_set_ssl_context, upscli_get_ssl_context - Set or get per-connection +SSL context configuration + +SYNOPSIS +-------- + +------ + #include + + void *upscli_set_ssl_context(UPSCONN_t *ups, void *ssl_ctx); + + void *upscli_get_ssl_context(UPSCONN_t *ups); +------ + +DESCRIPTION +----------- + +The *upscli_set_ssl_context*() function attaches an SSL context configuration +to a connection, enabling per-connection SSL settings for certificate verification, +client certificate identity, and certificate authority. + +The *upscli_get_ssl_context*() function retrieves the currently attached SSL +context for a connection. + +CONTEXT OWNERSHIP +----------------- + +SSL context configurations are obtained from the process-wide registry via +linkman:upscli_get_or_create_ssl_context[3] or +linkman:upscli_get_or_create_ssl_context_authconf[3]. The connection holds +a reference to the registry entry but does not own it; contexts are freed +only by linkman:upscli_cleanup[3] after all connections are disconnected. + +USAGE +----- + +These functions are typically used together with linkman:upscli_get_or_create_ssl_context_authconf[3] +to support multiple servers with independent SSL configurations within a single +process: + +------ + // Get per-server authconf + upscli_authconf_t *ac = upscli_get_authconf_item( + NULL, hostname, port_str, 1); + + // Create/fetch SSL context from registry + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + + // Attach to connection + if (ssl_ctx) { + upscli_set_ssl_context(&conn, ssl_ctx); + } + + // Connect (uses per-server CA, client cert, verify mode) + upscli_connect(&conn, hostname, port, flags); +------ + +MULTI-REALM CONNECTIONS +------------------------ + +OpenSSL builds support independent SSL_CTX objects per connection, enabling: +- Different certificate authorities per server +- Different client certificates per server +- Different verification modes per server +- All within a single process + +NSS builds share a process-global certificate database but support: +- Different client certificate identities per connection +- Different verification modes per connection +- Per-server CERTHOST security policies + +RETURN VALUE +------------ + +The *upscli_set_ssl_context*() function returns the previous SSL context +attached to the connection (or NULL if none was attached), allowing callers +to track context changes if needed. + +The *upscli_get_ssl_context*() function returns the currently attached SSL +context, or NULL if none is attached (connection will use registry default). + +PARAMETERS +---------- + +`ups`:: + Pointer to a linkman:UPSCONN_t[3] structure for a connection. + Must be initialized by linkman:upscli_connect[3] or a + failed connection attempt. + +`ssl_ctx`:: + An opaque SSL context handle obtained from + linkman:upscli_get_or_create_ssl_context[3] or + linkman:upscli_get_or_create_ssl_context_authconf[3]. + Pass NULL to clear any existing per-connection context (falls back to default). + +EXAMPLE +------- + +Connecting to multiple NUT servers with independent SSL configurations: + +------ + UPSCONN_t conn1, conn2; + + // Server 1: custom CA and verify settings + upscli_authconf_t *ac1 = upscli_get_authconf_item( + NULL, "server1.example.com", "3493", 1); + void *ctx1 = upscli_get_or_create_ssl_context_authconf(ac1); + upscli_set_ssl_context(&conn1, ctx1); + upscli_connect(&conn1, "server1.example.com", 3493, flags); + + // Server 2: different CA and client certificate + upscli_authconf_t *ac2 = upscli_get_authconf_item( + NULL, "server2.example.com", "3493", 1); + void *ctx2 = upscli_get_or_create_ssl_context_authconf(ac2); + upscli_set_ssl_context(&conn2, ctx2); + upscli_connect(&conn2, "server2.example.com", 3493, flags); + + // Both connections use independent SSL contexts + + // Cleanup + upscli_disconnect(&conn1); + upscli_disconnect(&conn2); + upscli_cleanup(); // Frees all registry contexts +------ + +LEGACY BEHAVIOR +--------------- + +When no per-connection SSL context is set (NULL), the connection uses the +ambient default context created by linkman:upscli_init[3], +linkman:upscli_init2[3], or linkman:upscli_init_authconf[3]. This provides +backward compatibility with existing client code. + +SEE ALSO +-------- + +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_connect[3], linkman:upscli_disconnect[3], +linkman:upscli_init[3], linkman:upscli_init2[3], +linkman:upscli_init_authconf[3], linkman:upscli_cleanup[3], +linkman:upscli_get_authconf_item[3], +linkman:UPSCONN_t[3] diff --git a/docs/man/upscli_ssl.txt b/docs/man/upscli_ssl.txt index fac04866c4..72ac88ff25 100644 --- a/docs/man/upscli_ssl.txt +++ b/docs/man/upscli_ssl.txt @@ -37,6 +37,9 @@ SEE ALSO -------- linkman:upscli_ssl_caps[3], linkman:upscli_ssl_caps_descr[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], linkman:upscli_fd[3], linkman:upscli_get[3], linkman:upscli_readline[3], linkman:upscli_sendline[3], linkman:upscli_strerror[3], linkman:upscli_upserror[3] diff --git a/docs/man/upsclient.txt b/docs/man/upsclient.txt index 3d428262f6..bc77ece95a 100644 --- a/docs/man/upsclient.txt +++ b/docs/man/upsclient.txt @@ -33,6 +33,38 @@ linkman:upscli_add_host_cert[3] before initializing a connection to it. In the same way, just before exiting, and after all upscli usage, you must call linkman:upscli_cleanup[3] to flush cache files and perform other cleanup. +SSL CONTEXT CONFIGURATION +------------------------- + +The NUT upsclient library supports multiple independent SSL contexts within a +single process, enabling simultaneous connections to multiple NUT servers with +different certificate authorities and assuming different client certificate +identities. + +A process-wide registry caches SSL context configurations, keyed by parameters +(CA path, verify mode, client certificate identity, etc.). + +Contexts are created by linkman:upscli_get_or_create_ssl_context[3] or +linkman:upscli_get_or_create_ssl_context_authconf[3] and attached to individual +connections via linkman:upscli_set_ssl_context[3] before calling +linkman:upscli_connect[3]. + +For most applications, the default context created by linkman:upscli_init[3] +legacy method, or linkman:upscli_init2[3] or linkman:upscli_init_authconf[3] +methods added in NUT v2.8.6, is sufficient for connections to a single NUT +data server or multiple servers sharing a common security management realm. + +Truly multi-server applications (e.g., `upsmon`, `upslog`, `upsstats.cgi`) +can however benefit from per-connection contexts when servers are managed +by different entities (different CAs or client certificate realms). + +* NUT builds against OpenSSL support independent `SSL_CTX` objects per server + that can be used by `SSL` objects for individual connections, for complete + multi-realm isolation. +* NUT builds against Mozilla NSS share a process-global certificate database + (so the NSS DB files prepared by sysadmin must include all CAs and other + data), but support per-connection client certificate identity selection. + NETWORK FUNCTIONS ----------------- @@ -88,6 +120,9 @@ linkman:upscli_init[3], linkman:upscli_cleanup[3], linkman:upscli_add_host_cert[3], linkman:upscli_connect[3], linkman:upscli_disconnect[3], linkman:upscli_fd[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], linkman:upscli_getvar[3], linkman:upscli_list_next[3], linkman:upscli_list_start[3], linkman:upscli_readline[3], linkman:upscli_sendline[3], diff --git a/docs/man/upslog.txt b/docs/man/upslog.txt index 97f9223339..8c5afdf89b 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -228,10 +228,13 @@ Since NUT v2.8.3, the single-UPS options are added to the list of tuples, so both legacy and new options can be reliably used to monitor multiple devices in the same run. -Since this client can establish multiple connections, keep in mind that -currently it can only identify itself with some one (first seen) client -certificate, if `CERTIDENT` settings are used in the linkman:nutauth.conf[5] -file. Multiple `CERTHOST` directives for specially trusted servers can be used. +As this client can establish multiple connections, since NUT v2.8.6 release it +supports per-connection SSL contexts for independent certificate verification +and client certificate identities. This allows *upslog* to securely log data +from multiple UPS servers managed by different entities (different CAs or +security realms). Configuration is specified via linkman:nutauth.conf[5] +using CERTPATH, CERTVERIFY, CERTNAME, and CERTHOST directives, and is +automatically applied per-connection. SEE ALSO -------- @@ -245,7 +248,15 @@ Clients: ~~~~~~~~ linkman:upsc[8], linkman:upscmd[8], -linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8], +linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8] + +Client libraries and utilities: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +linkman:upsclient[3], linkman:upscli_init[3], linkman:upscli_cleanup[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], linkman:nutauth.conf[5] Internet resources: diff --git a/docs/man/upsmon.txt b/docs/man/upsmon.txt index f1d9e13813..4598c67fdc 100644 --- a/docs/man/upsmon.txt +++ b/docs/man/upsmon.txt @@ -137,6 +137,27 @@ UPS actually plugged into a serial or USB port, and a "secondary" is drawing power from the UPS but can't talk to it directly. See the section on UPS types for more. +SSL/TLS CONFIGURATION +--------------------- + +*upsmon* can connect to linkman:upsd[8] servers using SSL/TLS with per-server +certificate verification and client certificate authentication. SSL/TLS +configuration is defined in linkman:upsmon.conf[5] using CERTPATH, CERTVERIFY, +CERTNAME, and CERTHOST directives. + +For systems monitoring multiple UPS devices managed by different entities +(different CAs or security realms), *upsmon* automatically uses per-connection +SSL contexts, allowing independent certificate verification and client +certificate identities for each monitored UPS. + +OpenSSL builds support independent per-connection certificate authorities, +while NSS builds support per-connection client certificate selection from a +shared process-global database. + +See linkman:upsmon.conf[5] for detailed SSL/TLS configuration options, and +linkman:upscli_get_or_create_ssl_context[3] for information on the registry +mechanism used to manage per-connection SSL contexts. + NOTIFY EVENTS ------------- @@ -737,6 +758,14 @@ Clients: linkman:upsc[8], linkman:upscmd[8], linkman:upsrw[8], linkman:upsmon[8] +Client libraries and utilities: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +linkman:upsclient[3], linkman:upscli_init[3], linkman:upscli_cleanup[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3] + CGI programs: ~~~~~~~~~~~~~ diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index 6d76a1ee1a..4af88f8358 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -43,6 +43,28 @@ is not authorized", check that file first. SSL access may be further managed by linkman:nutauth.conf[5] file. +SSL/TLS CONFIGURATION +--------------------- + +*upsstats.cgi* supports per-server SSL/TLS certificate verification and +client certificate authentication when monitoring multiple UPS devices. +SSL/TLS configuration is defined via linkman:hosts.conf[5] and +linkman:nutauth.conf[5] using CERTPATH, CERTVERIFY, CERTNAME, and CERTHOST +directives. + +When monitoring multiple UPS servers managed by different entities +(different CAs or security realms), *upsstats.cgi* automatically uses +per-connection SSL contexts, allowing independent certificate verification +and client certificate identities for each monitored UPS. + +OpenSSL builds support independent per-connection certificate authorities, +while NSS builds support per-connection client certificate selection from a +shared process-global database. + +See linkman:nutauth.conf[5] for detailed SSL/TLS configuration options, and +linkman:upscli_get_or_create_ssl_context[3] for information on the registry +mechanism used to manage per-connection SSL contexts. + TEMPLATES --------- @@ -117,7 +139,11 @@ SEE ALSO -------- linkman:upsimage.cgi[8], -linkman:nutauth.conf[5] +linkman:upsclient[3], linkman:upscli_init[3], linkman:upscli_cleanup[3], +linkman:upscli_get_or_create_ssl_context[3], +linkman:upscli_get_or_create_ssl_context_authconf[3], +linkman:upscli_set_ssl_context[3], linkman:upscli_get_ssl_context[3], +linkman:hosts.conf[5], linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/new-clients.txt b/docs/new-clients.txt index 21cec6e73c..a15bf5cf36 100644 --- a/docs/new-clients.txt +++ b/docs/new-clients.txt @@ -48,6 +48,28 @@ upsclient functions. link:https://www.networkupstools.org/projects.html[Other programs] not included in this package may also use this library, such as wmnut. +Multiple SSL contexts in one process +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When a client needs to talk to several NUT servers that use different trust +roots, client certificate identities, or security realms, create a dedicated +SSL context for each connection instead of reusing a single process-wide +default. The libupsclient registry keeps those contexts cached and returns a +matching entry for identical parameters, while still allowing independent +settings for each server. + +------ + upscli_authconf_t *ac = upscli_get_authconf_item(NULL, hostname, port_str, 1); + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + if (ssl_ctx) { + upscli_set_ssl_context(conn, ssl_ctx); + } + upscli_connect(conn, hostname, port, flags); +------ + +This pattern is used by the in-tree multi-server clients such as `upsmon`, +`upslog`, and `upsstats.cgi`. + High level library: libnutclient ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/nut.dict b/docs/nut.dict index 2fb4a9e1da..c4da6cba71 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3818 utf-8 +personal_ws-1.1 en 3820 utf-8 AAC AAS ABI @@ -1986,6 +1986,7 @@ cstdint ctime ctrl cts +ctx ctypes cua cuaa @@ -2681,6 +2682,7 @@ maintainership maj makefile makevartable +malloc mandir manpage manpages @@ -3153,10 +3155,10 @@ renderer renderers renice repindex +replug repo reportId reposurgeon -replug repotec req resetter From 0aae2cf0b887faf8733bc49b2235d3f93dc3a7cd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 1 Sep 2026 01:19:59 +0000 Subject: [PATCH 19/26] clients/upsclient.c, clients/upslog.c, clients/upsstats.c: upscli_sslinit(): search for suitable ups->ssl_ctx if missing [#3439] An older hypothesis about pre-assigning ups->ssl_ctx was wrong because upscli_tryconnect() wipes the ups contents as heap trash. Signed-off-by: Jim Klimov --- clients/upsclient.c | 44 +++++++++++++++++++++++++++++++++++++++++--- clients/upslog.c | 17 ----------------- clients/upsstats.c | 8 -------- docs/new-clients.txt | 6 ++++++ 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 168f8d8169..f984a37b7b 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -2425,12 +2425,50 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) # endif /* WITH_OPENSSL | WITH_NSS */ char buf[UPSCLI_NETBUF_LEN]; + if (!ups) { + return -1; + } + + if (!ups->ssl_ctx) { + /* Could be flushed in upscli_tryconnect() */ + char str_port[16]; + upscli_authconf_t *ac = upscli_get_authconf_item(NULL, ups->host, + snprintf(str_port, sizeof(str_port), "%" PRIu16, ups->port) > 0 ? str_port : NULL, 1); + + upsdebugx(3, "%s: %s authconf entry for [%s:%s]", + __func__, ac ? "Found" : "No", ups->host, str_port); + + if (ac) { + /* Set up per-connection SSL context (if available) for + * this specific data server. This allows differently + * hosted UPS devices to use different client certificates + * even when connecting through the same process. */ + void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + + upsdebugx(3, "%s: %s SSL context for [%s:%s]", + __func__, ssl_ctx ? "Using" : "NOT using", + ups->host, str_port); + if (ssl_ctx) { + upscli_set_ssl_context(ups, ssl_ctx); + } else { + if (ac->certverify || ac->certpath || ac->certident || ac->certpasswd || ac->certfile) { + upslogx(LOG_WARNING, "Failed upscli_get_or_create_ssl_context_authconf() for [%s:%s] while SSL was required", ups->host, str_port); + upsnotify(NOTIFY_STATE_STOPPING, "Failed upscli_get_or_create_ssl_context_authconf() while SSL was required"); + exit(EXIT_FAILURE); + } + upslogx(LOG_WARNING, "Failed upscli_get_or_create_ssl_context_authconf() for [%s:%s] but SSL ability was not required", ups->host, str_port); + } + } + } + /* Intend to initialize upscli with no ssl db if not already done. * Compatibility stuff for old clients which do not initialize them. */ - if (upscli_initialized==0) { - upsdebugx(3, "upscli not initialized, " - "force initialisation without SSL configuration"); + if (!(ups->ssl_ctx) && !upscli_initialized) { + upsdebugx(3, "%s: upscli not initialized, " + "force initialisation without SSL configuration", __func__); + upsdebugx(5, "%s: ssl_ctx=%p upscli_initialized=%d", + __func__, ups ? (void*)ups->ssl_ctx : NULL, upscli_initialized); upscli_init(0, NULL, NULL, NULL); } diff --git a/clients/upslog.c b/clients/upslog.c index 1f2230850f..154df23ddb 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -920,14 +920,6 @@ int main(int argc, char **argv) conn = (UPSCONN_t *)xmalloc(sizeof(*conn)); - /* Set up per-connection SSL context if available */ - { - void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac_current); - if (ssl_ctx) { - upscli_set_ssl_context(conn, ssl_ctx); - } - } - if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(conn)); } @@ -1055,15 +1047,6 @@ int main(int argc, char **argv) monhost_ups_current->ups = (UPSCONN_t *)xmalloc(sizeof(UPSCONN_t)); - /* Set up per-connection SSL context if available for this specific system */ - { - upscli_authconf_t *ac = upscli_get_authconf_item(NULL, monhost_ups_current->hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, monhost_ups_current->port) > 0 ? str_port : NULL, 1); - void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); - if (ssl_ctx) { - upscli_set_ssl_context(monhost_ups_current->ups, ssl_ctx); - } - } - if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) fprintf(stderr, "Warning: initial connect failed: %s\n", upscli_strerror(monhost_ups_current->ups)); diff --git a/clients/upsstats.c b/clients/upsstats.c index c9380d198c..ef25655b69 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -560,14 +560,6 @@ static void ups_connect(void) flags_ssl = flags_ssl_default; upscli_authconf_update_conn_flags(ac_current, &flags_ssl); - /* Set up per-connection SSL context if available */ - { - void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac_current); - if (ssl_ctx) { - upscli_set_ssl_context(&ups, ssl_ctx); - } - } - if (currups && upscli_connect(&ups, hostname, port, flags_ssl) < 0) { fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups ? NUT_STRARG(currups->sys) : "", diff --git a/docs/new-clients.txt b/docs/new-clients.txt index a15bf5cf36..fb485f9b83 100644 --- a/docs/new-clients.txt +++ b/docs/new-clients.txt @@ -61,9 +61,15 @@ settings for each server. ------ upscli_authconf_t *ac = upscli_get_authconf_item(NULL, hostname, port_str, 1); void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); + + /* NOTE: this clause is just an example of what happens + * under the hood; actually the upscli_tryconnect() method + * clears "conn" and finds or creates the suitable cached + * context by itself, anew every time. */ if (ssl_ctx) { upscli_set_ssl_context(conn, ssl_ctx); } + upscli_connect(conn, hostname, port, flags); ------ From 9ddfbc70fa4061fe561bbdb0943486b2f0055b3c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 31 Aug 2026 20:20:41 +0000 Subject: [PATCH 20/26] clients/upsmon.c, clients/upslog.c, NEWS.adoc, docs, conf/* samples: introduce AUTHCONF keyword [#3439] Signed-off-by: Jim Klimov --- NEWS.adoc | 6 ++ UPGRADING.adoc | 13 +++ clients/upsmon.c | 163 +++++++++++++++++++++++++----------- clients/upsstats.c | 36 ++++++-- conf/hosts.conf.sample | 14 ++++ conf/nutauth.conf.sample.in | 4 + conf/upsmon.conf.sample.in | 17 ++++ docs/man/hosts.conf.txt | 10 ++- docs/man/nutauth.conf.txt | 10 ++- docs/man/upsmon.conf.txt | 15 +++- 10 files changed, 225 insertions(+), 63 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index de4301447f..e2577f31a2 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -385,6 +385,10 @@ https://github.com/networkupstools/nut/milestone/13 by different entities (e.g. certificate authorities and client/server realms). In-tree multiple-connection clients (`upsmon`, `upsstats.cgi`, `upslog`) were updated to use this feature. [issue #3439] + * To support the ability of multiple SSL authentication details for + `upsmon` and `upsstats.cgi`, their configuration files were extended + with `AUTHCONF` keyword which can point to the `nutauth.conf` file + they should use. [issue #3439] - Various clients: * Flush standard output and error buffers before handling clean exit @@ -417,6 +421,8 @@ https://github.com/networkupstools/nut/milestone/13 * Added support for best-effort use of `nutauth.conf` files from default locations described above (no way to choose the location, other than by web-server environment variables for CGI calls). [#3329] + * `upsstats.cgi` additionally has a new `AUTHCONF` configuration option + that can be passed via its `hosts.conf` file, as detailed above. [#3439] - `upsmon` client updates: * Introduced support for `CERTFILE` option, so the client can identify diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 9a01aee6cf..ca0e4daa4e 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -74,6 +74,19 @@ The new `-A filename` option defaults to trying to use a `nutauth.conf` file reading one ('none' as the legacy default). See the updated manual pages for more details. [issues #3329, #3411] +- The `upsmon` and `upsstats.cgi` clients capable of connections to multiple + NUT data servers, potentially in separately managed security realms, now + support an `AUTHCONF` option in their configuration files to specify the + SSL details to be used with each such server. Legacy `upsmon` configuration + keywords like `CERTPATH`, `CERTFILE`, `CERTIDENT`, `CERTHOST`, `CERTVERIFY` + and `FORCESSL` act as a "global defaults" section in `nutauth.conf` terms, + and any actual `nutauth.conf` file entries gets loaded on top of these data. + The default behavior (when `AUTHCONF` remains unspecified) is a best-effort + attempt to load the file from one of the standard locations. If your NUT + deployment defines an e.g. `/etc/nut/nutauth.conf` file which would be + loaded automatically, but its contents conflict with what `upsmon` needs + to know, please specify `AUTHCONF = none` in `upsmon.conf`. [issue #3439] + - The `powervar_cx_usb`, `tripplite_usb` drivers used a built-in limit on reconnection attempts after which they exited ('60' and '10' respectively). This was revised to follow the new common setting `reconnect_max_tries`, diff --git a/clients/upsmon.c b/clients/upsmon.c index 7ad68cc3a4..3f5aa21d6f 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -147,12 +147,18 @@ static int overdurationtime = -1; static char *run_as_user = NULL; /* SSL details - where to find certs, whether to use them */ -static char *certpath = NULL; /* NSS database or OpenSSL CA collection, a directory */ -static char *certname = NULL; /* Client cert subject name (optionally validate that we got the right one) */ -static char *certpasswd = NULL; /* Private key password */ -static char *certfile = NULL; /* OpenSSL client cert, a PEM file */ -static int certverify = 0; /* don't verify by default */ -static int forcessl = 0; /* don't require ssl by default */ +static char *authconf_configured = NULL; /* location of a nutauth.conf file, if requested via upsmon.conf */ +static upscli_authconf_t *ac_default = NULL; + + /* Legacy SSL details (before AUTHCONF was introduced), + * now those keywords populate "global defaults" entry + * in the AUTHCONF list, and are not used directly. */ +/*static char *certpath = NULL; // NSS database or OpenSSL CA collection, a directory */ +/*static char *certname = NULL; // Client cert subject name (optionally validate that we got the right one) */ +/*static char *certpasswd = NULL; // Private key password */ +/*static char *certfile = NULL; // OpenSSL client cert, a PEM file */ +/*static int certverify = 0; // don't verify by default */ +/*static int forcessl = 0; // don't require ssl by default */ static int shutdownexitdelay = 0; /* by default doshutdown() exits immediately */ static int userfsd = 0, pipefd[2]; @@ -2551,29 +2557,37 @@ static int parse_conf_arg(size_t numargs, char **arg) return 1; } + /* AUTHCONF */ + if (!strcmp(arg[0], "AUTHCONF")) { + free(authconf_configured); + authconf_configured = xstrdup(arg[1]); + /* NOTE: We parse it after reading the default/legacy keywords, if any */ + return 1; + } + /* CERTPATH */ if (!strcmp(arg[0], "CERTPATH")) { - free(certpath); - certpath = xstrdup(arg[1]); + free(ac_default->certpath); + ac_default->certpath = xstrdup(arg[1]); return 1; } /* CERTFILE */ if (!strcmp(arg[0], "CERTFILE")) { - free(certfile); - certfile = xstrdup(arg[1]); + free(ac_default->certfile); + ac_default->certfile = xstrdup(arg[1]); return 1; } /* CERTVERIFY (0|1) */ if (!strcmp(arg[0], "CERTVERIFY")) { - certverify = atoi(arg[1]); + ac_default->certverify = atoi(arg[1]); return 1; } /* FORCESSL (0|1) */ if (!strcmp(arg[0], "FORCESSL")) { - forcessl = atoi(arg[1]); + ac_default->forcessl = atoi(arg[1]); return 1; } @@ -2613,10 +2627,10 @@ static int parse_conf_arg(size_t numargs, char **arg) /* CERTIDENT */ if (!strcmp(arg[0], "CERTIDENT")) { - free(certname); - certname = xstrdup(arg[1]); - free(certpasswd); - certpasswd = xstrdup(arg[2]); + free(ac_default->certident); + ac_default->certident = xstrdup(arg[1]); + free(ac_default->certpasswd); + ac_default->certpasswd = xstrdup(arg[2]); return 1; } @@ -2626,12 +2640,32 @@ static int parse_conf_arg(size_t numargs, char **arg) /* CERTHOST (0|1) (0|1) */ if (!strcmp(arg[0], "CERTHOST")) { + char *norm_sectname = NULL, *norm_host = NULL, *norm_port = NULL; + upscli_authconf_t *ac_host = NULL; + + if (upscli_split_authconf_section(arg[1], &norm_sectname, NULL, NULL, &norm_host, &norm_port) >= 0 + && norm_host && norm_port && *norm_host && *norm_port + ) { + ac_host = upscli_get_authconf_item(NULL, norm_host, norm_port, 1); + if (!ac_host || ac_host == ac_default) + fatalx(EXIT_FAILURE, "Fatal error: unable to get host-specific authconf entry"); + + free(ac_host->certhost); + ac_host->certhost = xstrdup(arg[2]); + ac_host->certverify = atoi(arg[3]); + ac_host->forcessl = atoi(arg[4]); + } + + /* Maybe a repeat that ends in a quick no-op, but better be sure */ upscli_add_host_cert(arg[1], arg[2], atoi(arg[3]), atoi(arg[4])); + free(norm_sectname); + free(norm_host); + free(norm_port); + return 1; } if (!strcmp(arg[0], "MONITOR")) { - /* original style: no username (only 5 args) */ if (numargs == 5) { upslogx(LOG_ERR, "Unable to use old-style MONITOR line without a username"); @@ -2641,6 +2675,8 @@ static int parse_conf_arg(size_t numargs, char **arg) } /* ("primary"|"master" | "secondary"|"slave") */ + /* TOTHINK: save into authconf list as "[user@host{:port}]" sections? + * ...Delay until after defaults and CERTHOSTs are all known? */ addups(reload_flag, arg[1], arg[2], arg[3], arg[4], arg[5]); return 1; } @@ -2709,6 +2745,14 @@ static void loadconfig(void) ups = (utype_t *)ups->next; } } + + /* TOTHINK: Close UPSes above, call this to close any active SSL + * contexts, and forget old SSL-related settings, before reload? + * Otherwise we pile up new entries but never forget old ones? + * And do not reload any new data for nutauth.conf (default or + * AUTHCONF-provided) file contents... + */ + /*upscli_cleanup();*/ } while (pconf_file_next(&ctx)) { @@ -2765,6 +2809,22 @@ static void loadconfig(void) } } + if (reload_flag == 0) { + /* FIXME: See also comment above, to support reloading AUTHCONF */ + /* NOTE: If there were legacy keywords in upsmon.conf, + * they would have been handled earlier to populate defaults + * and possibly CERTHOST-specific entries in the authconf list; + * loading a file now would at best populate any missing points. + */ + if (authconf_configured) { + upsdebugx(1, "Using configured auth config file: %s", authconf_configured); + upscli_read_authconf_file(authconf_configured, 1, -1); + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0, 1); + } + } + /* FIXME: Per legacy behavior, we silently went on. * Maybe should abort on unusable configs? */ @@ -2879,14 +2939,8 @@ static void upsmon_cleanup(void) free(configfile); configfile = NULL; - free(certpath); - certpath = NULL; - free(certname); - certname = NULL; - free(certpasswd); - certpasswd = NULL; - free(certfile); - certfile = NULL; + free(authconf_configured); + authconf_configured = NULL; if (shutdowncmd_argv) { for (i = 0; i < shutdowncmd_argc; i++) { @@ -2989,13 +3043,25 @@ static void update_crittimer(utype_t *ups) /* handle connecting to upsd, plus get SSL going too if possible */ static int try_connect(utype_t *ups) { - int flags = 0, ret; + int flags = 0, ret, forcessl = 0, certverify = 0; + char str_port[16], *certpath = NULL; - upsdebugx(1, "Trying to connect to UPS [%s]", ups->sys); + upscli_authconf_t *ac = upscli_get_authconf_item(NULL, ups->hostname, + snprintf(str_port, sizeof(str_port), "%" PRIu16, ups->port) > 0 ? str_port : NULL, 1); + upsdebugx(3, "%s: %s authconf entry for UPS [%s] at [%s:%s]", + __func__, ac ? "Found" : "No", ups->sys, ups->hostname, str_port); + + upsdebugx(1, "%s: Trying to connect to UPS [%s]", __func__, ups->sys); clearflag(&ups->status, ST_CLICONNECTED); /* force it if configured that way, just try it otherwise */ + if (ac) { + forcessl = ac->forcessl; + certverify = ac->certverify; + certpath = ac->certpath; + } + if (forcessl == 1) flags |= UPSCLI_CONN_REQSSL; else @@ -3025,21 +3091,6 @@ static int try_connect(utype_t *ups) flags |= UPSCLI_CONN_CERTVERIF; } - /* Set up per-connection SSL context if available for this specific UPS. - * This allows different UPS devices to use different client certificates - * even when connecting through the same process. */ - { - char str_port[16]; - upscli_authconf_t *ac = upscli_get_authconf_item(NULL, ups->hostname, - snprintf(str_port, sizeof(str_port), "%" PRIu16, ups->port) > 0 ? str_port : NULL, 1); - if (ac) { - void *ssl_ctx = upscli_get_or_create_ssl_context_authconf(ac); - if (ssl_ctx) { - upscli_set_ssl_context(&ups->conn, ssl_ctx); - } - } - } - ret = upscli_connect(&ups->conn, ups->hostname, ups->port, flags); if (ret < 0) { @@ -3048,6 +3099,8 @@ static int try_connect(utype_t *ups) ups_is_gone(ups); return 0; } + upsdebugx(3, "%s: UPS [%s]: connect succeeded", + __func__, ups->sys); /* we're definitely connected now */ setflag(&ups->status, ST_CLICONNECTED); @@ -4241,6 +4294,18 @@ int main(int argc, char *argv[]) } } + /* Make sure default entry is created even if we did not see any + * CERT* or AUTHCONF lines yet */ + ac_default = upscli_get_authconf_item(NULL, NULL, NULL, 1); + if (!ac_default) + fatalx(EXIT_FAILURE, "Fatal error: unable to get default authconf entry"); + free(ac_default->section); + ac_default->section = NULL; /* no section name for default entry */ + upsdebugx(1, "Empty default authconf section created"); + if (nut_debug_level > 4) { + upscli_dump_authconf_list(NULL, 1, 1); + } + loadconfig(); /* CLI debug level can not be smaller than debug_min specified @@ -4253,6 +4318,11 @@ int main(int argc, char *argv[]) } upsdebugx(1, "debug level is '%d'", nut_debug_level); + if (nut_debug_level > 4) { + upsdebugx(5, "Collected AUTHCONF entries:"); + upscli_dump_authconf_list(NULL, 0, 0); + } + if (checking_flag) exit(check_pdflag()); @@ -4317,15 +4387,6 @@ int main(int argc, char *argv[]) writepid(prog); } - if (upscli_init2(certverify, certpath, certname, certpasswd, certfile) < 0) { - if (certverify || certpath || certname || certpasswd || certfile) { - upslogx(LOG_WARNING, "Failed upscli_init2() while SSL was required"); - upsnotify(NOTIFY_STATE_STOPPING, "Failed upscli_init2() while SSL was required"); - exit(EXIT_FAILURE); - } - upslogx(LOG_WARNING, "Failed upscli_init2() but SSL ability was not required"); - } - /* prep our signal handlers */ setup_signals(); diff --git a/clients/upsstats.c b/clients/upsstats.c index ef25655b69..882027c4e3 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -40,6 +40,7 @@ /* network timeout for initial connection, in seconds */ #define UPSCLI_DEFAULT_CONNECT_TIMEOUT "10" +static char *authconf_configured = NULL; static upscli_authconf_t *ac_default = NULL; static int flags_ssl = UPSCLI_CONN_TRYSSL, flags_ssl_default = UPSCLI_CONN_TRYSSL; @@ -88,6 +89,21 @@ static ulist_t *ulhead = NULL, *currups = NULL, static int skip_clause = 0, skip_block = 0; +static void init_authconf(void) { + if (authconf_configured) { + upsdebugx(1, "Using configured auth config file: %s", authconf_configured); + upscli_read_authconf_file(authconf_configured, 1, -1); + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0, 1); + } + + /* Prepare for handling in first loop through ups_connect() */ + ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); +} + void parsearg(char *var, char *value) { upsdebug_call_starting_for_str2(var, value); @@ -1503,6 +1519,12 @@ static void load_hosts_conf(int handle_MONITOR) if (ctx.numargs < 2) continue; + /* AUTHCONF */ + if (!strcmp(ctx.arglist[0], "AUTHCONF")) { + free(authconf_configured); + authconf_configured = xstrdup(ctx.arglist[1]); + } + /* CUSTOM_TEMPLATE_LIST */ if (!strcmp(ctx.arglist[0], "CUSTOM_TEMPLATE_LIST")) add_allowed_template_list(ctx.arglist[1]); @@ -1517,7 +1539,6 @@ static void load_hosts_conf(int handle_MONITOR) /* MONITOR */ if (handle_MONITOR && !strcmp(ctx.arglist[0], "MONITOR")) add_ups(ctx.arglist[1], ctx.arglist[2]); - } pconf_finish(&ctx); @@ -1596,6 +1617,7 @@ static void display_json(void) * We need to load hosts.conf ONLY in multi-host mode. */ if (monhost) { + init_authconf(); /* best-effort */ if (!checkhost(monhost, &monhostdesc)) { printf("{\"error\": \"Access to host %s is not authorized.\"}", monhost); upsdebug_call_finished1(": not auth"); @@ -1605,6 +1627,7 @@ static void display_json(void) currups = ulhead; } else { load_hosts_conf(1); /* This populates ulhead */ + init_authconf(); /* require AUTHCONF, else best-effort */ currups = ulhead; } @@ -1726,6 +1749,8 @@ static void clean_exit(void) fflush(stderr); upscli_cleanup(); + free(authconf_configured); + upsdebugx(1, "%s: finished, exiting", __func__); } @@ -1789,15 +1814,8 @@ int main(int argc, char **argv) extractcgiargs(); - upsdebugx(1, "Using best-effort auth config detection"); - upscli_read_authconf_file(NULL, 0, 1); - - upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); - /* Prepare for handling in first loop through ups_connect() */ - ac_default = upscli_find_authconf_item(NULL, NULL, NULL); - /* * If json is in the query, bypass all HTML and call display_json() */ @@ -1837,10 +1855,12 @@ int main(int argc, char **argv) add_allowed_template_list(DEFAULT_TEMPLATE_LIST); if (monhost) { load_hosts_conf(0); + init_authconf(); /* require AUTHCONF, else best-effort */ display_single(); } else { /* default: multimon replacement mode */ load_hosts_conf(1); + init_authconf(); /* require AUTHCONF, else best-effort */ currups = ulhead; display_template(template_list, 2); } diff --git a/conf/hosts.conf.sample b/conf/hosts.conf.sample index 3dec950898..d209864ad2 100644 --- a/conf/hosts.conf.sample +++ b/conf/hosts.conf.sample @@ -44,6 +44,20 @@ # MONITOR su2200@10.64.1.1 "Finance department" # MONITOR matrix@shs-server.example.edu "Sierra High School data room #1" +# ----------------------------------------------------------------------- +# +# Optional `nutauth.conf` file location to use for this client. +# Such a file allows the single upsstats.cgi client process to +# assume different certificate identities per connection, and +# helps it trust (and be trusted by) NUT data servers in separate +# security management realms. +# +# AUTHCONF +# +# Examples: +# +# AUTHCONF "nutauth-cgi.conf" + # ----------------------------------------------------------------------- # # Allowed custom template file (adapted copy of upsstats.html) for listing diff --git a/conf/nutauth.conf.sample.in b/conf/nutauth.conf.sample.in index 11afa19600..961c0500b6 100644 --- a/conf/nutauth.conf.sample.in +++ b/conf/nutauth.conf.sample.in @@ -8,6 +8,10 @@ # Such a file may `INCLUDE` further configurations (e.g. hop from a # per-user file to load server-wide defaults) if desired. # +# Some clients provided by the NUT project or third-party consumers can +# additionally define command-line options (`-A`) or configuration file +# keywords (`AUTHCONF`) to specify a preferred copy of this file. +# # While it usually suffices to have one client certificate for all servers, # it may be that some remote system owned/managed by a different department # would insist on *themselves* issuing (and revoking) certificates for their diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index a2b23b0b6a..9cbf036237 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -601,6 +601,23 @@ FINALDELAY 5 ALARMCRITICAL 1 +# ----------------------------------------------------------------------- +# +# Optional `nutauth.conf` file location to use for this client. +# When compiled with SSL support, this file allows to flexibly customize +# values historically covered by CERTPATH, CERTFILE, CERTIDENT, CERTHOST, +# CERTVERIFY and FORCESSL options listed below (taking them, if specified, +# as the preferred global default settings). Notably, the AUTHCONF file +# allows the single upsmon client process to assume different certificate +# identities per connection, and helps it trust (and be trusted by) NUT data +# servers in separate security management realms. +# +# AUTHCONF +# +# Examples: +# +# AUTHCONF "nutauth-upsmon.conf" + # -------------------------------------------------------------------------- # CERTPATH - path to certificates (database directory or directory with CA's) # diff --git a/docs/man/hosts.conf.txt b/docs/man/hosts.conf.txt index 9f57e657d4..7efc27aa76 100644 --- a/docs/man/hosts.conf.txt +++ b/docs/man/hosts.conf.txt @@ -40,6 +40,13 @@ The description must be one element, so if it has spaces, then it must be wrapped with quotes as shown above. The default hostname is "localhost". +*AUTHCONF* 'filename':: +Optional linkman:nutauth.conf[5] file location to use for this client. ++ +Such a file allows the single linkman:upsstats.cgi[8] client process to +assume different certificate identities per connection, and helps it trust +(and be trusted by) NUT data servers in separate security management realms. + *CUSTOM_TEMPLATE_LIST* 'filename':: *CUSTOM_TEMPLATE_SINGLE* 'filename':: @@ -63,7 +70,8 @@ see linkman:upsstats.html[5] for more details). SEE ALSO -------- -linkman:upsset.cgi[8], linkman:upsstats.cgi[8], linkman:upsimage.cgi[8] +linkman:upsset.cgi[8], linkman:upsstats.cgi[8], linkman:upsimage.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index 2cb2761e43..d66a3e2cff 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -17,6 +17,10 @@ linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], and others. Note that there is a dedicated linkman:upsmon.conf[5] configuration file for the linkman:upsmon[8] client. +Some clients provided by the NUT project or third-party consumers can +additionally define command-line options (`-A`) or configuration file +keywords (`AUTHCONF`) to specify a preferred copy of this file. + This file begins with optional global directives which can provide defaults for all connections. Per-server or per-account sections can then be defined to override these defaults. @@ -186,9 +190,11 @@ SEE ALSO -------- linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], -linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], +linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], linkman:dummy-ups[8], +linkman:upslog[8], linkman:upsd.users[5], -linkman:upsmon.conf[5], linkman:upsmon[8] +linkman:upsmon.conf[5], linkman:upsmon[8], +linkman:hosts.conf[5], linkman:upsstats.cgi[8] Internet resources ~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index 9a01be2bbb..788b809fa5 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -601,6 +601,18 @@ or numbers to define a delay (in seconds) between calling `SHUTDOWNCMD` and exiting the daemon. Zero means immediate exit (default), negative values mean never exiting on its own accord. +*AUTHCONF* 'filename':: +Optional linkman:nutauth.conf[5] file location to use for this client. ++ +When compiled with SSL support, this file allows to flexibly customize +values historically covered by 'CERTPATH', 'CERTFILE', 'CERTIDENT', +'CERTHOST', 'CERTVERIFY' and 'FORCESSL' options listed below (taking +them, if specified, as the preferred global default settings). +Notably, the 'AUTHCONF' file allows the single linkman:upsmon[8] client +process to assume different certificate identities per connection, +and helps it trust (and be trusted by) NUT data servers in separate +security management realms. + *CERTPATH* 'certificate database':: When compiled with SSL support, you can enter the certificate database @@ -718,7 +730,8 @@ does not change the previously active logging verbosity. SEE ALSO -------- -linkman:upsmon[8], linkman:upsd[8], linkman:nutupsdrv[8]. +linkman:upsmon[8], linkman:upsd[8], linkman:nutupsdrv[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ From 49d4dd111e94ea9e934e3e19d7a790f528963d62 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 1 Sep 2026 01:34:33 +0000 Subject: [PATCH 21/26] clients/upsclient.c: upscli_tryconnect(): debug-log getting into an SSL attempt [#3439] Signed-off-by: Jim Klimov --- clients/upsclient.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index f984a37b7b..6921758c07 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -3047,6 +3047,9 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags tryssl = (flags & UPSCLI_CONN_TRYSSL) != 0 ? 1 : 0; if (tryssl || forcessl) { + upsdebugx(4, "%s: Attempting SSL connection to %s:%" + PRIu16 " (certverify=%d, forcessl=%d, tryssl=%d)", + __func__, ups->host, ups->port, certverify, forcessl, tryssl); ret = upscli_sslinit(ups, certverify); if (forcessl && ret != 1) { upslogx(LOG_ERR, "Can not connect to NUT server %s in SSL, disconnect", host); From 89f99a3e703270da910d68500ba6c5f7e07f1ec0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 1 Sep 2026 01:37:10 +0000 Subject: [PATCH 22/26] clients/upsmon.c: revise debug-logging of notifycmd_concat Signed-off-by: Jim Klimov --- clients/upsmon.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients/upsmon.c b/clients/upsmon.c index 3f5aa21d6f..ff0d7cd5d5 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -4333,6 +4333,7 @@ int main(int argc, char *argv[]) : "!"); fflush(stdout); } else { + /* NOTE: shutdowncmd_concat is constructed without built-in quotes for reporting */ upsdebugx(1, "will use a shutdown command (SHUTDOWNCMD): '%s'", NUT_STRARG(shutdowncmd_concat)); } @@ -4341,7 +4342,8 @@ int main(int argc, char *argv[]) printf("Warning: no custom notification command defined, just so you know\n"); fflush(stdout); } else { - upsdebugx(1, "will use custom notification command (NOTIFYCMD): '%s'", + /* NOTE: notifycmd_concat is constructed with built-in quotes for reporting */ + upsdebugx(1, "will use custom notification command (NOTIFYCMD): %s", NUT_STRARG(notifycmd_concat)); } From daea1ddc63401e1dcb12092e8409a7c6a571cf4d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 1 Sep 2026 09:17:19 +0200 Subject: [PATCH 23/26] clients/nutclient.cpp: fix builds without OpenSSL [#3439] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 8300e57cd1..f9acf3a940 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -869,6 +869,8 @@ Socket::Socket(): # endif # ifdef WITH_OPENSSL _ssl_ctx(nullptr), +# elif defined(WITH_NSS) + _nss_initialized(false), # endif # if defined(WITH_OPENSSL) _verify_depth(9), /* openssl default */ @@ -921,14 +923,23 @@ void Socket::setTimeout(time_t timeout) void *Socket::setSSLContext(void *ssl_ctx) { +#ifdef WITH_OPENSSL void *previous = _ssl_ctx; _ssl_ctx = static_cast(ssl_ctx); return previous; +#else + NUT_UNUSED_VARIABLE(ssl_ctx); + return nullptr; +#endif } void *Socket::getSSLContext() const { +#ifdef WITH_OPENSSL return _ssl_ctx; +#else + return nullptr; +#endif } void Socket::setDebugConnect(bool d) From bbf86e217ae17e9a74d710eb28dcfaef4a5440b6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 1 Sep 2026 09:33:08 +0000 Subject: [PATCH 24/26] tests/nutclienttest.cpp: test_ssl_context_registry(): adapt to builds without OpenSSL backend [#3439] Signed-off-by: Jim Klimov --- tests/nutclienttest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/nutclienttest.cpp b/tests/nutclienttest.cpp index fc8779e44c..29a2b9895f 100644 --- a/tests/nutclienttest.cpp +++ b/tests/nutclienttest.cpp @@ -230,6 +230,17 @@ void NutClientTest::test_ssl_context_registry() { void *initial = client.getSSLContext(); CPPUNIT_ASSERT_MESSAGE("Expected no initial SSL context", initial == nullptr); + /* Without OpenSSL support, Socket::setSSLContext()/getSSLContext() are + * stubs that never store the pointer, so skip the round-trip checks. */ + if (!(nut::TcpClient::getSslCaps() & UPSCLI_SSL_CAPS_OPENSSL)) { + void *test_ctx = reinterpret_cast(0x1234); + CPPUNIT_ASSERT_MESSAGE("Expected no previous SSL context from stub setter", + client.setSSLContext(test_ctx) == nullptr); + CPPUNIT_ASSERT_MESSAGE("Expected stub getter to never report a stored SSL context", + client.getSSLContext() == nullptr); + return; + } + //std::cerr << "Setting custom SSL context" << std::endl; /* Set a custom SSL context (using a test pointer) */ From a18a25001ddcd50f76db2ef97d0ad59ae3e1e6ad Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 2 Sep 2026 12:44:40 +0000 Subject: [PATCH 25/26] clients/authconf.c, conf/*, docs/man/*: parse_authconf_file(): treat non-absolute paths as relative to confpath() [#3439, #3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 26 +++++++++++++++++++++----- conf/hosts.conf.sample | 1 + conf/upsmon.conf.sample.in | 1 + docs/man/hosts.conf.txt | 1 + docs/man/nutauth.conf.txt | 3 +++ docs/man/upsc.txt | 1 + docs/man/upscli_read_authconf_file.txt | 10 +++++++++- docs/man/upscmd.txt | 1 + docs/man/upslog.txt | 1 + docs/man/upsmon.conf.txt | 1 + docs/man/upsrw.txt | 1 + tests/test_authconf.c | 25 +++++++++++++++++++++---- 12 files changed, 62 insertions(+), 10 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index ea78250538..675134a8f9 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -966,8 +966,24 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope) { PCONF_CTX_t ctx; + char fn[NUT_PATH_MAX + 1]; + const char *filename_to_use = filename; - check_perms(filename); + if (filename[0] != '/' +#ifdef WIN32 + && filename[1] != ':' +#endif /* WIN32 */ + ) { + int path_len = snprintf(fn, sizeof(fn), "%s/%s", confpath(), filename); + if (path_len < 1 || (size_t)path_len >= sizeof(fn) + ) { + upslogx(LOG_ERR, "Could not construct path for authconf file: %s", filename); + return -1; + } + filename_to_use = fn; + } + + check_perms(filename_to_use); if (!pconf_init(&ctx, authconf_err)) { if (fatal_errors) { @@ -976,11 +992,11 @@ static int parse_authconf_file(const char *filename, int fatal_errors, int globa return -1; } - if (!pconf_file_begin(&ctx, filename)) { + if (!pconf_file_begin(&ctx, filename_to_use)) { if (fatal_errors) { - fatalx(EXIT_FAILURE, "Can't open %s: %s", filename, ctx.errmsg); + fatalx(EXIT_FAILURE, "Can't open %s: %s", filename_to_use, ctx.errmsg); } else { - upslogx(LOG_WARNING, "Can't open %s: %s", filename, ctx.errmsg); + upslogx(LOG_WARNING, "Can't open %s: %s", filename_to_use, ctx.errmsg); pconf_finish(&ctx); return -1; } @@ -988,7 +1004,7 @@ static int parse_authconf_file(const char *filename, int fatal_errors, int globa while (pconf_file_next(&ctx)) { if (pconf_parse_error(&ctx)) { - upslogx(LOG_ERR, "Parse error: %s:%d: %s", filename, ctx.linenum, ctx.errmsg); + upslogx(LOG_ERR, "Parse error: %s:%d: %s", filename_to_use, ctx.linenum, ctx.errmsg); continue; } handle_authconf_args(ctx.numargs, ctx.arglist, global_scope); diff --git a/conf/hosts.conf.sample b/conf/hosts.conf.sample index d209864ad2..d68623d815 100644 --- a/conf/hosts.conf.sample +++ b/conf/hosts.conf.sample @@ -47,6 +47,7 @@ # ----------------------------------------------------------------------- # # Optional `nutauth.conf` file location to use for this client. +# A non-absolute path is treated as relative to NUT configuration directory. # Such a file allows the single upsstats.cgi client process to # assume different certificate identities per connection, and # helps it trust (and be trusted by) NUT data servers in separate diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index 9cbf036237..e18710742d 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -604,6 +604,7 @@ ALARMCRITICAL 1 # ----------------------------------------------------------------------- # # Optional `nutauth.conf` file location to use for this client. +# A non-absolute path is treated as relative to NUT configuration directory. # When compiled with SSL support, this file allows to flexibly customize # values historically covered by CERTPATH, CERTFILE, CERTIDENT, CERTHOST, # CERTVERIFY and FORCESSL options listed below (taking them, if specified, diff --git a/docs/man/hosts.conf.txt b/docs/man/hosts.conf.txt index 7efc27aa76..2c0a47e6d5 100644 --- a/docs/man/hosts.conf.txt +++ b/docs/man/hosts.conf.txt @@ -42,6 +42,7 @@ be wrapped with quotes as shown above. The default hostname is *AUTHCONF* 'filename':: Optional linkman:nutauth.conf[5] file location to use for this client. +A non-absolute path is treated as relative to NUT configuration directory. + Such a file allows the single linkman:upsstats.cgi[8] client process to assume different certificate identities per connection, and helps it trust diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index d66a3e2cff..04e03f117d 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -169,6 +169,9 @@ Included files are supported via the `INCLUDE` directive for optionally present files, and `INCLUDE_REQUIRED` for files that must be there (otherwise the program exits with a fatal error). +WARNING: Any non-absolute paths would be resolved relatively to the NUT +configuration file location. + Global-scope includes may modify global default items, as well as define new sections or overlay items in existing sections. diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index 6ebc5b8748..6d98dbde95 100644 --- a/docs/man/upsc.txt +++ b/docs/man/upsc.txt @@ -94,6 +94,7 @@ COMMON OPTIONS Require use of the specified linkman:nutauth.conf[5] file (fail if absent, not accessible, or has content errors when parsed) to specify connection security settings and/or credentials for this run. + A non-absolute path is treated as relative to NUT configuration directory. + NOTE: Credentials are not currently required for read-only access to NUT. + diff --git a/docs/man/upscli_read_authconf_file.txt b/docs/man/upscli_read_authconf_file.txt index 6c94df644c..3af48c1bb4 100644 --- a/docs/man/upscli_read_authconf_file.txt +++ b/docs/man/upscli_read_authconf_file.txt @@ -23,6 +23,11 @@ The *upscli_read_authconf_file()* function reads the specified 'filename' (which is usually the path to *nutauth.conf*) and populates an internal list of authentication and SSL configurations. +If the filename is NOT `NULL`, for security reasons it must be either an +absolute path, or one resolved relatively to NUT configuration file location +(a built-in path defined by `configure` script, or `NUT_CONFPATH` environment +variable). + If 'filename' is `NULL`, the function first tries to locate either a file whose path and name is fully provided in `${NUT_AUTHCONF_FILE}` environment variable, or a `${NUT_AUTHCONF_PATH}/nutauth.conf`, and would not try any @@ -53,9 +58,12 @@ NESTING (INCLUDE FILES) ~~~~~~~~~~~~~~~~~~~~~~~ Included files are supported via the `INCLUDE` directive for optionally -present files, and `INCLUDE_REQUIRED` for files that must be there +present files, and `INCLUDE_REQUIRED` for files which must be there (otherwise the program exits with a fatal error). +Non-absolute paths would be resolved relatively to NUT configuration file +location. + Global-scope includes may modify global default items and define new sections. Section-scope includes (nested within a section) can only modify data diff --git a/docs/man/upscmd.txt b/docs/man/upscmd.txt index a43057d6fa..34f921246e 100644 --- a/docs/man/upscmd.txt +++ b/docs/man/upscmd.txt @@ -92,6 +92,7 @@ Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. Require use of the specified linkman:nutauth.conf[5] file (fail if absent, not accessible, or has content errors when parsed) to specify connection security settings and/or credentials for this run. + A non-absolute path is treated as relative to NUT configuration directory. + By silent default, the client tries best-effort (non-fatal) detection of a configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` diff --git a/docs/man/upslog.txt b/docs/man/upslog.txt index 8c5afdf89b..0bdf1d9291 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -160,6 +160,7 @@ Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. Require use of the specified linkman:nutauth.conf[5] file (fail if absent, not accessible, or has content errors when parsed) to specify connection security settings and/or credentials for this run. + A non-absolute path is treated as relative to NUT configuration directory. + NOTE: Credentials are not currently required for read-only access to NUT. + diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index 788b809fa5..de7c7a86bf 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -603,6 +603,7 @@ values mean never exiting on its own accord. *AUTHCONF* 'filename':: Optional linkman:nutauth.conf[5] file location to use for this client. +A non-absolute path is treated as relative to NUT configuration directory. + When compiled with SSL support, this file allows to flexibly customize values historically covered by 'CERTPATH', 'CERTFILE', 'CERTIDENT', diff --git a/docs/man/upsrw.txt b/docs/man/upsrw.txt index 71041ae998..bfb0f4b79d 100644 --- a/docs/man/upsrw.txt +++ b/docs/man/upsrw.txt @@ -108,6 +108,7 @@ Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. Require use of the specified linkman:nutauth.conf[5] file (fail if absent, not accessible, or has content errors when parsed) to specify connection security settings and/or credentials for this run. + A non-absolute path is treated as relative to NUT configuration directory. + By silent default, the client tries best-effort (non-fatal) detection of a configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 0eb07518e7..917805925d 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -38,9 +38,26 @@ int main(int argc, char **argv) FILE *f; upscli_authconf_t *ac, *ac5, *ac7, *ac8, *ac9, *ac12; size_t num_sections, expected_sections = 0; - char buf[512], *s; + char buf[512], *s, test_conf_path[NUT_PATH_MAX], + cwd[NUT_PATH_MAX - 32]; /* truncate so test_conf / include_conf suffixes fit into NUT_PATH_MAX */ int l, testnum = 0; + /* NOTE: AUTHCONF feature requires absolute paths for security, + * and treats any others as relative to confpath()! */ + memset(cwd, 0, sizeof(cwd)); + if (!getcwd(cwd, sizeof(cwd) - 1) || !(*cwd)) + snprintf(cwd, sizeof(cwd), "."); + +#ifdef WIN32 + /* Assume modern enough Windows that supports both slashes in paths */ + for (s = cwd; *s; s++) { + if (*s == '\\') + *s = '/'; + } +#endif + + snprintf(test_conf_path, sizeof(test_conf_path), "%s/%s", cwd, test_conf); + s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &l, 10) && l > 0) { nut_debug_level = l; @@ -65,7 +82,7 @@ int main(int argc, char **argv) fprintf(f, "USER = globaluser\n"); fprintf(f, "PASS = globalpass\n"); fprintf(f, "CERTVERIFY = 1\n"); - fprintf(f, "INCLUDE %s\n", include_conf); + fprintf(f, "INCLUDE \"%s/%s\"\n", cwd, include_conf); expected_sections++; fprintf(f, "[@localhost:12345]\n"); @@ -137,8 +154,8 @@ int main(int argc, char **argv) } /* 1. Expected file read */ - printf("=== Reading '%s' generated for this test\n", test_conf); - if (upscli_read_authconf_file(test_conf, 1, -1) != 1) { + printf("=== Reading '%s' generated for this test\n", test_conf_path); + if (upscli_read_authconf_file(test_conf_path, 1, -1) != 1) { fprintf(stderr, "not ok %d - read_authconf failed\n", ++testnum); return 1; } From 4ca230b15f1f583d51e01c3f9fd3d3fa342088ef Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 2 Sep 2026 12:45:43 +0000 Subject: [PATCH 26/26] clients/upsmon.c, client/upsstats.c, conf/*, docs/man/*: support special values of AUTHCONF option like command-line clients do [#3439, #3329] Signed-off-by: Jim Klimov --- NEWS.adoc | 13 ++++++++----- UPGRADING.adoc | 8 ++++---- clients/upsmon.c | 13 ++++++++++--- clients/upsstats.c | 13 ++++++++++--- conf/hosts.conf.sample | 8 +++++++- conf/upsmon.conf.sample.in | 8 +++++++- docs/man/hosts.conf.txt | 11 ++++++++++- docs/man/upsmon.conf.txt | 9 +++++++++ 8 files changed, 65 insertions(+), 18 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index e2577f31a2..a33919280e 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -388,7 +388,10 @@ https://github.com/networkupstools/nut/milestone/13 * To support the ability of multiple SSL authentication details for `upsmon` and `upsstats.cgi`, their configuration files were extended with `AUTHCONF` keyword which can point to the `nutauth.conf` file - they should use. [issue #3439] + they should use, or pass the special values `default` (require a + user- or system-provided `nutauth.conf` file) or `none` (skip + authconf discovery entirely), matching the `-A` option semantics + of command-line NUT clients. [issue #3439] - Various clients: * Flush standard output and error buffers before handling clean exit @@ -404,10 +407,10 @@ https://github.com/networkupstools/nut/milestone/13 SSL settings in the client which previously only did best-effort attempts at secure communications without an individual certificate, and only anonymously for reading. The new `-A filename` option defaults to trying - to use a `nutauth.conf` file (if found in one of the default locations) - but not failing if one is not usable; specific values can require use of - such a file (`default`) or to not even try reading one (`none`). - [issues #3329, #3411] + to use a `nutauth.conf` file (if NOT specified here, but rather found in + one of the default locations) but not failing if one is not usable; any + specific values can require use of such a file (`default`) or to not even + try reading one (`none`). [issues #3329, #3411] - `upslog` client/tool updates: * Added support for best-effort use of `nutauth.conf` files from default diff --git a/UPGRADING.adoc b/UPGRADING.adoc index ca0e4daa4e..5e26f1a8dc 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -69,10 +69,10 @@ Changes from 2.8.5 to 2.8.6 for reading like `upsc`. + The new `-A filename` option defaults to trying to use a `nutauth.conf` file - (if found in one of the default locations) but not failing if one is not - usable; specific values can require use of such a file or to not even try - reading one ('none' as the legacy default). See the updated manual pages - for more details. [issues #3329, #3411] + (if NOT specified here, but rather found in one of the default locations) + but not failing if one is not usable; any specific values can require use + of such a file or to not even try reading one ('none' as the legacy default). + See the updated manual pages for more details. [issues #3329, #3411] - The `upsmon` and `upsstats.cgi` clients capable of connections to multiple NUT data servers, potentially in separately managed security realms, now diff --git a/clients/upsmon.c b/clients/upsmon.c index ff0d7cd5d5..93815b0d36 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -2557,7 +2557,7 @@ static int parse_conf_arg(size_t numargs, char **arg) return 1; } - /* AUTHCONF */ + /* AUTHCONF ( | "default" | "none") */ if (!strcmp(arg[0], "AUTHCONF")) { free(authconf_configured); authconf_configured = xstrdup(arg[1]); @@ -2817,8 +2817,15 @@ static void loadconfig(void) * loading a file now would at best populate any missing points. */ if (authconf_configured) { - upsdebugx(1, "Using configured auth config file: %s", authconf_configured); - upscli_read_authconf_file(authconf_configured, 1, -1); + if (!strcmp(authconf_configured, "none")) { + upsdebugx(1, "Using AUTHCONF='%s': skipping auth config", authconf_configured); + } else if (!strcmp(authconf_configured, "default")) { + upsdebugx(1, "Using AUTHCONF='%s': require a user or system provided file", authconf_configured); + upscli_read_authconf_file(NULL, 1, -1); + } else { + upsdebugx(1, "Using configured auth config file: %s", authconf_configured); + upscli_read_authconf_file(authconf_configured, 1, -1); + } } else { upsdebugx(1, "Using best-effort auth config detection"); upscli_read_authconf_file(NULL, 0, 1); diff --git a/clients/upsstats.c b/clients/upsstats.c index 882027c4e3..32a63bf84f 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -91,8 +91,15 @@ static int skip_clause = 0, skip_block = 0; static void init_authconf(void) { if (authconf_configured) { - upsdebugx(1, "Using configured auth config file: %s", authconf_configured); - upscli_read_authconf_file(authconf_configured, 1, -1); + if (!strcmp(authconf_configured, "none")) { + upsdebugx(1, "Using AUTHCONF='%s': skipping auth config", authconf_configured); + } else if (!strcmp(authconf_configured, "default")) { + upsdebugx(1, "Using AUTHCONF='%s': require a user or system provided file", authconf_configured); + upscli_read_authconf_file(NULL, 1, -1); + } else { + upsdebugx(1, "Using configured auth config file: %s", authconf_configured); + upscli_read_authconf_file(authconf_configured, 1, -1); + } } else { upsdebugx(1, "Using best-effort auth config detection"); upscli_read_authconf_file(NULL, 0, 1); @@ -1519,7 +1526,7 @@ static void load_hosts_conf(int handle_MONITOR) if (ctx.numargs < 2) continue; - /* AUTHCONF */ + /* AUTHCONF ( | "default" | "none") */ if (!strcmp(ctx.arglist[0], "AUTHCONF")) { free(authconf_configured); authconf_configured = xstrdup(ctx.arglist[1]); diff --git a/conf/hosts.conf.sample b/conf/hosts.conf.sample index d68623d815..f2f551fd1e 100644 --- a/conf/hosts.conf.sample +++ b/conf/hosts.conf.sample @@ -48,16 +48,22 @@ # # Optional `nutauth.conf` file location to use for this client. # A non-absolute path is treated as relative to NUT configuration directory. +# Special values "default" (require a default file in one of the locations +# supported by nutauth.conf) and "none" (do not try to find or load any +# such file) are also accepted. If not specified at all, best-effort +# (non-fatal) detection of a default file is attempted. # Such a file allows the single upsstats.cgi client process to # assume different certificate identities per connection, and # helps it trust (and be trusted by) NUT data servers in separate # security management realms. # -# AUTHCONF +# AUTHCONF ( | "default" | "none") # # Examples: # # AUTHCONF "nutauth-cgi.conf" +# AUTHCONF default +# AUTHCONF none # ----------------------------------------------------------------------- # diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index e18710742d..5dbea12c17 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -605,6 +605,10 @@ ALARMCRITICAL 1 # # Optional `nutauth.conf` file location to use for this client. # A non-absolute path is treated as relative to NUT configuration directory. +# Special values "default" (require a default file in one of the locations +# supported by nutauth.conf) and "none" (do not try to find or load any +# such file) are also accepted. If not specified at all, best-effort +# (non-fatal) detection of a default file is attempted. # When compiled with SSL support, this file allows to flexibly customize # values historically covered by CERTPATH, CERTFILE, CERTIDENT, CERTHOST, # CERTVERIFY and FORCESSL options listed below (taking them, if specified, @@ -613,11 +617,13 @@ ALARMCRITICAL 1 # identities per connection, and helps it trust (and be trusted by) NUT data # servers in separate security management realms. # -# AUTHCONF +# AUTHCONF ( | "default" | "none") # # Examples: # # AUTHCONF "nutauth-upsmon.conf" +# AUTHCONF default +# AUTHCONF none # -------------------------------------------------------------------------- # CERTPATH - path to certificates (database directory or directory with CA's) diff --git a/docs/man/hosts.conf.txt b/docs/man/hosts.conf.txt index 2c0a47e6d5..6c0c8b6b14 100644 --- a/docs/man/hosts.conf.txt +++ b/docs/man/hosts.conf.txt @@ -40,10 +40,19 @@ The description must be one element, so if it has spaces, then it must be wrapped with quotes as shown above. The default hostname is "localhost". -*AUTHCONF* 'filename':: +*AUTHCONF* ('filename' | `default` | `none`):: Optional linkman:nutauth.conf[5] file location to use for this client. A non-absolute path is treated as relative to NUT configuration directory. + +Special values: ++ +* `default`: require a default file in one of the locations listed in + linkman:nutauth.conf[5] (fail if none is found or accessible); +* `none`: do not try to find or load any such file. ++ +If 'AUTHCONF' is not specified at all, best-effort (non-fatal) detection +of a default file is attempted. ++ Such a file allows the single linkman:upsstats.cgi[8] client process to assume different certificate identities per connection, and helps it trust (and be trusted by) NUT data servers in separate security management realms. diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index de7c7a86bf..c75050f7cc 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -605,6 +605,15 @@ values mean never exiting on its own accord. Optional linkman:nutauth.conf[5] file location to use for this client. A non-absolute path is treated as relative to NUT configuration directory. + +Special values: ++ +* `default`: require a default file in one of the locations listed in + linkman:nutauth.conf[5] (fail if none is found or accessible); +* `none`: do not try to find or load any such file. ++ +If 'AUTHCONF' is not specified at all, best-effort (non-fatal) detection +of a default file is attempted. ++ When compiled with SSL support, this file allows to flexibly customize values historically covered by 'CERTPATH', 'CERTFILE', 'CERTIDENT', 'CERTHOST', 'CERTVERIFY' and 'FORCESSL' options listed below (taking