Skip to content

Commit ccdcc90

Browse files
authored
Merge a32e6dc into 68fb8bd
2 parents 68fb8bd + a32e6dc commit ccdcc90

File tree

21 files changed

+1136
-231
lines changed

21 files changed

+1136
-231
lines changed

Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,8 @@ SET_PARMAKES_OPT = \
874874
fi ; \
875875
PARMAKES_OPT="-j $${MAXPARMAKES}" ; \
876876
;; \
877-
esac
877+
esac ; \
878+
if [ x"$${V}$(V)" = x ]; then PARMAKES_OPT="$${PARMAKES_OPT} V=0" ; fi
878879

879880
all-quick: @dotMAKE@
880881
+@$(SET_PARMAKES_OPT); \

NEWS.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@ https://github.com/networkupstools/nut/milestone/13
6060
query (for protocol version) to verify that handshake succeeded.
6161
This change impacted also the classic C `libupsclient` library.
6262
[issue #3387, PR #3402]
63+
* Updated OpenSSL code paths in the `libupsclient` C library to support
64+
features earlier only available with NSS builds, like specifying the
65+
client certificate+key, optionally with password, and pinning expected
66+
server certificates. For both backends such pinning should now honour
67+
the 'certverify' setting of the `CERTHOST` entry (e.g. not abort the
68+
connection attempt if that number is '0'). [issue #3331]
69+
* Updated SSL support in the `upsd` data server to handle `CERTREQUEST`
70+
(optional validation of clients identified by a certificate) also
71+
when built with OpenSSL, optionally using the `CERTPATH` with a
72+
collection of CA certificates (directory or a big PEM file).
73+
Also support `CERTIDENT` to provide a private key password and ensure
74+
that the certificate in `CERTFILE` has an expected subject name as an
75+
exact string, or that its CN or SAN match the provided string as a
76+
standard expression of host name (section 3.5 of RFC 1034) or IP address.
77+
[issue #3331]
78+
* The `libupsclient` API was extended with a `upscli_init2()` method which
79+
allows to pass the `certfile` argument needed for OpenSSL builds. [#3331]
80+
81+
- `upsmon` client updates:
82+
* Introduced support for `CERTFILE` option, so the client can identify
83+
itself to the data server also in OpenSSL builds. [issue #3331]
6384

6485

6586
Release notes for NUT 2.8.5 - what's new since 2.8.4

clients/nutclient.cpp

Lines changed: 140 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class Socket
244244
bool isConnected()const;
245245
void setDebugConnect(bool d);
246246

247-
void setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass);
247+
void setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass, const std::string& certident_name);
248248
void setSSLConfig_NSS(bool force_ssl, int certverify, const std::string& certstore_path, const std::string& certstore_pass, const std::string& certstore_prefix, const std::string& certhost_name, const std::string& certident_name);
249249

250250
void startTLS();
@@ -318,19 +318,45 @@ class Socket
318318
# ifdef WITH_OPENSSL
319319
SSL_CTX* Socket::_ssl_ctx = nullptr;
320320

321-
/*static*/ int Socket::openssl_password_callback(char *buf, int size, int rwflag, void *userdata) /* pem_passwd_cb, 1.1.0+ */
321+
# if (defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB) || (defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_SET_DEFAULT_PASSWD_CB)
322+
/* Note: availability of these methods seems to predate C++11, but still... */
323+
/*static*/ int Socket::openssl_password_callback(char *buf, int size, int rwflag, void *userdata) /* pem_passwd_cb, cca OpenSSL 1.1.0+ */
322324
{
325+
/* See https://docs.openssl.org/1.0.2/man3/SSL_CTX_set_default_passwd_cb */
326+
/* is callback used for reading/decryption (rwflag=0) or writing/encryption (rwflag=1)? */
323327
NUT_UNUSED_VARIABLE(rwflag);
328+
/* "userdata" is generally the user-provided password, possibly cached
329+
* from an earlier loop (e.g. to check interactively typing it twice,
330+
* or to probe several items in a loop). For us, it should be this->_key_pass
331+
* via SSL_CTX_set_default_passwd_cb_userdata(), but most programs out
332+
* there do not have just one variable with one password to think about. */
333+
334+
if (!buf || size < 1) {
335+
/* Can not even set buf[0] */
336+
return 0;
337+
}
324338

325-
if (size < 1 || !userdata || !*(static_cast<char *>(userdata))) {
339+
if (!userdata || !*(static_cast<char *>(userdata))) {
340+
# if (defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA) || (defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA)
341+
/* Use what we were told to use (or not), do not surprise
342+
* anyone by some hard-coded fallback to _key_pass here! */
326343
buf[0] = '\0';
327344
return 0;
345+
# else
346+
userdata = static_cast<void *>(this->_key_pass.c_str());
347+
# endif
348+
}
349+
350+
if (strlen(static_cast<char *>(userdata)) >= (size_t)size) {
351+
/* Do not return truncated trash, just say we could not do it */
352+
return 0;
328353
}
329354

330355
strncpy(buf, static_cast<char *>(userdata), static_cast<size_t>(size));
331356
buf[size - 1] = '\0';
332357
return static_cast<int>(strlen(buf));
333358
}
359+
# endif /* ...SET_DEFAULT_PASSWD_CB */
334360
# endif /* WITH_OPENSSL */
335361

336362
# ifdef WITH_NSS
@@ -861,7 +887,7 @@ bool Socket::isSSL()const
861887
#endif
862888
}
863889

864-
void Socket::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass)
890+
void Socket::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass, const std::string& certident_name)
865891
{
866892
_force_ssl = force_ssl;
867893

@@ -875,6 +901,7 @@ void Socket::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::str
875901
_cert_file = cert_file;
876902
_key_file = key_file;
877903
_key_pass = key_pass;
904+
_certident_name = certident_name;
878905

879906
_ssl_configured |= UPSCLI_SSL_CAPS_OPENSSL;
880907
#else
@@ -884,6 +911,7 @@ void Socket::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::str
884911
NUT_UNUSED_VARIABLE(cert_file);
885912
NUT_UNUSED_VARIABLE(key_file);
886913
NUT_UNUSED_VARIABLE(key_pass);
914+
NUT_UNUSED_VARIABLE(certident_name);
887915

888916
_ssl_configured &= ~UPSCLI_SSL_CAPS_OPENSSL;
889917
#endif
@@ -978,32 +1006,123 @@ void Socket::startTLS()
9781006

9791007
if (!_ca_file.empty() || !_ca_path.empty()) {
9801008
if (SSL_CTX_load_verify_locations(_ssl_ctx, _ca_file.empty() ? nullptr : _ca_file.c_str(), _ca_path.empty() ? nullptr : _ca_path.c_str()) != 1) {
981-
throw nut::SSLException_OpenSSL("Failed to load CA verify locations");
1009+
if (!(_ca_file.empty() && !_ca_path.empty()
1010+
/* Retry in case CERTPATH pointed to PEM file */
1011+
&& SSL_CTX_load_verify_locations(_ssl_ctx, _ca_path.c_str(), nullptr) == 1)
1012+
) {
1013+
throw nut::SSLException_OpenSSL("Failed to load CA verify locations");
1014+
}
9821015
}
9831016
}
1017+
9841018
if (_certverify != -1) {
9851019
SSL_CTX_set_verify(_ssl_ctx, _certverify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, nullptr);
9861020
}
1021+
9871022
if (!_cert_file.empty()) {
9881023
if (SSL_CTX_use_certificate_chain_file(_ssl_ctx, _cert_file.c_str()) != 1) {
9891024
throw nut::SSLException_OpenSSL("Failed to load client certificate file");
9901025
}
1026+
9911027
if (!_key_pass.empty()) {
992-
# if OPENSSL_VERSION_NUMBER < 0x10100000L
993-
throw nut::SSLException_OpenSSL("Private key password support not implemented for OpenSSL < 1.1 yet");
994-
# else
995-
/* OpenSSL 1.1.0+
1028+
/* Note: availability of these methods seems to predate C++11, but still... */
1029+
# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB
1030+
/* Roughly OpenSSL 1.1.0+ or 1.0.2+ with patched distros
9961031
* https://docs.openssl.org/3.5/man3/SSL_CTX_set_default_passwd_cb/#return-values
9971032
*/
9981033
/* 1. Set the callback function */
9991034
SSL_CTX_set_default_passwd_cb(_ssl_ctx, openssl_password_callback);
1035+
# if defined(HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_CTX_SET_DEFAULT_PASSWD_CB_USERDATA
10001036
/* 2. Set the userdata to the password string */
10011037
SSL_CTX_set_default_passwd_cb_userdata(_ssl_ctx, const_cast<void *>(static_cast<const void *>(_key_pass.c_str())));
1002-
# endif
1038+
# endif /* else callback uses class instance field */
1039+
# else /* Not SSL_CTX_* methods */
1040+
/* Per https://docs.openssl.org/3.5/man3/SSL_CTX_set_default_passwd_cb,
1041+
* the `SSL_CTX*` variants were added in 1.1.
1042+
* The SSL_set_default_passwd_cb() and SSL_set_default_passwd_cb_userdata()
1043+
* for `SSL*` argument were around since the turn of millennium, approx 0.9.6+
1044+
* per https://github.com/openssl/openssl/commit/66ebbb6a56bc1688fa37878e4feec985b0c260d7
1045+
*
1046+
* But to use those, we would need to get that SSL* (maybe from socket FD?);
1047+
* that would also unlock us using the ssl_error() elsewhere.
1048+
*
1049+
* Alternately load PEM "manually", see e.g. Apache httpd sources before 2015.
1050+
*/
1051+
# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB) && HAVE_SSL_SET_DEFAULT_PASSWD_CB
1052+
/* Theoretical solution - didn't find a build system where such methods
1053+
* would actually be available, so this could be tested and used */
1054+
SSL *ssl_tmp = SSL_new(ssl_ctx);
1055+
/* OpenSSL 0.9.6+ at least? */
1056+
SSL_set_default_passwd_cb(ssl_tmp, openssl_password_callback);
1057+
# if defined(HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA) && HAVE_SSL_SET_DEFAULT_PASSWD_CB_USERDATA
1058+
SSL_set_default_passwd_cb_userdata(ssl_tmp, const_cast<void *>(static_cast<const void *>(_key_pass.c_str())));
1059+
# endif
1060+
SSL_free(ssl_tmp);
1061+
1062+
# else /* Not SSL_* methods either */
1063+
1064+
throw nut::SSLException_OpenSSL("Private key password support not implemented for OpenSSL < 1.1 yet");
1065+
# endif
1066+
# endif /* ...SET_DEFAULT_PASSWD_CB */
10031067
}
1068+
10041069
if (SSL_CTX_use_PrivateKey_file(_ssl_ctx, _key_file.empty() ? _cert_file.c_str() : _key_file.c_str(), SSL_FILETYPE_PEM) != 1) {
10051070
throw nut::SSLException_OpenSSL("Failed to load client private key file");
10061071
}
1072+
1073+
if (!_certident_name.empty()) {
1074+
# if (defined(HAVE_SSL_CTX_GET0_CERTIFICATE) && HAVE_SSL_CTX_GET0_CERTIFICATE) && (defined(HAVE_X509_CHECK_HOST) && HAVE_X509_CHECK_HOST) && (defined(HAVE_X509_CHECK_IP_ASC) && HAVE_X509_CHECK_IP_ASC) && (defined(HAVE_X509_NAME_ONELINE) && HAVE_X509_NAME_ONELINE)
1075+
/* Roughly OpenSSL 1.0.2+ */
1076+
X509 *x509 = SSL_CTX_get0_certificate(_ssl_ctx);
1077+
if (x509) {
1078+
/* Check if _certident_name matches the host (CN or SAN) */
1079+
if (X509_check_host(x509, _certident_name.c_str(), 0, 0, nullptr) != 1
1080+
&& X509_check_ip_asc(x509, _certident_name.c_str(), 0) != 1
1081+
) {
1082+
char *subject = X509_NAME_oneline(X509_get_subject_name(x509), nullptr, 0);
1083+
char *subject_CN = (subject ? static_cast<char*>(strstr(subject, "CN=")) + 3 : nullptr);
1084+
size_t certident_len = _certident_name.length();
1085+
1086+
if (_debugConnect) std::cerr <<
1087+
"[D4] Socket::startTLS(): My certificate subject: '" << (subject ? subject : "unknown") <<
1088+
"'; CN: '" << (subject_CN ? subject_CN : "unknown") <<
1089+
"'; CERTIDENT: [" << certident_len << "]'" << _certident_name << "'" <<
1090+
std::endl << std::flush;
1091+
1092+
/* Check if _certident_name matches the whole subject or just .../CN=.../ part as a string */
1093+
if (!subject || !(
1094+
strcmp(subject, _certident_name.c_str()) == 0
1095+
|| (subject_CN && !strncmp(subject_CN, _certident_name.c_str(), certident_len)
1096+
&& (subject_CN[certident_len] == '\0' || subject_CN[certident_len] == '/') )
1097+
)) {
1098+
/* This way or that, the names differ */
1099+
std::string err = "Certificate subject (" + std::string(subject ? subject : "unknown") + ") does not match CERTIDENT name (" + _certident_name + ")";
1100+
if (subject) {
1101+
OPENSSL_free(subject);
1102+
}
1103+
throw nut::SSLException_OpenSSL(err);
1104+
} else {
1105+
if (_debugConnect) std::cerr <<
1106+
"[D2] Socket::startTLS(): Certificate subject verified against CERTIDENT subject name (" << _certident_name << ")" <<
1107+
std::endl << std::flush;
1108+
}
1109+
if (subject) {
1110+
OPENSSL_free(subject);
1111+
}
1112+
} else {
1113+
if (_debugConnect) std::cerr <<
1114+
"[D2] Socket::startTLS(): Certificate subject verified against CERTIDENT host name (" << _certident_name << ")" <<
1115+
std::endl << std::flush;
1116+
}
1117+
}
1118+
# else /* Missing X509 methods wanted above */
1119+
throw nut::SSLException_OpenSSL("Can not verify CERTIDENT '" + _certident_name + "': not supported in this OpenSSL build (too old)");
1120+
# endif /* Got ways to check CERTIDENT? */
1121+
}
1122+
} else {
1123+
if (!_certident_name.empty()) {
1124+
throw nut::SSLException_OpenSSL("Can not verify CERTIDENT '" + _certident_name + "': no cert_file was provided");
1125+
}
10071126
}
10081127

10091128
_ssl = SSL_new(_ssl_ctx);
@@ -1419,7 +1538,7 @@ void SSLConfig::apply(TcpClient& client) const
14191538

14201539
void SSLConfig_OpenSSL::apply(TcpClient& client) const
14211540
{
1422-
client.setSSLConfig_OpenSSL(_force_ssl, _certverify, _ca_path, _ca_file, _cert_file, _key_file, _key_pass);
1541+
client.setSSLConfig_OpenSSL(_force_ssl, _certverify, _ca_path, _ca_file, _cert_file, _key_file, _key_pass, _certident_name);
14231542
}
14241543

14251544
void SSLConfig_NSS::apply(TcpClient& client) const
@@ -1432,7 +1551,7 @@ void TcpClient::setSSLConfig(const SSLConfig& config)
14321551
config.apply(*this);
14331552
}
14341553

1435-
void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass)
1554+
void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass, const char *certident_name)
14361555
{
14371556
_force_ssl = force_ssl;
14381557
_certverify = certverify;
@@ -1441,9 +1560,10 @@ void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const char
14411560
if (cert_file) _cert_file = cert_file;
14421561
if (key_file) _key_file = key_file;
14431562
if (key_pass) _key_pass = key_pass;
1563+
if (certident_name) _certident_name = certident_name;
14441564
}
14451565

1446-
void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass)
1566+
void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::string& ca_path, const std::string& ca_file, const std::string& cert_file, const std::string& key_file, const std::string& key_pass, const std::string& certident_name)
14471567
{
14481568
_force_ssl = force_ssl;
14491569
_certverify = certverify;
@@ -1452,6 +1572,7 @@ void TcpClient::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::
14521572
_cert_file = cert_file;
14531573
_key_file = key_file;
14541574
_key_pass = key_pass;
1575+
_certident_name = certident_name;
14551576
}
14561577

14571578
void TcpClient::setSSLConfig_NSS(bool force_ssl, int certverify, const char *certstore_path, const char *certstore_pass, const char *certstore_prefix, const char *certhost_name, const char *certident_name)
@@ -1493,8 +1614,8 @@ void TcpClient::connect()
14931614
{
14941615
_socket->connect(_host, _port);
14951616
if (_try_ssl || _force_ssl) {
1496-
if (!_ca_path.empty() || !_ca_file.empty() || !_cert_file.empty() || !_key_file.empty()) {
1497-
_socket->setSSLConfig_OpenSSL(_force_ssl, _certverify, _ca_path, _ca_file, _cert_file, _key_file, _key_pass);
1617+
if (!_ca_path.empty() || !_ca_file.empty() || !_cert_file.empty() || !_key_file.empty() || !_certident_name.empty()) {
1618+
_socket->setSSLConfig_OpenSSL(_force_ssl, _certverify, _ca_path, _ca_file, _cert_file, _key_file, _key_pass, _certident_name);
14981619
} else if (!_certstore_path.empty() || !_certstore_prefix.empty() || !_certhost_name.empty() || !_certident_name.empty()) {
14991620
_socket->setSSLConfig_NSS(_force_ssl, _certverify, _certstore_path, _key_pass, _certstore_prefix, _certhost_name, _certident_name);
15001621
}
@@ -2931,13 +3052,13 @@ NUTCLIENT_TCP_t nutclient_tcp_create_client(const char* host, uint16_t port)
29313052

29323053
int nutclient_tcp_get_ssl_caps(void) { return nut::TcpClient::getSslCaps(); }
29333054

2934-
NUTCLIENT_TCP_t nutclient_tcp_create_client_ssl_OpenSSL(const char* host, uint16_t port, int try_ssl, int force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass)
3055+
NUTCLIENT_TCP_t nutclient_tcp_create_client_ssl_OpenSSL(const char* host, uint16_t port, int try_ssl, int force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass, const char *certident_name)
29353056
{
29363057
nut::TcpClient* client = new nut::TcpClient;
29373058
try
29383059
{
29393060
client->setSSLConfig(nut::SSLConfig_OpenSSL(
2940-
(force_ssl > 0), certverify, ca_path, ca_file, cert_file, key_file, key_pass
3061+
(force_ssl > 0), certverify, ca_path, ca_file, cert_file, key_file, key_pass, certident_name
29413062
));
29423063
client->connect(host, port, try_ssl != 0);
29433064
return static_cast<NUTCLIENT_TCP_t>(client);
@@ -2950,15 +3071,15 @@ NUTCLIENT_TCP_t nutclient_tcp_create_client_ssl_OpenSSL(const char* host, uint16
29503071
}
29513072
}
29523073

2953-
void nutclient_tcp_set_ssl_config_OpenSSL(NUTCLIENT_TCP_t client, int force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass)
3074+
void nutclient_tcp_set_ssl_config_OpenSSL(NUTCLIENT_TCP_t client, int force_ssl, int certverify, const char *ca_path, const char *ca_file, const char *cert_file, const char *key_file, const char *key_pass, const char *certident_name)
29543075
{
29553076
if(client)
29563077
{
29573078
nut::TcpClient* cl = dynamic_cast<nut::TcpClient*>(static_cast<nut::Client*>(client));
29583079
if(cl)
29593080
{
29603081
cl->setSSLConfig(nut::SSLConfig_OpenSSL((
2961-
force_ssl > 0), certverify, ca_path, ca_file, cert_file, key_file, key_pass
3082+
force_ssl > 0), certverify, ca_path, ca_file, cert_file, key_file, key_pass, certident_name
29623083
));
29633084
}
29643085
}

0 commit comments

Comments
 (0)