diff --git a/Makefile.am b/Makefile.am index acc00c75ad..39bfa7162d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -250,6 +250,7 @@ all-libs-local/common: all-libs-local/include @dotMAKE@ ### Delivers: libupsclient.la libnutclient.la libnutclientstub.la ### Delivers: libupsclient-version.h ### LIB-Requires-ext: common/libcommonclient.la +### Requires-ext: include/nut_version.h ### Requires-ext: common/libcommon.la common/libcommonclient.la ### Requires-ext: common/libcommonversion.la ### Requires-ext: common/libparseconf.la diff --git a/NEWS.adoc b/NEWS.adoc index 23b9f8f38d..859ae7188b 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -3,7 +3,12 @@ NUT Release Notes ================= endif::txt[] -If you're upgrading from an earlier version, see the link:UPGRADING.adoc[] file. +This document summarizes the practical side of changes coming with each +newer release development, as compared to the preceding release. + +If you're upgrading from an earlier version, or are a package maintainer, +please see also the link:UPGRADING.adoc[] file about anticipated impacts +of ongoing development on existing deployments and third-party consumers. Please note that web and source document links, product and service names listed in historic entries of past releases may no longer be relevant. @@ -104,6 +109,16 @@ https://github.com/networkupstools/nut/milestone/12 characters. Now it is evaluated at `configure` time (to check that the characters may be used), and if not -- during `nut_stdint.h` parsing to fit known `int`/`long`/`long long` types. [#3300] + * Added new API methods and defined bitmap values for `libupsclient` + C binding to query and report SSL capabilities of the current library + build (none, OpenSSL, Mozilla NSS): `upscli_ssl_caps_descr()` and + `upscli_ssl_caps()`. Updated common NUT clients to report this info + in their detailed help banners. Done similarly for `upsd`. The NIT + (NUT Integration Test) suite piggy-backs on this to add run-time + dependent tests of SSL capability. Added `upscli_set_debug_level()` + and `upscli_set_debug_level()` methods to facilitate NUT debugging + for clients built with shared NUT private libraries. [issues #3328, + #1771, #2800, PR #3330] - NUT for Windows specific updates: * Revised detection of (relative) paths to program and configuration files @@ -302,6 +317,11 @@ https://github.com/networkupstools/nut/milestone/12 or Windows `HANDLE`'s at a time, and moving on to another chunk. The system-provided value can be further limited by `NUT_SYSMAXCONN_LIMIT` environment variable (e.g. in tests). [#3302] + * Extended processing of `CERTREQUEST` setting to handle numeric or specific + string values, to match both ways of reading ambiguous documentation. + Added `configure --with-ssl-client-validation` toggle to expose the + macro previously meant to be passed via `make` command line. [PR #3330, + but beware issue #3329] - `upsdrvctl` tool updates: * Make use of `setproctag()` and `getproctag()` to report parent/child @@ -530,6 +550,10 @@ several `FSD` notifications into one executed action. [PR #3097] * Dropped the `compile` script from Git sources. It originates from automake and is added to work area (if missing) during `autogen.sh` rituals anyway. It is still distributed as part of `make dist` tarball. [#1209] + * Extended `ci_build.sh` and `build-mingw-nut.sh` so that certain values + of `NUT_SSL_VARIANTS=[yes, no, auto, ssl, nss, openssl]` can be used + with generic builds to test a specific code path and not only the + auto-detected one. [#1711] - Upstreamed reference packaging recipes (DEB, RPM) from the 42ITy project which can be used with OBS (Open Build Service by SUSE), both to support diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 534bdf8e04..d840bee5c0 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -4,8 +4,15 @@ Upgrading notes endif::txt[] This file lists changes that affect users who installed older versions -of this software. When upgrading from an older version, be sure to -check this file to see if you need to make changes to your system. +of this software, or third-party integrations and library or data consumers. +When upgrading from an older NUT version, be sure to check this file to +see if you need to make changes to your system. + +We welcome feedback from package maintainers -- if you had to patch something +out, or work around something in NUT code or recipes, please let us know in +the issue tracker. Chances are, other distributions feel your pain, and some +generalized solution belongs in the upstream project as an easy to use build +configuration toggle to be shared by all interested downstream projects. [NOTE] ====== @@ -35,6 +42,10 @@ Changes from 2.8.4 to 2.8.5 library files to deliver with the packages (formally versioned and named by NUT release semantic version triplet). [issue #2800] +- Related to the above, `libupsclient` will remove the exported symbol for + `nut_debug_level` variable in a later NUT release, and now introduces the + `upscli_set_debug_level()` and `upscli_get_debug_level()` methods. [PR #3330] + - For ages, most recipes for building NUT had customized the `sysconfdir` to be `/etc/nut`, which is not exactly the *system* configuration directory. This is finally deprecated, with new `--with-confdir` configuration option @@ -99,6 +110,10 @@ Changes from 2.8.4 to 2.8.5 use `upsdrvquery_NOSIGPIPE=0` to disable neutering of the signal inside the API itself. [PR #3277] +- Added new API methods and defined bitmap values for `libupsclient` C binding + to query and report SSL capabilities of the library build (none, OpenSSL, + Mozilla NSS): `upscli_ssl_caps_descr()` and `upscli_ssl_caps()`. [PR #33xx] + - Fixed man page naming for `nutdrv_siemens-sitop(.8)` (dash vs. underscore) to match the driver program name. Packaging recipes may have to be updated. Follow-up from slightly botched renaming in original contribution. [PR #545] diff --git a/appveyor.yml b/appveyor.yml index 1ec6a5250d..b9c29eef93 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -63,7 +63,7 @@ install: # versions of packages. - cmd: | REM Prerequisites for NUT per https://github.com/networkupstools/nut/blob/master/docs/config-prereqs.txt : - C:\msys64\usr\bin\bash -lc "date -u; pacman --noconfirm -S --needed base-devel mingw-w64-x86_64-toolchain autoconf-wrapper automake-wrapper libtool mingw-w64-x86_64-libltdl gcc ccache mingw-w64-x86_64-ccache git aspell aspell-en python mingw-w64-x86_64-python-pygments mingw-w64-x86_64-winpthreads-git mingw-w64-x86_64-libusb mingw-w64-x86_64-libusb-compat-git mingw-w64-x86_64-neon libneon-devel mingw-w64-x86_64-libgd mingw-w64-x86_64-cppunit" + C:\msys64\usr\bin\bash -lc "date -u; pacman --noconfirm -S --needed base-devel mingw-w64-x86_64-toolchain autoconf-wrapper automake-wrapper libtool mingw-w64-x86_64-libltdl gcc ccache mingw-w64-x86_64-ccache git aspell aspell-en python mingw-w64-x86_64-python-pygments mingw-w64-x86_64-winpthreads-git mingw-w64-x86_64-libusb mingw-w64-x86_64-libusb-compat-git mingw-w64-x86_64-neon libneon-devel mingw-w64-x86_64-libgd mingw-w64-x86_64-cppunit mingw-w64-x86_64-nss mingw-w64-x86_64-openssl" REM SKIP mingw-w64-x86_64-libmodbus-git : we custom-build one with USB support REM SKIP for now NUT-Monitor prereqs (runtime Python would require somilar modules; need to fix localization builds like "fr.po"): gettext mingw-w64-x86_64-python-pyqt6 @@ -115,7 +115,7 @@ build_script: REM to find "nearby" program or configuration files (see common.c REM for current implementation). Hard-coded fallback strings may REM end up getting used in those cases. - C:\msys64\usr\bin\bash -lc 'date -u; PATH="/mingw64/bin:$PATH" CI_SKIP_CHECK=true CANBUILD_WITH_LIBMODBUS_USB=yes WITH_LIBNUTPRIVATE=true ./ci_build.sh --with-docs=no' + C:\msys64\usr\bin\bash -lc 'date -u; PATH="/mingw64/bin:$PATH" CI_SKIP_CHECK=true CANBUILD_WITH_LIBMODBUS_USB=yes WITH_LIBNUTPRIVATE=true NUT_SSL_VARIANTS=nss ./ci_build.sh --with-docs=no' after_build: diff --git a/ci_build.sh b/ci_build.sh index ddd4159c22..f21387bd27 100755 --- a/ci_build.sh +++ b/ci_build.sh @@ -35,6 +35,11 @@ SCRIPT_ARGS=("$@") # in a different directory and then it would be used with a warning. This may # require that you `make distclean` the original source checkout first: # CI_BUILDDIR=obj BUILD_TYPE=default-all-errors ./ci_build.sh +# +# The NUT_SSL_VARIANTS=[yes, no, auto, ssl, nss, openssl] values can be used +# with generic builds (not only iteration of a default-all-errors* matrix) +# to set specific SSL options in tested NUT builds. +# case "$BUILD_TYPE" in fightwarn) ;; # for default compiler fightwarn-all) @@ -1947,7 +1952,20 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al case "$BUILD_TYPE" in "default-all-errors"*) ;; # Treated below - *) configure_nut ;; + *) # Final choices that can conflict with the matrix + # tried in default-all-errors* builds + case "${NUT_SSL_VARIANTS}" in + ssl|nss|openssl) + CONFIG_OPTS+=("--with-${NUT_SSL_VARIANTS}") + ;; + yes) CONFIG_OPTS+=("--with-ssl") ;; + no) CONFIG_OPTS+=("--without-ssl") ;; + auto) CONFIG_OPTS+=("--with-ssl=auto") ;; + "") ;; + *) echo "WARNING: Unrecognized NUT_SSL_VARIANTS='${NUT_SSL_VARIANTS}' for a general deterministic build, ignored" >&2 ;; + esac + configure_nut + ;; esac # NOTE: There is also a case "$BUILD_TYPE" above for setting CONFIG_OPTS @@ -2842,6 +2860,17 @@ bindings) CONFIG_OPTS+=("--enable-shared-private-libs") fi + case "${NUT_SSL_VARIANTS}" in + ssl|nss|openssl) + CONFIG_OPTS+=("--with-${NUT_SSL_VARIANTS}") + ;; + yes) CONFIG_OPTS+=("--with-ssl") ;; + no) CONFIG_OPTS+=("--without-ssl") ;; + auto) CONFIG_OPTS+=("--with-ssl=auto") ;; + "") ;; + *) echo "WARNING: Unrecognized NUT_SSL_VARIANTS='${NUT_SSL_VARIANTS}' for a general deterministic build, ignored" >&2 ;; + esac + if [ -n "${BUILD_DEBUGINFO-}" ]; then CONFIG_OPTS+=("--with-debuginfo=${BUILD_DEBUGINFO}") else @@ -2987,6 +3016,8 @@ cross-windows-mingw*) fi # else we have some value from caller export WITH_LIBNUTPRIVATE + export NUT_SSL_VARIANTS + SOURCEMODE="out-of-tree" \ MAKEFLAGS="$PARMAKE_FLAGS" \ KEEP_NUT_REPORT_FEATURE="true" \ diff --git a/clients/Makefile.am b/clients/Makefile.am index ab81516a16..659dfdd51e 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -163,11 +163,21 @@ upsstats_cgi_LDADD = $(LDADD_CLIENT) $(top_builddir)/common/libcommonstrjson.la ################################## Plain C client library (libupsclient) : -# not LDADD... why? -libupsclient_la_SOURCES = upsclient.c upsclient.h # NOTE: The library does not require libcommonversion.la +# but it needs nut_version.h made before the rest of build, +# to include it into upsclient.c (without an explicit link, +# this target is sometimes missed in parallel builds): +libupsclient_la_SOURCES = $(top_builddir)/include/nut_version.h $(srcdir)/upsclient.c upsclient.h +### $(srcdir)/upsclient.c: $(top_builddir)/include/nut_version.h + +# not LDADD... why? +if ENABLE_SHARED_PRIVATE_LIBS +libupsclient_la_LIBADD = \ + $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la +else !ENABLE_SHARED_PRIVATE_LIBS libupsclient_la_LIBADD = \ $(top_builddir)/common/libcommonclient.la +endif !ENABLE_SHARED_PRIVATE_LIBS if HAVE_WINDOWS_SOCKETS libupsclient_la_LIBADD += -lws2_32 endif HAVE_WINDOWS_SOCKETS @@ -181,6 +191,15 @@ endif WITH_SSL # object .so names would differ) # libupsclient version information +# NOTE: with libnutprivate*common* builds dynamically linked into the same +# program (a typical in-tree NUT client, not typical for out-of-tree third +# party clients) we can end up with two copies of libcommon symbols present +# in each library. It's recommended to explicitly call upscli_set_debug_level() +# instead of ambiguously manipulating the nut_debug_level variable by name. +# TOTHINK: Un-export nut_debug_level from this library to avoid ambiguity +# for the run-time dynamic linker resolution? For now the shared-library +# builds are "exotic", but it makes sense to deprecate this export in a +# future release. libupsclient_la_LDFLAGS = -version-info 7:0:0 libupsclient_la_LDFLAGS += -export-symbols-regex '^(upscli_|nut_debug_level)' #|s_upsdebug|fatalx|fatal_with_errno|xcalloc|xbasename|print_banner_once)' @@ -207,6 +226,7 @@ endif HAVE_WINDOWS # ways to skip this rebuild noise for common NUT parallel `make -j N all` runs. CLEANFILES += libupsclient-version.h libupsclient-version.h.tmp* libupsclient-version.h: libupsclient.la + @test -s '$?' || { echo "[Error] Missing or empty input file: $?" >&2 ; exit 1; } @if [ -s '$@' -a -s '$?' ] ; then \ if test -n "`find '$@' -newer '$?' 2>/dev/null`" ; then \ if [ x"$(MAINTAINER_GENERATE_HEADER_DEBUG)" = xyes ] ; then \ diff --git a/clients/upsc.c b/clients/upsc.c index f3736b8a4f..eaca4d834d 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -2,7 +2,7 @@ Copyright (C) 1999 Russell Kroll Copyright (C) 2012 Arnaud Quette - Copyright (C) 2020-2025 Jim Klimov + Copyright (C) 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 @@ -48,14 +48,14 @@ static void fatalx_error_json_simple(int msg_is_simple, const char *msg) { if (output_json) { if (msg_is_simple) { /* Caller knows there is nothing to escape here, pass through */ - printf("{\"error\": \"%s\"}\n", msg); + printf("{\"error\": \"%s\"}\n", NUT_STRARG(msg)); } else { printf("{\"error\": \""); json_print_esc(msg); printf("\"}\n"); } } - fatalx(EXIT_FAILURE, "Error: %s", msg); + fatalx(EXIT_FAILURE, "Error: %s", NUT_STRARG(msg)); } static void usage(const char *prog) @@ -91,6 +91,7 @@ static void usage(const char *prog) printf(" -h - display this help text\n"); nut_report_config_flags(); + upscli_report_build_details(); printf("\n%s", suggest_doc_links(prog, NULL)); } @@ -168,6 +169,8 @@ static void list_vars(void) int msg_is_simple = 1; /* check for an old upsd */ + upsdebugx(1, "%s: got code %d, upserror %d", + __func__, ret, upscli_upserror(ups)); if (upscli_upserror(ups) == UPSCLI_ERR_UNKCOMMAND) { msg = "upsd is too old to support this query"; } else { @@ -177,14 +180,14 @@ static void list_vars(void) if (output_json) { if (msg_is_simple) { - printf(" \"error\": \"%s\"\n}\n", msg); + printf(" \"error\": \"%s\"\n}\n", NUT_STRARG(msg)); } else { printf(" \"error\": \""); json_print_esc(msg); printf("\"\n}\n"); } } - fatalx(EXIT_FAILURE, "Error: %s", msg); + fatalx(EXIT_FAILURE, "Error: %s", NUT_STRARG(msg)); } while (upscli_list_next(ups, numq, query, &numa, &answer) == 1) { @@ -237,6 +240,8 @@ static void list_upses(int verbose) int msg_is_simple = 1; /* check for an old upsd */ + upsdebugx(1, "%s: got code %d, upserror %d", + __func__, ret, upscli_upserror(ups)); if (upscli_upserror(ups) == UPSCLI_ERR_UNKCOMMAND) { msg = "upsd is too old to support this query"; } else { @@ -394,6 +399,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } upsdebugx(1, "Starting NUT client: %s", prog); diff --git a/clients/upsclient.c b/clients/upsclient.c index deebb7fbda..4db525a3a1 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -3,7 +3,7 @@ Copyright (C) 2002 Russell Kroll 2008 Arjen de Korte - 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 @@ -96,6 +96,9 @@ # define shutdown_how 2 #endif +#include "nut_version.h" +static const char *UPSCLI_VERSION = NUT_VERSION_MACRO; + static struct { int flags; const char *str; @@ -337,20 +340,25 @@ static void HandshakeCallback(PRFileDesc *fd, UPSCONN_t *client_data) int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd) { - const char *quiet_init_ssl; + const char *quiet_init_ssl; #ifdef WITH_OPENSSL - long ret; - int ssl_mode = SSL_VERIFY_NONE; + long ret; + int ssl_mode = SSL_VERIFY_NONE; + /* Now these are technically "used" below for the log messaging. + * Keeping macros here to remind devs that these arguments are + * not really used by library code in this variant of the build. + */ NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certpasswd); -#elif defined(WITH_NSS) /* WITH_OPENSSL */ +#elif defined(WITH_NSS) /* WITH_OPENSSL */ SECStatus status; -#else +#else /* neither backend: */ + /* See comment above */ NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(certpath); NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certpasswd); -#endif /* WITH_OPENSSL | WITH_NSS */ +#endif /* WITH_OPENSSL | WITH_NSS */ if (upscli_initialized == 1) { upslogx(LOG_WARNING, "upscli already initialized"); @@ -378,6 +386,10 @@ int upscli_init(int certverify, const char *certpath, #ifdef WITH_OPENSSL + if (certname || certpasswd) { + upslogx(LOG_ERR, "upscli_init called with 'certname' and/or 'certpasswd' but OpenSSL backend does not use them"); + } + # if OPENSSL_VERSION_NUMBER < 0x10100000L SSL_load_error_strings(); SSL_library_init(); @@ -489,9 +501,12 @@ int upscli_init(int certverify, const char *certpath, verify_certificate = certverify; #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. */ - upslogx(LOG_ERR, "upscli_init called but SSL wasn't compiled in"); + if (certverify || certpath || certname || certpasswd) { + upslogx(LOG_ERR, "upscli_init called but SSL wasn't compiled in"); + } #endif /* WITH_OPENSSL | WITH_NSS */ upscli_initialized = 1; @@ -513,6 +528,8 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(forcessl); + + upsdebugx(1, "%s: no-op when libupsclient was not built WITH_NSS", __func__); #endif /* WITH_NSS */ } @@ -530,6 +547,8 @@ static HOST_CERT_t* upscli_find_host_cert(const char* hostname) } #else NUT_UNUSED_VARIABLE(hostname); + + upsdebugx(4, "%s: no-op when libupsclient was not built WITH_NSS", __func__); #endif /* WITH_NSS */ return NULL; } @@ -577,7 +596,7 @@ const char *upscli_strerror(UPSCONN_t *ups) return upscli_errlist[UPSCLI_ERR_INVALIDARG].str; } - if (ups->upserror > UPSCLI_ERR_MAX) { + if (ups->upserror < 0 || ups->upserror > UPSCLI_ERR_MAX) { return "Invalid error number"; } @@ -593,7 +612,7 @@ const char *upscli_strerror(UPSCONN_t *ups) "%s", strerror(ups->syserrno)); return ups->errbuf; - case 2: /* SSL error */ + case 2: /* SSL error, with 1 arg */ #ifdef WITH_OPENSSL err = ERR_get_error(); if (err) { @@ -609,12 +628,19 @@ const char *upscli_strerror(UPSCONN_t *ups) "%s", "peer disconnected"); } #elif defined(WITH_NSS) /* WITH_OPENSSL */ - if (PR_GetErrorTextLength() < UPSCLI_ERRBUF_LEN) { - PR_GetErrorText(ups->errbuf); + if (PR_GetErrorTextLength() > 0 && PR_GetErrorTextLength() + strlen(upscli_errlist[ups->upserror].str) < UPSCLI_ERRBUF_LEN) { + char errbuf[UPSCLI_ERRBUF_LEN]; + memset(errbuf, 0, UPSCLI_ERRBUF_LEN); + PR_GetErrorText(errbuf); + snprintf_dynamic( + ups->errbuf, UPSCLI_ERRBUF_LEN, + upscli_errlist[ups->upserror].str, + "%s", errbuf); } else { snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN, - "SSL error #%ld, message too long to be displayed", - (long)PR_GetError()); + "SSL error #%ld, message too %s to be displayed", + (long)PR_GetError(), + PR_GetErrorTextLength() > 0 ? "long" : "short"); } #else snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN, @@ -811,23 +837,30 @@ static ssize_t net_write(UPSCONN_t *ups, const char *buf, size_t buflen, const t # pragma GCC diagnostic pop #endif - -#ifdef WITH_SSL - /* - * 1 : OK + * 1 : OK * -1 : ERROR - * 0 : SSL NOT SUPPORTED + * 0 : SSL NOT SUPPORTED (whether by library or by server) */ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) { -#ifdef WITH_OPENSSL +#ifndef WITH_SSL + NUT_UNUSED_VARIABLE(ups); + NUT_UNUSED_VARIABLE(verifycert); + + upsdebugx(1, "%s: no-op when libupsclient was not built WITH_SSL", __func__); + + return 0; /* not supported */ + +#else /* WITH_SSL */ + +# ifdef WITH_OPENSSL int res; -#elif defined(WITH_NSS) /* WITH_OPENSSL */ +# elif defined(WITH_NSS) /* WITH_OPENSSL */ SECStatus status; PRFileDesc *socket; HOST_CERT_t *cert; -#endif /* WITH_OPENSSL | WITH_NSS */ +# endif /* WITH_OPENSSL | WITH_NSS */ char buf[UPSCLI_NETBUF_LEN]; /* Intend to initialize upscli with no ssl db if not already done. @@ -856,7 +889,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) /* upsd is happy, so let's crank up the client */ -#ifdef WITH_OPENSSL +# ifdef WITH_OPENSSL if (!ssl_ctx) { upsdebugx(3, "SSL context is not available"); @@ -898,7 +931,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) return 1; -#elif defined(WITH_NSS) /* WITH_OPENSSL */ +# elif defined(WITH_NSS) /* WITH_OPENSSL */ socket = PR_ImportTCPSocket(ups->fd); if (socket == NULL){ @@ -987,20 +1020,9 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) return 1; -#endif /* WITH_OPENSSL | WITH_NSS */ -} - -#else /* WITH_SSL */ - -static int upscli_sslinit(UPSCONN_t *ups, int verifycert) -{ - NUT_UNUSED_VARIABLE(ups); - NUT_UNUSED_VARIABLE(verifycert); - - return 0; /* not supported */ -} - +# endif /* WITH_OPENSSL | WITH_NSS */ #endif /* WITH_SSL */ +} int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval * timeout) { @@ -1252,7 +1274,9 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags upslogx(LOG_INFO, "Connected to NUT server %s in SSL", host); if (certverify == 0) { /* you REALLY should set CERTVERIFY to 1 if using SSL... */ - upslogx(LOG_WARNING, "Certificate verification is disabled"); + upslogx(LOG_WARNING, "Certificate verification (by client) is disabled"); + } else { + upsdebugx(1, "Certificate verification (by client) is enabled, and apparently succeeded"); } } } @@ -1881,6 +1905,55 @@ int upscli_ssl(UPSCONN_t *ups) return 0; } +/* Return a bitmap of the abilities for the current libupsclient build */ +int upscli_ssl_caps(void) +{ + int ret = UPSCLI_SSL_CAPS_NONE; + +#ifdef WITH_SSL +# ifdef WITH_OPENSSL + ret |= UPSCLI_SSL_CAPS_OPENSSL; +# endif +# ifdef WITH_NSS + ret |= UPSCLI_SSL_CAPS_NSS; +# endif +#endif /* WITH_SSL */ + + return ret; +} + +/* String version (English) for program help banners etc. */ +const char *upscli_ssl_caps_descr(void) +{ + static const char *ret = "with" +#ifndef WITH_SSL + "out SSL support"; +#else /* WITH_SSL */ + " SSL support: " +# ifdef WITH_OPENSSL + "OpenSSL" +# ifdef WITH_NSS + /* Not likely we'd get here, but... */ + " and " +# endif +# endif +# ifdef WITH_NSS + "Mozilla NSS" +# endif +# if !(defined WITH_NSS) && !(defined WITH_OPENSSL) + "oddly undefined" +# endif + ; +#endif /* WITH_SSL */ + + return ret; +} + +void upscli_report_build_details(void) +{ + upsdebugx(1, "Using NUT libupsclient library version %s built %s", UPSCLI_VERSION, upscli_ssl_caps_descr()); +} + int upscli_set_default_connect_timeout(const char *secs) { double fsecs; @@ -2003,3 +2076,20 @@ int upscli_str_add_unique_token(char *tgt, size_t tgtsize, const char *token, ) { return str_add_unique_token(tgt, tgtsize, token, callback_always, callback_unique); } + +/* On some platforms, libupsclient builds tend to get a built-in copy + * of the internal code from NUT libcommon library, so for NUT client + * programs using both libraries as dynamically-linked shared code, + * the nut_debug_level setting is backed by independent variables in + * active memory, and upsdebugx() calls suffer if the library's copy + * is never changed from zero. + */ +void upscli_set_debug_level(int lvl) +{ + nut_debug_level = lvl; +} + +int upscli_get_debug_level(void) +{ + return nut_debug_level; +} diff --git a/clients/upsclient.h b/clients/upsclient.h index 6990b44eee..ea80550bc6 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -2,7 +2,7 @@ Copyright (C) 2002 Russell Kroll - 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 @@ -99,6 +99,16 @@ typedef struct { const char *upscli_strerror(UPSCONN_t *ups); +/* On some platforms, libupsclient builds tend to get a built-in copy + * of the internal code from NUT libcommon library, so for NUT client + * programs using both libraries as dynamically-linked shared code, + * the nut_debug_level setting is backed by independent variables in + * active memory, and upsdebugx() calls suffer if the library's copy + * is never changed from zero. + */ +void upscli_set_debug_level(int lvl); +int upscli_get_debug_level(void); + /* NOTE: effectively only runs once; re-runs quickly skip out */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd); int upscli_cleanup(void); @@ -140,6 +150,16 @@ int upscli_upserror(UPSCONN_t *ups); /* returns 1 if SSL mode is active for this connection */ int upscli_ssl(UPSCONN_t *ups); +#define UPSCLI_SSL_CAPS_NONE 0 /* No ability to use SSL */ +#define UPSCLI_SSL_CAPS_OPENSSL 1 /* Can use OpenSSL-specific setup */ +#define UPSCLI_SSL_CAPS_NSS 2 /* Can use Mozilla NSS-specific setup */ + +/* Return a bitmap of the above for the current libupsclient build */ +int upscli_ssl_caps(void); +/* String version for program help banners etc. */ +const char *upscli_ssl_caps_descr(void); +void upscli_report_build_details(void); + /* Assign default upscli_connect() from string; return 0 if OK, or * return -1 if parsing failed and current value was kept */ int upscli_set_default_connect_timeout(const char *secs); diff --git a/clients/upscmd.c b/clients/upscmd.c index 561d7d5bb6..0206918595 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -74,6 +74,7 @@ static void usage(const char *prog) printf(" -h - display this help text\n"); nut_report_config_flags(); + upscli_report_build_details(); printf("\n%s", suggest_doc_links(prog, "upsd.users")); } @@ -305,6 +306,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } upsdebugx(1, "Starting NUT client: %s", prog); diff --git a/clients/upsimage.c b/clients/upsimage.c index 18d7cf882b..d3399d938d 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -645,6 +645,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } #ifdef NUT_CGI_DEBUG_UPSIMAGE @@ -654,6 +655,7 @@ int main(int argc, char **argv) # endif /* Un-comment via make flags when developer-troubleshooting: */ nut_debug_level = NUT_CGI_DEBUG_UPSIMAGE; + upscli_set_debug_level(nut_debug_level); #endif if (nut_debug_level > 0) { diff --git a/clients/upslog.c b/clients/upslog.c index c5c6a9a038..b8561203d9 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -2,7 +2,7 @@ Copyright (C) 1998 Russell Kroll - 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 @@ -239,6 +239,7 @@ static void help(const char *prog) printf("%s\n", DEFAULT_LOGFORMAT); nut_report_config_flags(); + upscli_report_build_details(); printf("\n%s", suggest_doc_links(prog, NULL)); @@ -537,6 +538,7 @@ int main(int argc, char **argv) case 'D': nut_debug_level++; + upscli_set_debug_level(nut_debug_level); break; case 'm': { /* var scope */ diff --git a/clients/upsmon.c b/clients/upsmon.c index 1e29250ce9..f00d01ba00 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -4,7 +4,7 @@ 1998 Russell Kroll 2012 Arnaud Quette 2017 Eaton (author: Arnaud Quette ) - 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 @@ -2670,6 +2670,7 @@ static void loadconfig(void) nut_debug_level_args); nut_debug_level = nut_debug_level_args; } + upscli_set_debug_level(nut_debug_level); if (pollfail_log_throttle_max >= 0) { upslogx(LOG_INFO, @@ -3407,12 +3408,14 @@ static void help(const char *arg_progname) nut_debug_level = -2; nut_debug_level_args = -2; nut_debug_level_global = -2; + upscli_set_debug_level(nut_debug_level); loadconfig(); nut_debug_level = old_debug_level; nut_debug_level_args = old_debug_level_args; nut_debug_level_global = old_debug_level_global; + upscli_set_debug_level(nut_debug_level); /* Separate from logs emitted by loadconfig() */ /* printf("\n"); */ @@ -3446,6 +3449,7 @@ static void help(const char *arg_progname) printf(" -h - display this help text\n"); nut_report_config_flags(); + upscli_report_build_details(); printf("\n%s", suggest_doc_links(arg_progname, "upsmon.conf")); @@ -3847,6 +3851,7 @@ int main(int argc, char *argv[]) case 'D': nut_debug_level++; nut_debug_level_args++; + upscli_set_debug_level(nut_debug_level); break; case 'F': foreground = 1; @@ -3915,6 +3920,7 @@ int main(int argc, char *argv[]) nut_debug_level_args = l; } /* else follow -D settings */ } /* else nothing to bother about */ + upscli_set_debug_level(nut_debug_level); } if (cmd) { @@ -4064,8 +4070,10 @@ int main(int argc, char *argv[]) if (checking_flag) { /* Do not normally report the UPSes we would monitor, etc. * from loadconfig() for just checking the killpower flag */ - if (nut_debug_level == 0) + if (nut_debug_level == 0) { nut_debug_level = -2; + upscli_set_debug_level(nut_debug_level); + } } loadconfig(); @@ -4074,8 +4082,10 @@ int main(int argc, char *argv[]) * in upsmon.conf. Note that non-zero debug_min does not impact * foreground running mode. */ - if (nut_debug_level_global > nut_debug_level) + if (nut_debug_level_global > nut_debug_level) { nut_debug_level = nut_debug_level_global; + upscli_set_debug_level(nut_debug_level); + } upsdebugx(1, "debug level is '%d'", nut_debug_level); if (checking_flag) diff --git a/clients/upsrw.c b/clients/upsrw.c index 797445eeee..c5ab5fa2ff 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -75,6 +75,7 @@ static void usage(const char *prog) printf("Call without -s to show all possible read/write variables (same as -l).\n"); nut_report_config_flags(); + upscli_report_build_details(); printf("\n%s", suggest_doc_links(prog, "upsd.users")); } @@ -663,6 +664,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } upsdebugx(1, "Starting NUT client: %s", prog); diff --git a/clients/upsset.c b/clients/upsset.c index 3074bf63ef..36d231f7d3 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1149,6 +1149,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } #ifdef NUT_CGI_DEBUG_UPSSET @@ -1158,6 +1159,7 @@ int main(int argc, char **argv) # endif /* Un-comment via make flags when developer-troubleshooting: */ nut_debug_level = NUT_CGI_DEBUG_UPSSET; + upscli_set_debug_level(nut_debug_level); #endif if (nut_debug_level > 0) { diff --git a/clients/upsstats.c b/clients/upsstats.c index d630769c8c..55ad9b8cf4 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -1709,6 +1709,7 @@ int main(int argc, char **argv) s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &i, 10) && i > 0) { nut_debug_level = i; + upscli_set_debug_level(nut_debug_level); } @@ -1719,6 +1720,7 @@ int main(int argc, char **argv) # endif /* Un-comment via make flags when developer-troubleshooting: */ nut_debug_level = NUT_CGI_DEBUG_UPSSTATS; + upscli_set_debug_level(nut_debug_level); #endif if (nut_debug_level > 0) { diff --git a/common/common-nut_version.c b/common/common-nut_version.c index cbaf506955..a30b31ee31 100644 --- a/common/common-nut_version.c +++ b/common/common-nut_version.c @@ -43,7 +43,11 @@ # define suggest_doc_links LIBNUTPRIVATE_suggest_doc_links # define suggest_NDE_conflict LIBNUTPRIVATE_suggest_NDE_conflict # else /* !BUILD_FOR_SHARED_PRIVATE_LIBS => for leaf binary */ - extern const char *LIBNUTPRIVATE_UPS_VERSION; + /* Refer to UPS_VERSION => LIBNUTPRIVATE_UPS_VERSION (renamed by macro + * above) as exported by the library build so a NUT program can see + * the source code version of libnutprivate-XYZ-...so binary library. + */ + extern const char *LIBNUTPRIVATE_UPS_VERSION; # endif /* !BUILD_FOR_SHARED_PRIVATE_LIBS */ #endif @@ -63,6 +67,8 @@ * depends on nut_version.h), and also to prevent all sources from having * to be recompiled each time the version changes (they only need to be * re-linked). Similarly for other variables below in the code. + * NOTE: Depending on build circumstances, the symbol may in fact be named + * LIBNUTPRIVATE_UPS_VERSION, or not (when built into leaf binaries). */ #include "nut_version.h" const char *UPS_VERSION = NUT_VERSION_MACRO; diff --git a/conf/upsd.conf.sample b/conf/upsd.conf.sample index f03b67b480..81f70a999e 100644 --- a/conf/upsd.conf.sample +++ b/conf/upsd.conf.sample @@ -170,16 +170,20 @@ # CERTREQUEST # CERTREQUEST REQUIRE # -# When compiled with SSL support with NSS backend and client certificate +# When compiled with SSL support with NSS backend *and* client certificate # validation (disabled by default, see 'docs/security.txt'), -# you can specify if upsd requests or requires client's' certificates. +# you can specify if upsd requests or requires clients' certificates. # Possible values are : -# - 0 to not request to clients to provide any certificate -# - 1 to require to all clients a certificate -# - 2 to require to all clients a valid certificate +# - '0' or 'NO' to not request that clients provide any certificate +# - '1' or 'REQUEST' to require all clients to present some certificate +# - '2' or 'REQUIRE' to require all clients to present a valid certificate +# (trusted by server database) # # See 'docs/security.txt' or the Security chapter of NUT user manual # for more information on the SSL support in NUT. +# +# WARNING: Until https://github.com/networkupstools/nut/issues/3329 is solved, +# not all stock NUT clients even have a way to present some certain certificate. # ======================================================================= # DISABLE_WEAK_SSL diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index 1a0be42969..0353fa8a72 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -608,6 +608,7 @@ ALARMCRITICAL 1 # When compiled with SSL support with NSS, you can specify the certificate # name to retrieve from database to authenticate itself and the password # required to access certificate related private key. +# Currently no-op when built with OpenSSL. # # CERTIDENT "my nut monitor" "MyPasSw0rD" # @@ -623,6 +624,7 @@ ALARMCRITICAL 1 # Each entry maps server name with the expected certificate name and flags # indicating if the server certificate is verified and if the connection # must be secure. +# Currently no-op when built with OpenSSL. # # CERTHOST localhost "My nut server" 1 1 # diff --git a/configure.ac b/configure.ac index f894b2b766..b26269f038 100644 --- a/configure.ac +++ b/configure.ac @@ -2840,6 +2840,7 @@ nut_ssl_lib="" NUT_ARG_WITH([ssl], [enable SSL support (either NSS or OpenSSL)], [auto]) NUT_ARG_WITH([nss], [enable SSL support using Mozilla NSS], [auto]) NUT_ARG_WITH([openssl], [enable SSL support using OpenSSL], [auto]) +NUT_ARG_WITH([ssl-client-validation], [enable SSL client certificate validation ability (currently requires NSS, beware issue #3329)], [auto]) dnl ${nut_with_ssl}: any value except "yes" or "no" is treated as "auto". if test "${nut_with_ssl}" != "no"; then @@ -2882,6 +2883,18 @@ AM_CONDITIONAL(WITH_OPENSSL, test "${nut_with_openssl}" = "yes") NUT_REPORT_FEATURE([enable SSL support], [${nut_with_ssl}], [${nut_ssl_lib}], [WITH_SSL], [Define to enable SSL]) +AS_CASE([${nut_with_ssl_client_validation}], + [no], [], + [yes|""], [nut_with_ssl_client_validation="yes"], + dnl TOTHINK: reject if openssl/none build? + [auto|*], [nut_with_ssl_client_validation="no"] + dnl Until #3329 is solved, auto==no +) +NUT_REPORT_FEATURE([enable SSL client certificate validation], + [${nut_with_ssl_client_validation}], [], + [WITH_CLIENT_CERTIFICATE_VALIDATION], + [Define to enable SSL client certificate validation]) + dnl ---------------------------------------------------------------------- dnl Check for presence and compiler flags of various libraries diff --git a/docs/config-prereqs.txt b/docs/config-prereqs.txt index 9a22df0ad0..8d3c091cd3 100644 --- a/docs/config-prereqs.txt +++ b/docs/config-prereqs.txt @@ -255,9 +255,11 @@ metadata about recently published package revisions: # NOTE: Some older Debian-like distributions, could ship "libcrypto-dev" # and/or "openssl-dev" instead of "libssl-dev" by its modern name # and may lack a libgpiod2 + libgpiod-dev altogether +# For NIT tests of SSL support you would also want key store management tools. :; apt-get install \ libcppunit-dev \ - libssl-dev libnss3-dev \ + libssl-dev openssl \ + libnss3-dev libnss3-tools \ augeas-tools libaugeas-dev augeas-lenses \ libusb-dev libusb-1.0-0-dev \ libglib2.0-dev \ @@ -269,6 +271,9 @@ metadata about recently published package revisions: libavahi-common-dev libavahi-core-dev libavahi-client-dev # For libneon, see below +# NOTE: On Termux the -dev suffixes may be missing, so you would need e.g. +:; pkg install libnss nss-utils + # NOTE: Older Debian-like distributions may lack a "libgpiod-dev" # Others above are present as far back as Debian 7 at least :; apt-get install \ @@ -1451,6 +1456,10 @@ Typical tooling would include: freeipmi \ avahi +# For tests with NSS, the certutil tool is needed: +:; pkg install \ + system/mozilla-nss + # With 2024.04, some packages were split for development vs. run-time # and/or based on architecture: :; pkg install \ @@ -2024,6 +2033,12 @@ export PATH mingw-w64-x86_64-libgd \ mingw-w64-clang-x86_64-libgd +# For SSL support (note that OpenSSL is usually available +# in the default MSYS2 footprint, but may get upgraded here): +:; pacman -S --needed \ + mingw-w64-x86_64-openssl \ + mingw-w64-x86_64-nss + # For C++ tests: :; pacman -S --needed \ mingw-w64-x86_64-cppunit \ diff --git a/docs/configure.txt b/docs/configure.txt index 0b70cf5cfc..ccb3702cd1 100644 --- a/docs/configure.txt +++ b/docs/configure.txt @@ -789,10 +789,21 @@ Enable SSL support, using either Mozilla NSS or OpenSSL. If both are present, and nothing was specified, OpenSSL support will be preferred. -Read link:docs/security.txt[] for instructions on SSL support. - NOTE: Currently the two implementations differ in supported features. + --with-ssl-client-validation (default: no) + +Enable the `upsd` server-side ability to set up `CERTREQUEST` directive in +`upsd.conf`, which lets the server reject clients which do not present a +certificate. + +WARNING: Until link:https://github.com/networkupstools/nut/issues/3329[issue +#3329] is solved, not all stock NUT clients may even have a way to present +some certain certificate. + +Read link:docs/security.txt[] for instructions on practical setup of SSL +support. + Networking access security ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 444710adfe..e5a74e8a18 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -544,10 +544,13 @@ SRC_DEV_PAGES = \ upscli_list_next.txt \ upscli_list_start.txt \ upscli_readline.txt \ + upscli_report_build_details.txt \ upscli_sendline.txt \ + upscli_set_debug_level.txt \ upscli_splitaddr.txt \ upscli_splitname.txt \ upscli_ssl.txt \ + upscli_ssl_caps.txt \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_str_add_unique_token.txt \ @@ -649,6 +652,16 @@ LIBNUTCLIENT_DEVICES_DEPS = \ $(LIBNUTCLIENT_DEVICES_DEPS): libnutclient_devices.$(MAN_SECTION_API) touch $@ +UPSCLI_SSL_CAPS_DEPS = \ + upscli_ssl_caps_descr.$(MAN_SECTION_API) + +$(UPSCLI_SSL_CAPS_DEPS): upscli_ssl_caps.$(MAN_SECTION_API) + +UPSCLI_SET_DEBUG_LEVEL_DEPS = \ + upscli_get_debug_level.$(MAN_SECTION_API) + +$(UPSCLI_SET_DEBUG_LEVEL_DEPS): upscli_set_debug_level.$(MAN_SECTION_API) + INST_MAN_DEV_API_PAGES = \ upsclient.$(MAN_SECTION_API) \ upscli_add_host_cert.$(MAN_SECTION_API) \ @@ -666,11 +679,16 @@ INST_MAN_DEV_API_PAGES = \ upscli_list_start.$(MAN_SECTION_API) \ upscli_readline.$(MAN_SECTION_API) \ upscli_readline_timeout.$(MAN_SECTION_API) \ + upscli_report_build_details.$(MAN_SECTION_API) \ upscli_sendline.$(MAN_SECTION_API) \ upscli_sendline_timeout.$(MAN_SECTION_API) \ + upscli_set_debug_level.$(MAN_SECTION_API) \ + $(UPSCLI_SET_DEBUG_LEVEL_DEPS) \ upscli_splitaddr.$(MAN_SECTION_API) \ upscli_splitname.$(MAN_SECTION_API) \ upscli_ssl.$(MAN_SECTION_API) \ + upscli_ssl_caps.$(MAN_SECTION_API) \ + $(UPSCLI_SSL_CAPS_DEPS) \ upscli_strerror.$(MAN_SECTION_API) \ upscli_upserror.$(MAN_SECTION_API) \ upscli_str_add_unique_token.$(MAN_SECTION_API) \ @@ -781,10 +799,13 @@ INST_HTML_DEV_MANS = \ upscli_list_next.html \ upscli_list_start.html \ upscli_readline.html \ + upscli_report_build_details.html \ upscli_sendline.html \ + upscli_set_debug_level.html \ upscli_splitaddr.html \ upscli_splitname.html \ upscli_ssl.html \ + upscli_ssl_caps.html \ upscli_strerror.html \ upscli_upserror.html \ upscli_str_add_unique_token.html \ diff --git a/docs/man/upscli_add_host_cert.txt b/docs/man/upscli_add_host_cert.txt index 10192b6688..ef88874164 100644 --- a/docs/man/upscli_add_host_cert.txt +++ b/docs/man/upscli_add_host_cert.txt @@ -29,9 +29,12 @@ The rule is composed of the certificate name 'certname' expected for the host, 'certverify' if the certificate must be validated for the host and 'forcessl' if a secured connection must be used to connect to the host. -Note: This call only functions if upsclient has been compiled with NSS -support. If instead it was compiled with OpenSSL support, this function -contains an empty definition and will take no action when called. +NOTE: This call currently only functions if `libupsclient` has been compiled +with NSS support. If instead it was compiled with OpenSSL support, this method +contains an empty definition and will take no action when called. Starting with +NUT v2.8.5, you can use linkman:upscli_ssl_caps[3] from consumer code to query +the library about its build circumstances (whether it supports SSL, and which +backend(s)). RETURN VALUE ------------ @@ -41,5 +44,6 @@ RETURN VALUE SEE ALSO -------- -linkman:upscli_init[3], linkman:upscli_connect[3], linkman:upscli_ssl[3], +linkman:upscli_init[3], linkman:upscli_connect[3], +linkman:upscli_ssl[3], linkman:upscli_ssl_caps[3], linkman:upscli_strerror[3], linkman:upscli_upserror[3] diff --git a/docs/man/upscli_init.txt b/docs/man/upscli_init.txt index 37b0e69094..85c012d675 100644 --- a/docs/man/upscli_init.txt +++ b/docs/man/upscli_init.txt @@ -69,6 +69,10 @@ security policy before initialize connections to them. You must call linkman:upscli_cleanup[3] when exiting application. +Starting with NUT v2.8.5, you can use linkman:upscli_ssl_caps[3] from consumer +code to query the library about its build circumstances (whether it supports +SSL, and which backend(s)). + RETURN VALUE ------------ @@ -82,5 +86,6 @@ 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_splitaddr[3], linkman:upscli_splitname[3], -linkman:upscli_ssl[3], linkman:upscli_strerror[3], -linkman:upscli_upserror[3] +linkman:upscli_ssl[3], linkman:upscli_ssl_caps[3], +linkman:upscli_ssl_caps_descr[3], +linkman:upscli_strerror[3], linkman:upscli_upserror[3] diff --git a/docs/man/upscli_report_build_details.txt b/docs/man/upscli_report_build_details.txt new file mode 100644 index 0000000000..d184cafa6b --- /dev/null +++ b/docs/man/upscli_report_build_details.txt @@ -0,0 +1,34 @@ +UPSCLI_REPORT_BUILD_DETAILS(3) +============================== + +NAME +---- + +upscli_report_build_details - Print a debug message to report details of the +current libupsclient build such as its source-code version and SSL capabilities + +SYNOPSIS +-------- + +------ + #include + + void upscli_report_build_details(void); +------ + +DESCRIPTION +----------- + +The *upscli_report_build_details*() function prints a debug log message at +verbosity '1' to summarize details about the currently used build of the +linkman:upsclient[3] library, including its SSL support. + +RETURN VALUE +------------ + +There is no return value. + +SEE ALSO +-------- + +linkman:upscli_ssl_caps[3], linkman:upscli_ssl_caps_descr[3] diff --git a/docs/man/upscli_set_debug_level.txt b/docs/man/upscli_set_debug_level.txt new file mode 100644 index 0000000000..f7e057283e --- /dev/null +++ b/docs/man/upscli_set_debug_level.txt @@ -0,0 +1,51 @@ +UPSCLI_SET_DEBUG_LEVEL(3) +========================= + +NAME +---- + +upscli_set_debug_level, upscli_get_debug_level - manipulate the possibly +separate copy of the `nut_debug_level` variable in the `libupsclient` build + +SYNOPSIS +-------- + +------ + #include + + void upscli_set_debug_level(int); + int upscli_get_debug_level(void); +------ + +DESCRIPTION +----------- + +On some platforms, 'libupsclient' builds tend to get a built-in copy +of the internal code from NUT 'libcommon' library, so for NUT client +programs using both libraries as dynamically-linked shared code, +the `nut_debug_level` setting is backed by independent variables in +active memory, and `upsdebugx()` calls suffer if the library's copy +is never changed from zero. + +These methods allow to set or retrieve the value of `nut_debug_level` +setting known by the 'libupsclient' library, regardless of build mode. + +The most likely use (at least in NUT programs) is to call +`upscli_set_debug_level(nut_debug_level);` after changing the +original variable. Values of the debugging level are zero to disable +debug, may be negative for a few special cases, and generally are +positive numbers to cut off the more verbose logging attempts; +otherwise it is up to the code base and NUT style guide practices +to assign certain levels to some classes of messages. + +RETURN VALUE +------------ + +There is no return value for the setter. + +The getter returns the current value of the internal variable. + +SEE ALSO +-------- + +linkman:upscli_init[3], linkman:upscli_report_build_details[3] diff --git a/docs/man/upscli_ssl.txt b/docs/man/upscli_ssl.txt index 6113de4cc4..fac04866c4 100644 --- a/docs/man/upscli_ssl.txt +++ b/docs/man/upscli_ssl.txt @@ -23,6 +23,10 @@ The *upscli_ssl*() function takes the pointer 'ups' to a compiled into the linkman:upsclient[3] library, and if it was successfully enabled for this connection. +Starting with NUT v2.8.5, you can use linkman:upscli_ssl_caps[3] from consumer +code to query the library about its build circumstances (whether it supports +SSL, and which backend(s)). + RETURN VALUE ------------ @@ -32,6 +36,7 @@ not. It returns '-1' in the event of an error. SEE ALSO -------- +linkman:upscli_ssl_caps[3], linkman:upscli_ssl_caps_descr[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/upscli_ssl_caps.txt b/docs/man/upscli_ssl_caps.txt new file mode 100644 index 0000000000..0970ea8947 --- /dev/null +++ b/docs/man/upscli_ssl_caps.txt @@ -0,0 +1,55 @@ +UPSCLI_SSL_CAPS(3) +================== + +NAME +---- + +upscli_ssl_caps, upscli_ssl_caps_descr - Report SSL capabilities of the +current libupsclient build as a bitmap or string + +SYNOPSIS +-------- + +------ + #include + + /* Header above defines bitmap values: + * UPSCLI_SSL_CAPS_NONE - No ability to use SSL + * UPSCLI_SSL_CAPS_OPENSSL - Can use OpenSSL-specific setup + * UPSCLI_SSL_CAPS_NSS - Can use Mozilla NSS-specific setup + */ + + int upscli_ssl_caps(void); + const char *upscli_ssl_caps_descr(void); +------ + +DESCRIPTION +----------- + +The *upscli_ssl_caps*() function returns an `int` as a bitmap of SSL capability +flags which may be of interest to the consumer of the linkman:upsclient[3] +library (e.g. how to set up cryptographic material storage, and whether that +should have any effect when initializing connections). + +The *upscli_ssl_caps_descr*() function returns a string with such information +primarily intended for humans, e.g. to be used in NUT client program banners. + +If there is no built-in SSL support, alternative methods like SSH tunnels or +the `stunnel` tool can be explored. + +RETURN VALUE +------------ + +The *upscli_ssl_caps*() function effectively returns '0' if SSL support is +missing, '1' if OpenSSL can be used, or '2' if Mozilla NSS can be used, +since currently the backends are exclusive and it matters because they +are differently capable. + +In the future it may return '-1' in the event of an error, or e.g. '3' if +both OpenSSL and Mozilla NSS could be supported simultaneously (current code +structure does not allow for that though). + +SEE ALSO +-------- + +linkman:upscli_ssl[3], linkman:upscli_report_build_details[3] diff --git a/docs/man/upsd.conf.txt b/docs/man/upsd.conf.txt index f6d538e7c6..c42170399d 100644 --- a/docs/man/upsd.conf.txt +++ b/docs/man/upsd.conf.txt @@ -188,15 +188,20 @@ are using a value with spaces in it. *CERTREQUEST 'certificate request level'*:: -When compiled with SSL support with NSS backend and client certificate +When compiled with SSL support with NSS backend *and* client certificate validation (disabled by default, see `docs/security.txt` in NUT sources), -you can specify if `upsd` requests or requires clients' certificates. +you can specify whether `upsd` requests or requires clients' certificates. ++ +WARNING: Until link:https://github.com/networkupstools/nut/issues/3329[issue +#3329] is solved, not all stock NUT clients may even have a way to present +some certain certificate. + Possible values are: + -- '0' to not request to clients to provide any certificate -- '1' to require to all clients a certificate -- '2' to require to all clients a valid certificate +- '0' or 'NO' to not request that clients provide any certificate +- '1' or 'REQUEST' to require all clients to present *some* certificate +- '2' or 'REQUIRE' to require all clients to present a valid certificate + (with auth chain trusted by server database) *DISABLE_WEAK_SSL 'BOOLEAN'*:: diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index 0772be515e..aeba57bb8b 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -600,6 +600,8 @@ When compiled with SSL support with NSS, you can specify the certificate name to retrieve from database to authenticate itself and the password required to access certificate related private key. + +Currently no-op when built with OpenSSL. ++ NOTE: Be sure to enclose "certificate name" in double-quotes if you are using a value with spaces in it. @@ -612,6 +614,8 @@ Each entry maps server name with the expected certificate name and flags indicating if the server certificate is verified and if the connection must be secure. + +Currently no-op when built with OpenSSL. ++ NOTE: Be sure to enclose "certificate name" in double-quotes if you are using a value with spaces in it. diff --git a/docs/nut.dict b/docs/nut.dict index 6f295f2b46..86b73968fa 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3687 utf-8 +personal_ws-1.1 en 3691 utf-8 AAC AAS ABI @@ -304,6 +304,7 @@ DeviceKit DeviceLogin DeviceLogout Dgtk +Dh Dharm Diehl Dietze @@ -1907,6 +1908,7 @@ crossbuild crt crw crypto +cryptographic csh cshdelay csi @@ -2205,6 +2207,7 @@ getenv gethostbyname getopt getproctag +getter gettext gettextize getval @@ -3280,6 +3283,7 @@ structs sts stst stty +stunnel stylesheet stylesheets su diff --git a/docs/security.txt b/docs/security.txt index 6a96d4f8ac..49811992db 100644 --- a/docs/security.txt +++ b/docs/security.txt @@ -314,6 +314,16 @@ You can choose to use one of them by specifying it with a build option (`--with-nss` or `--with-openssl`). If neither is specified, the configure script will try to detect one of them, with a precedence for OpenSSL. +Starting with NUT v2.8.5, you can check the actual support of your NUT build +with `upsd -Dh` for the server, and `upsmon -Dh` for the typical client, e.g.: + +------ + 0.000000 [D1] Network UPS Tools version 2.8.4.... + 0.019319 [D1] NUT data server was built with SSL support: OpenSSL; + without client certificate validation +------ + + OpenSSL backend usage ~~~~~~~~~~~~~~~~~~~~~ @@ -582,25 +592,30 @@ a hard rule. The certificate can be named as useful. upsd (optional): client authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -NOTE: This functionality is disabled by default. To activate it, recompile -NUT with `WITH_CLIENT_CERTIFICATE_VALIDATION` defined: +NOTE: This functionality is disabled by default. To activate it, initially +`configure --with-ssl-client-validation` or just recompile NUT with macro +`WITH_CLIENT_CERTIFICATE_VALIDATION` defined: make CFLAGS="-DWITH_CLIENT_CERTIFICATE_VALIDATION" UPSD can accept three levels of client authentication. Just specify it with -the directive `CERTREQUEST` with the corresponding value in the `upsd.conf` -file: +the directive `CERTREQUEST` with the corresponding numeric or string value +in the `upsd.conf` file: -- NO: no client authentication. -- REQUEST: a certificate is request to the client but it is not strictly -validated. -If the client does not send any certificate, the connection is closed. -- REQUIRE: a certificate is requested to the client and if it is not valid -(no validation chain) the connection is closed. +- ('0') 'NO': no client authentication. +- ('1') 'REQUEST': a certificate is requested from the client, but it is not + strictly validated. ++ +NOTE: If the client does not send *any* certificate, the connection is closed. +- ('2') 'REQUIRE': a certificate is requested from the client, and if it is + not valid (no trusted validation chain) the connection is closed. Like CA certificates, you can add many "trusted" client and CA certificates in server's certificate databases. +WARNING: Until link:https://github.com/networkupstools/nut/issues/3329[issue +#3329] is solved, not all stock NUT clients may even have a way to present +some certain certificate. upsmon (required): upsd authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/drivers/Makefile.am b/drivers/Makefile.am index ee1d90cde8..93b1e4d927 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -12,6 +12,7 @@ # Make sure out-of-dir dependencies exist (especially when dev-building parts): $(top_builddir)/include/nut_version.h \ $(top_builddir)/common/libcommon.la \ +$(top_builddir)/common/libcommonclient.la \ $(top_builddir)/common/libcommonversion.la \ $(top_builddir)/common/libparseconf.la \ $(top_builddir)/clients/libupsclient.la: dummy @dotMAKE@ @@ -19,11 +20,10 @@ $(top_builddir)/clients/libupsclient.la: dummy @dotMAKE@ # Builds from root dir arrange stuff decently. Make sure parallel builds # started from scratch right in this dir get dependencies in proper order -# (sub-makes are independent as far as trying to write into same files): -$(top_builddir)/common/libcommon.la: $(top_builddir)/common/libparseconf.la +# (sub-makes are independent as far as trying to write into same files). # Internally (clients/Makefile.am) libupsclient.la requires libcommonclient.la -# and that in turn libparseconf.la: -$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libparseconf.la +# (or the shared libnutprivate equivalent), and that in turn libparseconf.la: +$(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonclient.la: $(top_builddir)/common/libparseconf.la $(top_builddir)/common/libcommonversion.la: $(top_builddir)/include/nut_version.h if ENABLE_SHARED_PRIVATE_LIBS @@ -35,7 +35,11 @@ $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-comm $(top_builddir)/common/libcommonversion-private.la: $(top_builddir)/include/nut_version.h $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion-private.la $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/common/libcommonversion-private.la -endif ENABLE_SHARED_PRIVATE_LIBS + +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la $(top_builddir)/include/nut_version.h +else !ENABLE_SHARED_PRIVATE_LIBS +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/include/nut_version.h +endif !ENABLE_SHARED_PRIVATE_LIBS lib_LTLIBRARIES = diff --git a/drivers/dummy-ups.c b/drivers/dummy-ups.c index a4b455b64a..863740890e 100644 --- a/drivers/dummy-ups.c +++ b/drivers/dummy-ups.c @@ -2,7 +2,7 @@ Copyright (C) 2005 - 2015 Arnaud Quette - 2014 - 2025 Jim Klimov + 2014 - 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 @@ -48,7 +48,7 @@ #include "dummy-ups.h" #define DRIVER_NAME "Device simulation and repeater driver" -#define DRIVER_VERSION "0.23" +#define DRIVER_VERSION "0.24" /* driver description structure */ upsdrv_info_t upsdrv_info = @@ -120,6 +120,8 @@ void upsdrv_initinfo(void) { dummy_info_t *item; + upscli_set_debug_level(nut_debug_level); + switch (mode) { case MODE_DUMMY_ONCE: @@ -266,6 +268,8 @@ void upsdrv_updateinfo(void) { upsdebugx(1, "upsdrv_updateinfo..."); + upscli_set_debug_level(nut_debug_level); + sleep(1); switch (mode) @@ -415,6 +419,8 @@ static int instcmd(const char *cmdname, const char *extra) void upsdrv_help(void) { + upscli_set_debug_level(nut_debug_level); + upscli_report_build_details(); } /* optionally tweak prognames[] entries */ @@ -432,6 +438,8 @@ void upsdrv_initups(void) { const char *val; + upscli_set_debug_level(nut_debug_level); + val = dstate_getinfo("driver.parameter.mode"); if (val) { if (!strcmp(val, "dummy-loop") diff --git a/drivers/main.c b/drivers/main.c index 9f42910978..847afc435e 100644 --- a/drivers/main.c +++ b/drivers/main.c @@ -164,13 +164,29 @@ static int handle_reload_flag(void); /* Set in do_ups_confargs() for consumers like handle_reload_flag() */ static int reload_requires_restart = -1; -#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC) -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_BRACES_BESIDEFUNC) ) +#pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_BRACES_BESIDEFUNC +#pragma GCC diagnostic ignored "-Wmissing-braces" #endif +/* Note about the pragmas: for pedantic warnings, the sub-arrays in our struct + * (struct_magic and padding) are "sub-objects" that should all have their own + * initializers, e.g. { {0}, 0, ..., 0, {0} } which would be insane to maintain. + * Newer C standards require that a statically initialized object is zeroed by + * the compiler (so `= {0}` part is not needed at all), but I guess NUT with + * decades of backwards compatibility should not rely on that being the case + * with each and every one of 1990's compilers. Likewise, this initializer + * should ensure that all memory in the struct is zeroed out, and so does the + * init_upsdrv_callbacks() macro. So belts and suspenders, better safe than + * sorry, and all that. + */ static upsdrv_callback_t upsdrv_callbacks = {0}; -#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC) -# pragma GCC diagnostic pop +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_BRACES_BESIDEFUNC) ) +#pragma GCC diagnostic pop #endif void register_upsdrv_callbacks(upsdrv_callback_t *runtime_callbacks, size_t cb_struct_sz) { /* Plain memcpy of arbitrarily ordered list of function pointers @@ -179,17 +195,17 @@ void register_upsdrv_callbacks(upsdrv_callback_t *runtime_callbacks, size_t cb_s * shared library, years apart from a driver that tries to use it) * was built against: */ #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_ADDRESS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)) -# pragma GCC diagnostic push +#pragma GCC diagnostic push #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_ADDRESS -# pragma GCC diagnostic ignored "-Waddress" +#pragma GCC diagnostic ignored "-Waddress" #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE -# pragma GCC diagnostic ignored "-Wunreachable-code" +#pragma GCC diagnostic ignored "-Wunreachable-code" #endif safe_copy_upsdrv_callbacks(runtime_callbacks, &upsdrv_callbacks, cb_struct_sz); #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_ADDRESS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)) -# pragma GCC diagnostic pop +#pragma GCC diagnostic pop #endif } diff --git a/drivers/main.h b/drivers/main.h index e7e2cb00d4..e9ced7078e 100644 --- a/drivers/main.h +++ b/drivers/main.h @@ -218,11 +218,12 @@ typedef struct upsdrv_callback_s { * generations of NUT drivers try to link with the * main() logic shipped as a shared library aimed * at its particular release */ + char struct_magic[16]; /* should be UPSDRV_CALLBACK_MAGIC */ + uint64_t struct_version; /* how to interpret what we see */ uint64_t ptr_size; /* be sure about bitness */ uint64_t ptr_count; /* amount of non-null pointers passed here */ - char struct_magic[16]; - /* char NUT_VERSION[32]; / * Just have it built into each binary */ + /* char NUT_VERSION[32]; / * TO THINK: Just have it built into each binary */ /* Do not change the order of these entries, * only add at the end of list (if needed). @@ -258,43 +259,55 @@ typedef struct upsdrv_callback_s { } upsdrv_callback_t; void register_upsdrv_callbacks(upsdrv_callback_t *runtime_callbacks, size_t cb_struct_sz); -/* simple call to register implementations named as dictated - * by this header, which (being a macro) can be called easily - * from both static and shared builds; keep in mind that builds - * using these macros for binaries that try to fit together may - * be years apart eventually. Note that consumers may have to - * use HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_ADDRESS and/or - * some of HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE* - * pragmas around these macros, for builds to succeed under - * stricter compiler settings (see examples in existing code). +/* Suite of simple calls to register driver callback method implementations + * named as dictated by this header, which (being macros) can be called easily + * from both static and shared builds (with libnutprivate-X_Y_Z-drivers-common). + * Keep in mind that practical builds using these macros for binaries that try + * to fit together may be years apart eventually. + * + * Note that consumers may have to use HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_ADDRESS + * and/or some of HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE* pragmas + * around these macros, for builds to succeed under stricter compiler settings + * (see examples in existing code). + */ + +/* Initialize an upsdrv_callback_t* cbptr memory block sized cbsz (should + * be sizeof(upsdrv_callback_t)) with basic metadata (version, ARCH info). + * Claims to serve the 9 non-NULL method pointers currently defined by the + * NUT driver coding standard, and some more from common_nut-version.c, + * named in the current structure version definition above (which are in fact + * NULL just after this call -- so we must use validate_upsdrv_callbacks() + * before actually referencing or calling that stuff). */ #define init_upsdrv_callbacks(cbptr, cbsz) do { \ size_t cbptr_counter = 0; \ if ((cbptr) == NULL) fatalx(EXIT_FAILURE, "Could not init callbacks for shared driver code: null structure pointer"); \ if ((cbsz) != sizeof(upsdrv_callback_t)) fatalx(EXIT_FAILURE, "Could not init callbacks for shared driver code: unexpected structure size"); \ memset((cbptr), 0, sizeof(upsdrv_callback_t)); \ + snprintf((cbptr)->struct_magic, sizeof((cbptr)->struct_magic), "%s", UPSDRV_CALLBACK_MAGIC); \ (cbptr)->struct_version = 1; \ (cbptr)->ptr_size = sizeof(void*); \ - (cbptr)->ptr_count = 9; \ + (cbptr)->ptr_count = 16; \ (cbptr)->sentinel = NULL; \ for (cbptr_counter = 0; cbptr_counter < UPSDRV_CALLBACK_PADDING; cbptr_counter++) \ (cbptr)->padding[cbptr_counter] = NULL; \ - snprintf((cbptr)->struct_magic, sizeof((cbptr)->struct_magic), "%s", UPSDRV_CALLBACK_MAGIC); \ } while (0) +/* Make sure metadata seems valid and method pointers are not NULL (if !isnew), + * or crash with reasonable diagnostics */ #define validate_upsdrv_callbacks(cbptr, cbsz, isnew) do { \ upsdebugx(5, "validate_upsdrv_callbacks: cbsz=%" PRIuMAX, (uintmax_t)cbsz); \ if ((cbptr) == NULL) fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: null structure pointer"); \ if ((cbsz) != sizeof(upsdrv_callback_t)) fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: unexpected structure size"); \ + if (strcmp((cbptr)->struct_magic, UPSDRV_CALLBACK_MAGIC)) \ + fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: wrong magic"); \ upsdebugx(5, "validate_upsdrv_callbacks: ver=%" PRIu64 " ptr_count=%" PRIu64, (cbptr)->struct_version, (cbptr)->ptr_count); \ if ((cbptr)->struct_version != 1 \ - || (cbptr)->ptr_count != 9 \ + || (cbptr)->ptr_count != 16 \ ) fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: unexpected structure contents"); \ upsdebugx(5, "validate_upsdrv_callbacks: ptr_size: passed=%" PRIu64 " expected=%" PRIuSIZE, (cbptr)->ptr_size, sizeof(void*)); \ if ((cbptr)->ptr_size != sizeof(void*)) \ fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: wrong structure bitness"); \ - if (strcmp((cbptr)->struct_magic, UPSDRV_CALLBACK_MAGIC)) \ - fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: wrong magic"); \ upsdebugx(5, "validate_upsdrv_callbacks: NULL-check: sentinel: %s", (cbptr)->sentinel == NULL ? "Y" : "N"); \ if ((cbptr)->sentinel != NULL) \ fatalx(EXIT_FAILURE, "Could not register callbacks for shared driver code: wrong sentinels"); \ @@ -334,6 +347,7 @@ void register_upsdrv_callbacks(upsdrv_callback_t *runtime_callbacks, size_t cb_s if (isnew) upsdebugx(5, "validate_upsdrv_callbacks: this is a newly created structure, so some/all NULL references are okay"); \ } while (0) +/* Populate library's copy of callbacks and other info with what the driver tells us */ #define safe_copy_upsdrv_callbacks(cbptrDrv, cbptrLib, cbszDrv) do { \ validate_upsdrv_callbacks(cbptrDrv, cbszDrv, 0); \ init_upsdrv_callbacks(cbptrLib, sizeof(upsdrv_callback_t)); \ @@ -356,6 +370,10 @@ void register_upsdrv_callbacks(upsdrv_callback_t *runtime_callbacks, size_t cb_s (cbptrLib)->suggest_NDE_conflict = (cbptrDrv)->suggest_NDE_conflict; \ } while (0) +/* Simplify life for driver implementations which have the legacy-named methods + * required by NUT coding standards (including test code that partially poses + * as a driver). + */ #define default_register_upsdrv_callbacks() do { \ upsdrv_callback_t callbacksTmp; \ init_upsdrv_callbacks(&callbacksTmp, sizeof(callbacksTmp)); \ diff --git a/m4/ax_c_pragmas.m4 b/m4/ax_c_pragmas.m4 index 9800f03fcc..b93b63dede 100644 --- a/m4/ax_c_pragmas.m4 +++ b/m4/ax_c_pragmas.m4 @@ -923,6 +923,36 @@ dnl ### [CFLAGS="${CFLAGS_SAVED} -Werror=pragmas -Werror=unknown-warning" AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_FIELD_INITIALIZERS_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wmissing-field-initializers" (outside functions)]) ]) + dnl Older GCC and CLANG complain about later-standardized `something={0}` + dnl syntax of initialization which zeroes out (struct) bytes even with a + dnl non-nested struct like upsdrv_callback_t: + AC_CACHE_CHECK([for pragma GCC diagnostic ignored "-Wmissing-braces"], + [ax_cv__pragma__gcc__diags_ignored_missing_braces], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[void func(void) { +#pragma GCC diagnostic ignored "-Wmissing-braces" +} +]], [])], + [ax_cv__pragma__gcc__diags_ignored_missing_braces=yes], + [ax_cv__pragma__gcc__diags_ignored_missing_braces=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_missing_braces" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_BRACES], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wmissing-braces"]) + ]) + + AC_CACHE_CHECK([for pragma GCC diagnostic ignored "-Wmissing-braces" (outside functions)], + [ax_cv__pragma__gcc__diags_ignored_missing_braces_besidefunc], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#pragma GCC diagnostic ignored "-Wmissing-braces"]], [])], + [ax_cv__pragma__gcc__diags_ignored_missing_braces_besidefunc=yes], + [ax_cv__pragma__gcc__diags_ignored_missing_braces_besidefunc=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_missing_braces_besidefunc" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_MISSING_BRACES_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wmissing-braces" (outside functions)]) + ]) + AC_CACHE_CHECK([for pragma GCC diagnostic ignored "-Wassign-enum"], [ax_cv__pragma__gcc__diags_ignored_assign_enum], [AC_COMPILE_IFELSE( diff --git a/scripts/Windows/build-mingw-nut.sh b/scripts/Windows/build-mingw-nut.sh index aca64aa86b..e17aff65fc 100755 --- a/scripts/Windows/build-mingw-nut.sh +++ b/scripts/Windows/build-mingw-nut.sh @@ -158,6 +158,17 @@ do_build_mingw_nut() { ENABLE_NUT_SHARED_PRIVATE_LIBS_FLAG="--enable-shared-private-libs" fi + WITH_SSL_FLAG="" + case "${NUT_SSL_VARIANTS}" in + ssl|nss|openssl) + WITH_SSL_FLAG="--with-${NUT_SSL_VARIANTS}" ;; + yes) WITH_SSL_FLAG="--with-ssl" ;; + no) WITH_SSL_FLAG="--without-ssl" ;; + auto) WITH_SSL_FLAG="--with-ssl=auto" ;; + "") ;; + *) echo "WARNING: Unrecognized NUT_SSL_VARIANTS='${NUT_SSL_VARIANTS}' for a general deterministic build, ignored" >&2 ;; + esac + # Note: installation prefix here is "/" and desired INSTALL_DIR # location is passed to `make install` as DESTDIR below. # WIN32 builds resolve PREFIX'ed paths relative to each current binary @@ -171,6 +182,7 @@ do_build_mingw_nut() { $CONFIGURE_SCRIPT $HOST_FLAG $BUILD_FLAG --prefix=/ \ $KEEP_NUT_REPORT_FEATURE_FLAG \ $ENABLE_NUT_SHARED_PRIVATE_LIBS_FLAG \ + $WITH_SSL_FLAG \ PKG_CONFIG_PATH="${ARCH_PREFIX}/lib/pkgconfig" \ --with-all=auto \ --with-doc="man=auto html-single=auto html-chunked=skip pdf=skip" \ diff --git a/server/conf.c b/server/conf.c index b2edf311b6..89a804b7b4 100644 --- a/server/conf.c +++ b/server/conf.c @@ -1,6 +1,8 @@ /* conf.c - configuration handlers for upsd - Copyright (C) 2001 Russell Kroll + Copyright (C) + 2001 Russell Kroll + 2019 - 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 @@ -285,6 +287,18 @@ static int parse_upsd_conf_args(size_t numargs, char **arg) return 1; } else { + if (!strcmp(arg[1], "NO")) { + certrequest = NETSSL_CERTREQ_NO; /* 0 */ + return 1; + } + if (!strcmp(arg[1], "REQUEST")) { + certrequest = NETSSL_CERTREQ_REQUEST; /* 1 */ + return 1; + } + if (!strcmp(arg[1], "REQUIRE")) { + certrequest = NETSSL_CERTREQ_REQUIRE; /* 2 */ + return 1; + } upslogx(LOG_ERR, "CERTREQUEST has non numeric value (%s)!", arg[1]); return 0; } diff --git a/server/netssl.c b/server/netssl.c index 5a48201a87..70ad6639e8 100644 --- a/server/netssl.c +++ b/server/netssl.c @@ -3,6 +3,7 @@ Copyright (C) 2002 Russell Kroll 2008 Arjen de Korte + 2020 - 2026 Jim Klimov based on the original implementation: @@ -67,11 +68,43 @@ char *certpasswd = NULL; int disable_weak_ssl = 0; #ifdef WITH_CLIENT_CERTIFICATE_VALIDATION -int certrequest = 0; +int certrequest = 0; #endif /* WITH_CLIENT_CERTIFICATE_VALIDATION */ static int ssl_initialized = 0; +/* Similar to upscli_ssl_caps_descr() for client library, + * but with more bells and whistles */ +const char *net_ssl_caps_descr(void) +{ + static const char *ret = "with" +#ifndef WITH_SSL + "out SSL support"; +#else + " SSL support: " +# ifdef WITH_OPENSSL + "OpenSSL" +# ifdef WITH_NSS + /* Not likely we'd get here, but... */ + " and " +# endif +# endif +# ifdef WITH_NSS + "Mozilla NSS" +# endif +# if !(defined WITH_NSS) && !(defined WITH_OPENSSL) + "oddly undefined" +# endif + "; with" +# ifndef WITH_CLIENT_CERTIFICATE_VALIDATION + "out" +# endif /* WITH_CLIENT_CERTIFICATE_VALIDATION */ + " client certificate validation"; +#endif + + return ret; +} + #ifndef WITH_SSL /* stubs for non-ssl compiles */ @@ -428,7 +461,19 @@ void net_starttls(nut_ctype_t *client, size_t numarg, const char **arg) if (status != SECSuccess) { PRErrorCode code = PR_GetError(); if (code==SSL_ERROR_NO_CERTIFICATE) { - upslogx(LOG_WARNING, "Client %s do not provide certificate.", +# ifdef WITH_CLIENT_CERTIFICATE_VALIDATION + if (certrequest == NETSSL_CERTREQ_REQUEST + || certrequest == NETSSL_CERTREQ_REQUIRE + ) { + upslogx(LOG_ERR, "Client %s did not provide any certificate while we %s one.", + client->addr, + (certrequest == NETSSL_CERTREQ_REQUIRE ? "require" : "request") + ); + nss_error("net_starttls / SSL_ForceHandshake"); + return; + } +# endif + upslogx(LOG_WARNING, "Client %s did not provide any certificate.", client->addr); } else { nss_error("net_starttls / SSL_ForceHandshake"); @@ -604,16 +649,16 @@ void ssl_init(void) # endif /* NSS_VMAJOR */ } -# ifdef WITH_CLIENT_CERTIFICATE_VALIDATION - if (certrequest < NETSSL_CERTREQ_NO - && certrequest > NETSSL_CERTREQ_REQUEST +#ifdef WITH_CLIENT_CERTIFICATE_VALIDATION + if (certrequest < NETSSL_CERTREQ_NO /* < 0 */ + || certrequest > NETSSL_CERTREQ_REQUIRE /* > 2 */ ) { upslogx(LOG_ERR, "Invalid certificate requirement"); return; } - if (certrequest == NETSSL_CERTREQ_REQUEST - || certrequest == NETSSL_CERTREQ_REQUIRE + if (certrequest == NETSSL_CERTREQ_REQUEST /* 1 */ + || certrequest == NETSSL_CERTREQ_REQUIRE /* 2 */ ) { status = SSL_OptionSetDefault(SSL_REQUEST_CERTIFICATE, PR_TRUE); if (status != SECSuccess) { @@ -623,7 +668,7 @@ void ssl_init(void) } } - if (certrequest == NETSSL_CERTREQ_REQUIRE) { + if (certrequest == NETSSL_CERTREQ_REQUIRE) { /* 2 */ status = SSL_OptionSetDefault(SSL_REQUIRE_CERTIFICATE, PR_TRUE); if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable certificate requirement"); diff --git a/server/netssl.h b/server/netssl.h index 1cff564b16..8b27420ed1 100644 --- a/server/netssl.h +++ b/server/netssl.h @@ -33,7 +33,7 @@ extern char *certname; extern char *certpasswd; extern int disable_weak_ssl; #ifdef WITH_CLIENT_CERTIFICATE_VALIDATION -extern int certrequest; +extern int certrequest; #endif /* WITH_CLIENT_CERTIFICATE_VALIDATION */ /* List possible values for certrequested */ @@ -44,6 +44,9 @@ extern int certrequest; /* Required (cnx failed if no certificate or invalid CA chain) */ #define NETSSL_CERTREQ_REQUIRE 2 +/* Similar to upscli_ssl_caps_descr() for client library, + * but with more bells and whistles */ +const char *net_ssl_caps_descr(void); void ssl_init(void); void ssl_finish(nut_ctype_t *client); diff --git a/server/upsd.c b/server/upsd.c index fd82500f75..b843f1b5e1 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -1759,6 +1759,21 @@ static void mainloop(void) if (fds[i].revents & (POLLHUP|POLLERR|POLLNVAL)) { + upsdebug_with_errno(3, "%s: Disconnect %s%s due to%s%s%s", + __func__, + (handler[i].type==DRIVER ? "driver " : + (handler[i].type==CLIENT ? "client " : + (handler[i].type==SERVER ? "server" : + ""))), + (handler[i].type==DRIVER ? ((upstype_t *)handler[i].data)->name : + (handler[i].type==CLIENT ? ((nut_ctype_t *)handler[i].data)->addr : + (handler[i].type==SERVER ? "" : + ""))), + (fds[i].revents & POLLHUP ? " POLLHUP" : ""), + (fds[i].revents & POLLERR ? " POLLERR" : ""), + (fds[i].revents & POLLNVAL ? " POLLNVAL" : "") + ); + switch(handler[i].type) { case DRIVER: @@ -2211,6 +2226,7 @@ static void help(const char *arg_progname) printf(" -6 IPv6 only\n"); nut_report_config_flags(); + upsdebugx(1, "NUT data server was built %s", net_ssl_caps_descr()); printf("\n%s", suggest_doc_links(progname, "ups.conf, upsd.conf and upsd.users")); diff --git a/tests/NIT/Makefile.am b/tests/NIT/Makefile.am index b6c929b2bd..eac7c165ca 100644 --- a/tests/NIT/Makefile.am +++ b/tests/NIT/Makefile.am @@ -24,6 +24,15 @@ endif !WITH_CHECK_NIT ABS_TOP_SRCDIR = @ABS_TOP_SRCDIR@ ABS_TOP_BUILDDIR = @ABS_TOP_BUILDDIR@ +# Early in issue #1711 OpenSSL tests failed more often than we liked, so +# this toggle was added. It may still be useful for CI on some platforms. +# Developers can run the script directly or override the make variable. +if WITH_OPENSSL +WITHOUT_SSL_TESTS = true +else !WITH_OPENSSL +WITHOUT_SSL_TESTS = false +endif !WITH_OPENSSL + # Run in builddir, use script from srcdir # Avoid running "$<" - not all make implementations handle that check-NIT: $(abs_srcdir)/nit.sh @@ -33,6 +42,7 @@ check-NIT: $(abs_srcdir)/nit.sh abs_top_srcdir='$(abs_top_srcdir)' abs_top_builddir='$(abs_top_builddir)' \ ABS_TOP_SRCDIR='$(ABS_TOP_SRCDIR)' ABS_TOP_BUILDDIR='$(ABS_TOP_BUILDDIR)' \ EXEEXT='$(EXEEXT)' \ + WITHOUT_SSL_TESTS='$(WITHOUT_SSL_TESTS)' \ "$(abs_srcdir)/nit.sh" # Make sure pre-requisites for NIT are fresh as we iterate @@ -57,6 +67,7 @@ check-NIT-sandbox: $(abs_srcdir)/nit.sh abs_top_srcdir='$(abs_top_srcdir)' abs_top_builddir='$(abs_top_builddir)' \ ABS_TOP_SRCDIR='$(ABS_TOP_SRCDIR)' ABS_TOP_BUILDDIR='$(ABS_TOP_BUILDDIR)' \ EXEEXT='$(EXEEXT)' \ + WITHOUT_SSL_TESTS='$(WITHOUT_SSL_TESTS)' \ "$(abs_srcdir)/nit.sh" check-NIT-sandbox-devel: $(abs_srcdir)/nit.sh @dotMAKE@ diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 43df6e47fc..82a7700543 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -75,6 +75,7 @@ LANG=C LC_ALL=C export TZ LANG LC_ALL +# May flip to "false" if SSL tests are enabled NUT_QUIET_INIT_SSL="true" export NUT_QUIET_INIT_SSL @@ -337,7 +338,16 @@ if [ x"${TOP_BUILDDIR}" = x ] || [ ! -d "${TOP_BUILDDIR}" ] ; then case "${BUILDDIR}" in */tests/NIT) TOP_BUILDDIR="`cd \"${BUILDDIR}\"/../.. && pwd`" ;; - *) log_info "Current directory '${BUILDDIR}' is not a .../tests/NIT" ;; + *) if [ -x ./tests/NIT/nit.sh ] ; then + TOP_BUILDDIR="`pwd`" + else + if [ -x ./NIT/nit.sh ] ; then + TOP_BUILDDIR="`cd .. && pwd`" + else + log_info "Current directory '${BUILDDIR}' is not a .../tests/NIT or similar" + fi + fi + ;; esac log_info "Guessing TOP_BUILDDIR='${TOP_BUILDDIR}' from script location and/or BUILDDIR value..." else @@ -438,6 +448,68 @@ PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" +WITH_SSL_CLIENT="`upsmon -Dh 2>&1 | grep 'Using NUT libupsclient library'`" || WITH_SSL_CLIENT="none" +# NOTE: Currently OpenSSL/NSS builds and codepaths are exclusive of each other! +# Interesting idea: build and test server with one and clients with the other... +# SIDE NOTE: As of NUT v2.8.5, it seems that only upsmon client cares about SSL! +case "${WITH_SSL_CLIENT}" in + *"without SSL"*|none|"") WITH_SSL_CLIENT="none" ;; + *OpenSSL*) WITH_SSL_CLIENT="OpenSSL" ;; + *NSS*) WITH_SSL_CLIENT="NSS" ;; + *) log_warn "Unexpected client SSL support reported, ignoring: ${WITH_SSL_CLIENT}" ; WITH_SSL_CLIENT="none" ;; +esac +log_info "Tested client binaries SSL support: ${WITH_SSL_CLIENT}" + +WITH_SSL_SERVER="`upsd -Dh 2>&1 | grep 'NUT data server was built with'`" || WITH_SSL_SERVER="none" +WITH_SSL_SERVER_CLIVAL="none" +case "${WITH_SSL_SERVER}" in + *"without client certificate validation"*) WITH_SSL_SERVER_CLIVAL="false" ;; + *"with client certificate validation"*) WITH_SSL_SERVER_CLIVAL="true" ;; +esac +case "${WITH_SSL_SERVER}" in + *"without SSL"*|none|"") WITH_SSL_SERVER="none" ;; + *OpenSSL*) WITH_SSL_SERVER="OpenSSL" ;; + *NSS*) WITH_SSL_SERVER="NSS" ;; + *) log_warn "Unexpected server SSL support reported, ignoring: ${WITH_SSL_SERVER}" ; WITH_SSL_SERVER="none" ;; +esac +log_info "Tested server binaries SSL support: ${WITH_SSL_SERVER}" +log_info "Tested server binaries client certificate validation: ${WITH_SSL_SERVER_CLIVAL}" + +if [ x"${WITHOUT_SSL_TESTS}" = xtrue ]; then + log_info "Disabling SSL tests (even if they are possible) due to WITHOUT_SSL_TESTS='${WITHOUT_SSL_TESTS}'" + WITH_SSL_CLIENT="none" + WITH_SSL_SERVER="none" +fi + +case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *NSS*) + (command -v certutil) || { + log_warn "NUT can use NSS, but needed third-party tooling was not found to produce the crypto credential stores" + if [ x"${WITH_SSL_CLIENT}" = xNSS ] ; then WITH_SSL_CLIENT="none" ; fi + if [ x"${WITH_SSL_SERVER}" = xNSS ] ; then WITH_SSL_SERVER="none" ; fi + } + ;; +esac + +case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *OpenSSL*) + (command -v openssl) || { + log_warn "NUT can use OpenSSL, but needed third-party tooling was not found to produce the crypto credential stores" + if [ x"${WITH_SSL_CLIENT}" = xOpenSSL ] ; then WITH_SSL_CLIENT="none" ; fi + if [ x"${WITH_SSL_SERVER}" = xOpenSSL ] ; then WITH_SSL_SERVER="none" ; fi + } + ;; +esac + +TESTCERT_ROOTCA_NAME="NUT Mock Root CA" +TESTCERT_ROOTCA_PASS="VeryS@cur@1337" +TESTCERT_CLIENT_NAME="NIT upsmon" +TESTCERT_CLIENT_PASS="MyPasSw0rD" +TESTCERT_SERVER_NAME="NIT data server" +TESTCERT_SERVER_PASS="TestS@rv!" +# Continued below regarding setup of crypto material data files +# (and possibly skipping SSL tests if we fail this setup). + # Platforms vary in abilities to report this... I_AM_NAME="" get_my_user_name() { @@ -761,6 +833,215 @@ export NUT_PORT # Help track collisions in log, if someone else starts a test in same directory log_info "Using NUT_PORT=${NUT_PORT} for this test run" +# Adjust path spelling to run-time platform, libraries seem to want that on WIN32 +# NOTE: Windows backslashes are pre-escaped in the configure-generated value +case "${ABS_TOP_BUILDDIR}" in + ?":\\"*) + TESTCERT_PATH_SEP='\\' + TESTCERT_PATH_BASE="${ABS_TOP_BUILDDIR}${TESTCERT_PATH_SEP}tests${TESTCERT_PATH_SEP}NIT${TESTCERT_PATH_SEP}tmp${TESTCERT_PATH_SEP}etc${TESTCERT_PATH_SEP}cert" + ;; + "") case "${TOP_BUILDDIR}" in + ?":\\"*) TESTCERT_PATH_SEP='\\' + TESTCERT_PATH_BASE="${TOP_BUILDDIR}${TESTCERT_PATH_SEP}tests${TESTCERT_PATH_SEP}NIT${TESTCERT_PATH_SEP}tmp${TESTCERT_PATH_SEP}etc${TESTCERT_PATH_SEP}cert" + ;; + *) TESTCERT_PATH_SEP="/" ;; + esac + ;; + *) TESTCERT_PATH_SEP="/" + ;; +esac + +case "${TESTCERT_PATH_SEP}" in + /) TESTCERT_PATH_BASE="${NUT_CONFPATH}${TESTCERT_PATH_SEP}cert" ;; +esac + +# SSL preparations: Can only do this after we learn NUT_CONFPATH +TESTCERT_PATH_ROOTCA="${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}rootca" +TESTCERT_PATH_SERVER="${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}upsd" +# TOTHINK: If other NUT clients get SSL support tested, +# should they use same or different cryptostore?.. +TESTCERT_PATH_CLIENT="${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}upsmon" + +# Follow docs/security.txt points about setting up the crypto material +# stores and their contents (mock a self-signed CA here where appropriate) +case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *OpenSSL*|*NSS*) + ( # Sub-shelling here to keep soft failure cases handled once, + # and changes of directory constrained without pushd/popd + # (not in all shells) or remembering of `pwd` (clumsy-ish) + log_info "Setting up crypto material storage for SSL capability tests..." + + if shouldDebug ; then + set -x + fi + set -e + + mkdir -p "${TESTCERT_PATH_ROOTCA}" + ( cd "${TESTCERT_PATH_ROOTCA}" + log_info "SSL: Preparing test Root CA..." + echo "${TESTCERT_ROOTCA_PASS}" > ".pwfile" + case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *NSS*) + { [ -e /dev/urandom ] && \ + dd if=/dev/urandom of=.random bs=16 count=1 + } || { + [ -e /dev/random ] && \ + dd if=/dev/random of=.random bs=16 count=1 + } || date > .random + # Create the certificate database: + certutil -N -d . -f .pwfile + # Generate a certificate for CA: + # HACK NOTE: The first "yes" is for "Is this a CA certificate [y/N]?" question, + # others default (empty) for possible other questions, e.g. + # Enter the path length constraint, enter to skip [<0 for unlimited path]: > + # Is this a critical extension [y/N]? : + (echo y; yes "") | certutil -S -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -s "CN=${TESTCERT_ROOTCA_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -t "CT,," -x -2 -z .random + # Extract the CA certificate to be able to use or import it later: + certutil -L -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -a -o rootca.pem + # Use this later for signing, move on to server/client requests... + + ls -l "${TESTCERT_PATH_ROOTCA}"/*.db "${TESTCERT_PATH_ROOTCA}"/*.txt + ;; + esac + + [ -s rootca.pem ] || \ + case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *OpenSSL*) + # Generate an AES encrypted private key: + openssl genrsa -aes256 -out rootca.key -passout file:.pwfile 4096 + # Generate a certificate for CA using that key: + MSYS_NO_PATHCONV=1 \ + openssl req -x509 -new -nodes -key rootca.key -passin file:.pwfile -sha256 -days 1826 -out rootca.pem -subj "/CN=${TESTCERT_ROOTCA_NAME}/OU=Test/O=NIT/ST=StateOfChaos/C=US" + ;; + esac + + case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *OpenSSL*) + # OpenSSL CA trust "database" should include hashes + # of CA PEM certificates as symlinks to actual files: + CERTHASH="`openssl x509 -subject_hash -in rootca.pem | head -1`" + ln -s rootca.pem "${CERTHASH}".0 + ln -s rootca.pem "${CERTHASH}" + ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/${CERTHASH}"* + ;; + *) + ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem + ;; + esac + + ) + + mkdir -p "${TESTCERT_PATH_SERVER}" + ( cd "${TESTCERT_PATH_SERVER}" + log_info "SSL: Preparing test server certificate..." + echo "${TESTCERT_SERVER_PASS}" > ".pwfile" + case "${WITH_SSL_SERVER}" in + NSS) + # Create the certificate database: + certutil -N -d . -f .pwfile + # Import the CA certificate, so users of this DB trust it: + certutil -A -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -t "TC,," -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem + # Create a server certificate request: + # NOTE: IRL Each run should have a separate random seed; for tests we cut a few corners! + certutil -R -d . -f .pwfile -s "CN=${TESTCERT_SERVER_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -a -o server.req -z "${TESTCERT_PATH_ROOTCA}"/.random + + # Sign a certificate request with the CA certificate: + # HACK NOTE: "No" for "Is this a CA certificate" question, defaults for others + (echo n; yes "") | certutil -C -d "${TESTCERT_PATH_ROOTCA}" -f "${TESTCERT_PATH_ROOTCA}"/.pwfile -c "${TESTCERT_ROOTCA_NAME}" -a -i server.req -o server.crt -2 --extKeyUsage "serverAuth" --nsCertType sslServer + + # Import the signed certificate into server database: + certutil -A -d . -f .pwfile -n "${TESTCERT_SERVER_NAME}" -a -i server.crt -t ",," + + ls -l "${TESTCERT_PATH_SERVER}"/*.db "${TESTCERT_PATH_SERVER}"/*.txt + ;; + OpenSSL) + # Create a server certificate request: + MSYS_NO_PATHCONV=1 \ + openssl req -new -nodes -out server.req -newkey rsa:4096 -passout file:.pwfile -keyout server.key -subj "/CN=${TESTCERT_SERVER_NAME}/OU=Test/O=NIT/ST=StateOfChaos/C=US" + cat > server.v3.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +DNS.2 = localhost6 +IP.1 = 127.0.0.1 +IP.2 = ::1 +EOF + # Sign a certificate request with the CA certificate: + ( cd "${TESTCERT_PATH_ROOTCA}" + openssl x509 -req -in "${TESTCERT_PATH_SERVER}/server.req" -passin file:.pwfile -CA rootca.pem -CAkey rootca.key -CAcreateserial -out "${TESTCERT_PATH_SERVER}/server.crt" -days 730 -sha256 -extfile "${TESTCERT_PATH_SERVER}/server.v3.ext" + ) + cat server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem server.key > upsd.pem + + ls -l "${TESTCERT_PATH_SERVER}"/upsd.pem + ;; + esac + ) + + mkdir -p "${TESTCERT_PATH_CLIENT}" + ( cd "${TESTCERT_PATH_CLIENT}" + case "${WITH_SSL_CLIENT}" in + NSS) + log_info "SSL: Preparing test client certificate..." + # Also create 3-file database of client key+cert store + echo "${TESTCERT_CLIENT_PASS}" > ".pwfile" + # Create the certificate database: + certutil -N -d . -f .pwfile + # Import the CA certificate, so users of this DB trust it: + certutil -A -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -t "TC,," -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem + + # Import server cert into client database so we can trust it (CERTHOST directive): + # NOTE: Seems we must do this before requesting or signing the client cert, + # otherwise (if importing server cert after doing everything about the + # client one) we get an error: + # certutil: could not decode certificate: SEC_ERROR_REUSED_ISSUER_AND_SERIAL: + # You are attempting to import a cert with the same issuer/serial + # as an existing cert, but that is not the same cert. + certutil -A -d . -f .pwfile -n "${TESTCERT_SERVER_NAME}" -a -i "${TESTCERT_PATH_SERVER}/server.crt" -t ",," + + # Create a client certificate request: + # NOTE: IRL Each run should have a separate random seed; for tests we cut a few corners! + certutil -R -d . -f .pwfile -s "CN=${TESTCERT_CLIENT_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -a -o client.req -z "${TESTCERT_PATH_ROOTCA}"/.random + + # Sign a certificate request with the CA certificate: + # HACK NOTE: "No" for "Is this a CA certificate" question, defaults for others + (echo n; yes "") | certutil -C -d "${TESTCERT_PATH_ROOTCA}" -f "${TESTCERT_PATH_ROOTCA}"/.pwfile -c "${TESTCERT_ROOTCA_NAME}" -a -i client.req -o client.crt -2 --extKeyUsage "clientAuth" --nsCertType sslClient + + # Import the signed certificate into client database: + certutil -A -d . -f .pwfile -n "${TESTCERT_CLIENT_NAME}" -a -i client.crt -t ",," + + ls -l "${TESTCERT_PATH_CLIENT}"/*.db "${TESTCERT_PATH_CLIENT}"/*.txt + ;; + OpenSSL) + # NOTE: No special keys for an OpenSSL client so far, + # it only checks/trusts a server (public data in a PEM file) + log_info "SSL: Exporting public data of server certificate for client use..." + cat "${TESTCERT_PATH_SERVER}"/server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem > upsd-public.pem + + ls -l "${TESTCERT_PATH_CLIENT}/upsd-public.pem" + ;; + esac + ) + ) || { + log_warn "Something failed about setup of crypto credential stores, will skip SSL tests" + WITH_SSL_CLIENT="none" + WITH_SSL_SERVER="none" + } + ;; +esac + +# This does not seem to cause NUT clients to trust nor distrust +# (or anyhow verify) a presented server certificate, but just in case: +#if [ "${WITH_SSL_CLIENT}" = OpenSSL ] ; then +# SSL_CERT_DIR="${TESTCERT_PATH_ROOTCA}" +# export SSL_CERT_DIR +# +# SSL_CERT_FILE="${TESTCERT_PATH_ROOTCA}/rootca.pem" +# export SSL_CERT_FILE +#fi + # This file is not used by the test code, it is an # aid for "DEBUG_SLEEP=X" mode so the caller can # quickly source needed values into their shell. @@ -776,7 +1057,7 @@ log_info "Using NUT_PORT=${NUT_PORT} for this test run" # the values when fallback is used. If this is a # problem on any platform (Win/Mac and spaces in # paths?) please investigate and fix accordingly. -set | ${EGREP} '^(NUT_|TESTDIR|LD_LIBRARY_PATH|DEBUG|PATH).*=' \ +set | ${EGREP} '^(NUT_|TESTDIR|TESTCERT|LD_LIBRARY_PATH|DEBUG|PATH).*=' \ | while IFS='=' read K V ; do case "$K" in LD_LIBRARY_PATH_CLIENT|LD_LIBRARY_PATH_ORIG|PATH_*|NUT_PORT_*|TESTDIR_*) @@ -836,6 +1117,59 @@ generatecfg_upsd_nodev() { || die "Failed to populate temporary FS structure for the NIT: upsd.conf" } +generatecfg_upsd_add_SSL() { + # May first call one of the above consumers of generatecfg_upsd_trivial() + if [ ! -s "$NUT_CONFPATH/upsd.conf" ] ; then + generatecfg_upsd_trivial + fi + + if grep CERT "$NUT_CONFPATH/upsd.conf" >/dev/null ; then + # Already configured for SSL + return 0 + fi + + case "${WITH_SSL_SERVER}" in + none) return 0;; + OpenSSL) + log_info "Adding ${WITH_SSL_SERVER} server-side SSL config to upsd.conf" + { cat << EOF +# OpenSSL CERTFILE: PEM file with data server cert, possibly the +# intermediate and root CA's, and finally corresponding private key +CERTFILE "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}upsd.pem" +EOF + } >> "$NUT_CONFPATH/upsd.conf" \ + || die "Failed to populate temporary FS structure for the NIT: upsd.conf" + ;; + NSS) + log_info "Adding ${WITH_SSL_SERVER} server-side SSL config to upsd.conf" + { cat << EOF +# NSS CERTPATH: Directory with 3-file database of cert/key store +CERTPATH "${TESTCERT_PATH_SERVER}" +CERTIDENT "${TESTCERT_SERVER_NAME}" "${TESTCERT_SERVER_PASS}" +EOF + + if [ x"${WITH_SSL_SERVER_CLIVAL}" = xtrue -a x"${WITH_SSL_CLIENT}" = xNSS ]; then + cat << EOF +# - 0 to not request clients to provide any certificate +# - 1 to require all clients to present some certificate +# - 2 to require all clients to present a valid certificate +# (trusted by server database) +CERTREQUEST 2 +EOF + fi + } >> "$NUT_CONFPATH/upsd.conf" \ + || die "Failed to populate temporary FS structure for the NIT: upsd.conf" + ;; + esac + + # FIXME: Check for old/new OS and libs to toggle this? + # echo "DISABLE_WEAK_SSL true" >> "$NUT_CONFPATH/upsd.conf" \ + # || die "Failed to populate temporary FS structure for the NIT: upsd.conf" + + NUT_QUIET_INIT_SSL=false + export NUT_QUIET_INIT_SSL +} + ### upsd.users: ################################################## TESTPASS_ADMIN='mypass' @@ -995,6 +1329,81 @@ generatecfg_upsmon_secondary() { fi } +generatecfg_upsmon_add_SSL() { + # May first call one of the above consumers of generatecfg_upsmon_trivial() + if [ ! -s "$NUT_CONFPATH/upsmon.conf" ] ; then + generatecfg_upsmon_trivial + fi + + if grep CERTPATH "$NUT_CONFPATH/upsmon.conf" >/dev/null ; then + # Already configured for SSL + return 0 + fi + + case "${WITH_SSL_CLIENT}" in + none) return 0;; + OpenSSL) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to upsmon.conf" + { cat << EOF +# OpenSSL CERTPATH: Directory with PEM file(s), looked up by the +# CA subject name hash value (which must include our NUT server). +# Here we just use the path for PEM file that should be populated +# by the generatecfg_upsd_add_SSL() method. +# We only support CERTPATH (to recognize servers), FORCESSL and +# CERTVERIFY in OpenSSL builds. +CERTPATH "${TESTCERT_PATH_ROOTCA}" +EOF + + if [ x"${WITH_SSL_SERVER}" != xnone ] ; then + cat << EOF +# With OpenSSL this is the only way to configure these behaviors, +# no CERTHOST setting here so far: +FORCESSL 1 +CERTVERIFY 1 +EOF + fi + } >> "$NUT_CONFPATH/upsmon.conf" \ + || die "Failed to populate temporary FS structure for the NIT: upsmon.conf" + ;; + NSS) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to upsmon.conf" + { cat << EOF +# NSS CERTPATH: Directory with 3-file database of cert/key store +CERTPATH "${TESTCERT_PATH_CLIENT}" +CERTIDENT "${TESTCERT_CLIENT_NAME}" "${TESTCERT_CLIENT_PASS}" +EOF + + case "${WITH_SSL_SERVER}" in + none) ;; + NSS) + cat << EOF +CERTHOST localhost "${TESTCERT_SERVER_NAME}" 1 1 +EOF + ;; + *) # OpenSSL + cat << EOF +CERTHOST localhost "${TESTCERT_SERVER_NAME}" 0 0 +EOF + ;; + esac + + if [ x"${WITH_SSL_SERVER}" != xnone ] ; then + cat << EOF +# Defaults that NSS CERTHOST may override per-server, but +# note that this impacts also the general upsmon behavior: +FORCESSL 1 +CERTVERIFY 1 +EOF + fi + } >> "$NUT_CONFPATH/upsmon.conf" \ + || die "Failed to populate temporary FS structure for the NIT: upsmon.conf" + ;; + esac + + NUT_QUIET_INIT_SSL=false + export NUT_QUIET_INIT_SSL +} + ### ups.conf: ################################################## generatecfg_ups_trivial() { @@ -1335,6 +1744,7 @@ sandbox_generate_configs() { log_info "Generating configs for sandbox" generatecfg_upsd_nodev + generatecfg_upsd_add_SSL generatecfg_upsdusers_trivial generatecfg_ups_dummy SANDBOX_CONFIG_GENERATED=true @@ -2165,6 +2575,7 @@ sandbox_start_upsmon_master() { sandbox_generate_configs generatecfg_upsmon_master "$@" + generatecfg_upsmon_add_SSL log_info "Starting UPSMON as master for sandbox" diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index f4295ffff6..b25237b3b0 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -59,7 +59,7 @@ $(top_builddir)/common/libcommonversion.la: $(top_builddir)/include/nut_version. # To extract the values used in the factually built library, this header's # recipe internally (clients/Makefile.am) requires libupsclient.la, and in # turn libcommonclient.la and in turn libparseconf.la: -$(top_builddir)/clients/libupsclient-version.h: $(top_builddir)/common/libparseconf.la +$(top_builddir)/common/libcommonclient.la: $(top_builddir)/common/libparseconf.la # do not hard depend on '../clients/libupsclient-version.h', since it blocks # 'dist', and is only required for actual build, in which case @@ -81,7 +81,12 @@ $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-comm $(top_builddir)/common/libcommonversion-private.la: $(top_builddir)/include/nut_version.h $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion-private.la $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/common/libcommonversion-private.la -endif ENABLE_SHARED_PRIVATE_LIBS + +# Additionally depend on SO made from libcommonclient.la: +$(top_builddir)/clients/libupsclient-version.h: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la +else !ENABLE_SHARED_PRIVATE_LIBS +$(top_builddir)/clients/libupsclient-version.h: $(top_builddir)/common/libcommonclient.la $(top_builddir)/include/nut_version.h +endif !ENABLE_SHARED_PRIVATE_LIBS # We optionally append values to this below bin_PROGRAMS = diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index acdaa16c47..f143b67a36 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -51,6 +51,9 @@ static int (*nut_upscli_list_start)(UPSCONN_t *ups, size_t numq, static int (*nut_upscli_list_next)(UPSCONN_t *ups, size_t numq, const char **query, size_t *numa, char ***answer); static int (*nut_upscli_disconnect)(UPSCONN_t *ups); +static int (*nut_upscli_ssl_caps)(void); +static int (*nut_upscli_set_debug_level)(int); +static void (*nut_upscli_report_build_details)(void); /* This variable collects device(s) from a sequential or parallel scan, * is returned to caller, and cleared to allow subsequent independent scans */ @@ -148,6 +151,34 @@ int nutscan_load_upsclient_library(const char *libname_path) goto err; } + *(void **) (&nut_upscli_ssl_caps) = lt_dlsym(dl_handle, + symbol = "upscli_ssl_caps"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_ssl_caps = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_set_debug_level) = lt_dlsym(dl_handle, + symbol = "upscli_set_debug_level"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_set_debug_level = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } else { + (*nut_upscli_set_debug_level)(nut_debug_level); + } + + *(void **) (&nut_upscli_report_build_details) = lt_dlsym(dl_handle, + symbol = "upscli_report_build_details"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_report_build_details = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } else { + (*nut_upscli_report_build_details)(); + } + /* Passed final lt_dlsym() */ symbol = NULL; diff --git a/tools/nutconf/Makefile.am b/tools/nutconf/Makefile.am index 784ee16ae5..dc5a884aff 100644 --- a/tools/nutconf/Makefile.am +++ b/tools/nutconf/Makefile.am @@ -30,7 +30,6 @@ $(top_builddir)/tools/nut-scanner/libnutscan.la: dummy @dotMAKE@ $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonclient.la: $(top_builddir)/common/libparseconf.la $(top_builddir)/common/libcommonversion.la: $(top_builddir)/include/nut_version.h $(top_builddir)/clients/libupsclient-version.h: $(top_builddir)/clients/libupsclient.la $(top_builddir)/common/libparseconf.la -$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libcommonclient.la if ENABLE_SHARED_PRIVATE_LIBS $(top_builddir)/common/libcommonversion-private.la \ @@ -42,9 +41,11 @@ $(top_builddir)/common/libcommonversion-private.la: $(top_builddir)/include/nut_ $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion-private.la $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/common/libcommonversion-private.la +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la $(top_builddir)/include/nut_version.h $(top_builddir)/common/libnutconf.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la $(top_builddir)/tools/nut-scanner/libnutscan.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la $(top_builddir)/clients/libupsclient-version.h else !ENABLE_SHARED_PRIVATE_LIBS +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/include/nut_version.h $(top_builddir)/common/libnutconf.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/tools/nut-scanner/libnutscan.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion.la $(top_builddir)/common/libcommonclient.la $(top_builddir)/clients/libupsclient-version.h $(top_builddir)/common/libcommonstr.la endif !ENABLE_SHARED_PRIVATE_LIBS