Skip to content

Commit 3505acd

Browse files
committedNov 16, 2023
Add client X509 certificate based authentication
1 parent c4db23f commit 3505acd

17 files changed

+234
-8
lines changed
 

‎CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ add_executable(flashmq
132132
dnsresolver.cpp
133133
bridgeinfodb.h bridgeinfodb.cpp
134134
globber.cpp
135+
x509manager.h x509manager.cpp
135136

136137
)
137138

‎FlashMQTests/FlashMQTests.pro

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ SOURCES += tst_maintests.cpp \
6666
../dnsresolver.cpp \
6767
../bridgeinfodb.cpp \
6868
../globber.cpp \
69+
../x509manager.cpp \
6970
conffiletemp.cpp \
7071
dnstests.cpp \
7172
filecloser.cpp \
@@ -133,6 +134,7 @@ HEADERS += \
133134
../dnsresolver.h \
134135
../bridgeinfodb.h \
135136
../globber.h \
137+
../x509manager.h \
136138
conffiletemp.h \
137139
filecloser.h \
138140
flashmqtempdir.h \

‎bridgeconfig.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ void BridgeConfig::isValid()
149149
port = 8883;
150150
}
151151

152-
testSslVerifyLocations(caFile, caDir);
152+
testSslVerifyLocations(caFile, caDir, "Loading bridge ca_file/ca_dir failed.");
153153
}
154154
else
155155
{

‎client.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -963,3 +963,63 @@ std::string &Client::getMutableUsername()
963963
return this->username;
964964
}
965965

966+
void Client::setSslVerify(X509ClientVerification verificationMode)
967+
{
968+
const int mode = verificationMode > X509ClientVerification::None ? SSL_VERIFY_PEER : SSL_VERIFY_NONE;
969+
this->x509ClientVerification = verificationMode;
970+
ioWrapper.setSslVerify(mode, "");
971+
}
972+
973+
std::optional<std::string> Client::getUsernameFromPeerCertificate()
974+
{
975+
if (!ioWrapper.isSsl() || x509ClientVerification == X509ClientVerification::None)
976+
return std::optional<std::string>();
977+
978+
X509Manager client_cert = ioWrapper.getPeerCertificate();
979+
980+
if (!client_cert)
981+
throw ProtocolError("Client did not provide X509 peer certificate", ReasonCodes::BadUserNameOrPassword);
982+
983+
X509_NAME *x509_name = X509_get_subject_name(client_cert.get());
984+
int index = X509_NAME_get_index_by_NID(x509_name, NID_commonName, -1);
985+
986+
if (index < 0)
987+
return std::optional<std::string>();
988+
989+
X509_NAME_ENTRY *name_entry = X509_NAME_get_entry(x509_name, index);
990+
991+
if (!name_entry)
992+
throw std::runtime_error("X509_NAME_get_entry failed. This should be impossible.");
993+
994+
ASN1_STRING *asn1_string = X509_NAME_ENTRY_get_data(name_entry);
995+
996+
if (!asn1_string)
997+
throw std::runtime_error("Cannot obtain asn1 string from x509 certificate.");
998+
999+
const unsigned char *str = ASN1_STRING_get0_data(asn1_string);
1000+
1001+
if (!str)
1002+
throw std::runtime_error("ASN1_STRING_get0_data failed. This should be impossible.");
1003+
1004+
std::string username(reinterpret_cast<const char*>(str));
1005+
1006+
if (!isValidUtf8(username))
1007+
throw ProtocolError("Common name from peer certificate is not valid UTF8.", ReasonCodes::MalformedPacket);
1008+
1009+
return username;
1010+
}
1011+
1012+
X509ClientVerification Client::getX509ClientVerification() const
1013+
{
1014+
return x509ClientVerification;
1015+
}
1016+
1017+
1018+
1019+
1020+
1021+
1022+
1023+
1024+
1025+

‎client.h

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ See LICENSE for license details.
1717
#include <mutex>
1818
#include <iostream>
1919
#include <time.h>
20+
#include <optional>
2021

2122
#include <openssl/ssl.h>
2223
#include <openssl/err.h>
@@ -28,6 +29,7 @@ See LICENSE for license details.
2829
#include "types.h"
2930
#include "iowrapper.h"
3031
#include "bridgeconfig.h"
32+
#include "enums.h"
3133

3234
#include "publishcopyfactory.h"
3335

@@ -85,6 +87,7 @@ class Client
8587
std::string username;
8688
uint16_t keepalive = 10;
8789
bool clean_start = false;
90+
X509ClientVerification x509ClientVerification = X509ClientVerification::None;
8891

8992
std::shared_ptr<WillPublish> stagedWillPublish;
9093
std::shared_ptr<WillPublish> willPublish;
@@ -218,6 +221,10 @@ class Client
218221
void setFakeUpgraded();
219222
#endif
220223

224+
void setSslVerify(X509ClientVerification verificationMode);
225+
std::optional<std::string> getUsernameFromPeerCertificate();
226+
X509ClientVerification getX509ClientVerification() const;
227+
221228
};
222229

223230
#endif // CLIENT_H

‎configfileparser.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ ConfigFileParser::ConfigFileParser(const std::string &path) :
159159
validListenKeys.insert("inet4_bind_address");
160160
validListenKeys.insert("inet6_bind_address");
161161
validListenKeys.insert("haproxy");
162+
validListenKeys.insert("client_verification_ca_file");
163+
validListenKeys.insert("client_verification_ca_dir");
164+
validListenKeys.insert("client_verification_still_do_authn");
162165

163166
validBridgeKeys.insert("local_username");
164167
validBridgeKeys.insert("remote_username");
@@ -358,6 +361,8 @@ void ConfigFileParser::loadFile(bool test)
358361

359362
std::string key = matches[1].str();
360363
const std::string value = matches[2].str();
364+
std::string valueTrimmed = value;
365+
trim(valueTrimmed);
361366

362367
try
363368
{
@@ -407,6 +412,21 @@ void ConfigFileParser::loadFile(bool test)
407412
bool val = stringTruthiness(value);
408413
curListener->haproxy = val;
409414
}
415+
if (testKeyValidity(key, "client_verification_ca_file", validListenKeys))
416+
{
417+
checkFileExistsAndReadable(key, valueTrimmed, 1024*1024);
418+
curListener->clientVerificationCaFile = valueTrimmed;
419+
}
420+
if (testKeyValidity(key, "client_verification_ca_dir", validListenKeys))
421+
{
422+
checkDirExists(key, value);
423+
curListener->clientVerificationCaDir = valueTrimmed;
424+
}
425+
if (testKeyValidity(key, "client_verification_still_do_authn", validListenKeys))
426+
{
427+
bool val = stringTruthiness(value);
428+
curListener->clientVerifictionStillDoAuthn = val;
429+
}
410430

411431
continue;
412432
}

‎enums.h

+6
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,11 @@ See LICENSE for license details.
1111
#ifndef ENUMS_H
1212
#define ENUMS_H
1313

14+
enum class X509ClientVerification
15+
{
16+
None,
17+
X509IsEnough,
18+
X509AndUsernamePassword
19+
};
1420

1521
#endif // ENUMS_H

‎iowrapper.cpp

+13-4
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,14 @@ void IoWrapper::setSslVerify(int mode, const std::string &hostname)
219219

220220
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
221221

222-
if (!SSL_set1_host(ssl, hostname.c_str()))
223-
throw std::runtime_error("Failed setting hostname of SSL context.");
222+
if (!hostname.empty())
223+
{
224+
if (!SSL_set1_host(ssl, hostname.c_str()))
225+
throw std::runtime_error("Failed setting hostname of SSL context.");
224226

225-
if (SSL_set_tlsext_host_name(ssl, hostname.c_str()) != 1)
226-
throw std::runtime_error("Failed setting SNI hostname of SSL context.");
227+
if (SSL_set_tlsext_host_name(ssl, hostname.c_str()) != 1)
228+
throw std::runtime_error("Failed setting SNI hostname of SSL context.");
229+
}
227230

228231
SSL_set_verify(ssl, mode, verify_callback);
229232
}
@@ -268,6 +271,12 @@ WebsocketState IoWrapper::getWebsocketState() const
268271
return websocketState;
269272
}
270273

274+
X509Manager IoWrapper::getPeerCertificate() const
275+
{
276+
X509Manager result(this->ssl);
277+
return result;
278+
}
279+
271280
bool IoWrapper::needsHaProxyParsing() const
272281
{
273282
return _needsHaProxyParsing;

‎iowrapper.h

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ See LICENSE for license details.
2121
#include "logger.h"
2222
#include "haproxy.h"
2323
#include "cirbuf.h"
24+
#include "x509manager.h"
2425

2526
#define WEBSOCKET_MIN_HEADER_BYTES_NEEDED 2
2627
#define WEBSOCKET_MAX_SENDING_HEADER_SIZE 10
@@ -136,6 +137,7 @@ class IoWrapper
136137
bool hasProcessedBufferedBytesToRead() const;
137138
bool isWebsocket() const;
138139
WebsocketState getWebsocketState() const;
140+
X509Manager getPeerCertificate() const;
139141

140142
bool needsHaProxyParsing() const;
141143
HaProxyConnectionType readHaProxyData(int fd, struct sockaddr *addr);

‎listener.cpp

+38
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ it under the terms of The Open Software License 3.0 (OSL-3.0).
88
See LICENSE for license details.
99
*/
1010

11+
#include <openssl/err.h>
12+
1113
#include "listener.h"
1214

1315
#include "utils.h"
1416
#include "exceptions.h"
17+
#include "logger.h"
1518

1619
void Listener::isValid()
1720
{
@@ -26,6 +29,7 @@ void Listener::isValid()
2629
}
2730

2831
testSsl(sslFullchain, sslPrivkey);
32+
testSslVerifyLocations(clientVerificationCaFile, clientVerificationCaDir, "Loading client_verification_ca_dir/client_verification_ca_file failed.");
2933
}
3034
else
3135
{
@@ -38,6 +42,11 @@ void Listener::isValid()
3842
}
3943
}
4044

45+
if ((!clientVerificationCaDir.empty() || !clientVerificationCaFile.empty()) && !isSsl())
46+
{
47+
throw ConfigFileException("X509 client verification can only be done on TLS listeners.");
48+
}
49+
4150
if (port <= 0 || port > 65534)
4251
{
4352
throw ConfigFileException(formatString("Port nr %d is not valid", port));
@@ -98,6 +107,35 @@ void Listener::loadCertAndKeyFromConfig()
98107
throw std::runtime_error("Loading cert failed. This was after test loading the certificate, so is very unexpected.");
99108
if (SSL_CTX_use_PrivateKey_file(sslctx->get(), sslPrivkey.c_str(), SSL_FILETYPE_PEM) != 1)
100109
throw std::runtime_error("Loading key failed. This was after test loading the certificate, so is very unexpected.");
110+
111+
{
112+
const char *ca_file = clientVerificationCaFile.empty() ? nullptr : clientVerificationCaFile.c_str();
113+
const char *ca_dir = clientVerificationCaDir.empty() ? nullptr : clientVerificationCaDir.c_str();
114+
115+
if (ca_file || ca_dir)
116+
{
117+
if (SSL_CTX_load_verify_locations(sslctx->get(), ca_file, ca_dir) != 1)
118+
{
119+
ERR_print_errors_cb(logSslError, NULL);
120+
throw std::runtime_error("Loading client_verification_ca_dir/client_verification_ca_file failed. "
121+
"This was after test loading the certificate, so is very unexpected.");
122+
}
123+
}
124+
}
125+
}
126+
127+
X509ClientVerification Listener::getX509ClientVerficiationMode() const
128+
{
129+
X509ClientVerification result = X509ClientVerification::None;
130+
const bool clientCADefined = !clientVerificationCaDir.empty() || !clientVerificationCaFile.empty();
131+
132+
if (clientCADefined)
133+
result = X509ClientVerification::X509IsEnough;
134+
135+
if (result >= X509ClientVerification::X509IsEnough && clientVerifictionStillDoAuthn)
136+
result = X509ClientVerification::X509AndUsernamePassword;
137+
138+
return result;
101139
}
102140

103141
std::string Listener::getBindAddress(ListenerProtocol p)

‎listener.h

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ See LICENSE for license details.
1515
#include <memory>
1616

1717
#include "sslctxmanager.h"
18+
#include "enums.h"
1819

1920
enum class ListenerProtocol
2021
{
@@ -33,13 +34,17 @@ struct Listener
3334
bool haproxy = false;
3435
std::string sslFullchain;
3536
std::string sslPrivkey;
37+
std::string clientVerificationCaFile;
38+
std::string clientVerificationCaDir;
39+
bool clientVerifictionStillDoAuthn = false;
3640
std::unique_ptr<SslCtxManager> sslctx;
3741

3842
void isValid();
3943
bool isSsl() const;
4044
bool isHaProxy() const;
4145
std::string getProtocolName() const;
4246
void loadCertAndKeyFromConfig();
47+
X509ClientVerification getX509ClientVerficiationMode() const;
4348

4449
std::string getBindAddress(ListenerProtocol p);
4550
};

‎mainapp.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,11 @@ void MainApp::start()
733733
// Don't use std::make_shared to avoid the weak pointers keeping the control block in memory.
734734
std::shared_ptr<Client> client = std::shared_ptr<Client>(new Client(fd, thread_data, clientSSL, listener->websocket, listener->isHaProxy(), addr, settings));
735735

736+
if (listener->getX509ClientVerficiationMode() != X509ClientVerification::None)
737+
{
738+
client->setSslVerify(listener->getX509ClientVerficiationMode());
739+
}
740+
736741
thread_data->giveClient(std::move(client));
737742

738743
globalStats->socketConnects.inc();

‎mqttpacket.cpp

+16
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,17 @@ void MqttPacket::handleConnect()
804804

805805
ConnectData connectData = parseConnectData();
806806

807+
if (sender->getX509ClientVerification() > X509ClientVerification::None)
808+
{
809+
std::optional<std::string> certificateUsername = sender->getUsernameFromPeerCertificate();
810+
811+
if (!certificateUsername || certificateUsername.value().empty())
812+
throw ProtocolError("Client certificate did not provider username", ReasonCodes::BadUserNameOrPassword);
813+
814+
connectData.user_name_flag = true;
815+
connectData.username = certificateUsername.value();
816+
}
817+
807818
sender->setBridge(connectData.bridge);
808819

809820
if (this->protocolVersion == ProtocolVersion::None)
@@ -928,6 +939,11 @@ void MqttPacket::handleConnect()
928939
{
929940
authResult = AuthResult::success;
930941
}
942+
else if (sender->getX509ClientVerification() == X509ClientVerification::X509IsEnough)
943+
{
944+
// The client will have been kicked out already if the certificate is not valid, so we can just approve it.
945+
authResult = AuthResult::success;
946+
}
931947
else if (connectData.authenticationMethod.empty())
932948
{
933949
authResult = authentication.unPwdCheck(connectData.client_id, connectData.username, connectData.password, getUserProperties(), sender);

‎utils.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ void testSsl(const std::string &fullchain, const std::string &privkey)
560560
}
561561
}
562562

563-
void testSslVerifyLocations(const std::string &caFile, const std::string &caDir)
563+
void testSslVerifyLocations(const std::string &caFile, const std::string &caDir, const std::string &error)
564564
{
565565
if (!caFile.empty() && getFileSize(caFile) <= 0)
566566
throw ConfigFileException(formatString("SSL 'ca_file' file '%s' is empty or invalid", caFile.c_str()));
@@ -576,7 +576,7 @@ void testSslVerifyLocations(const std::string &caFile, const std::string &caDir)
576576
if (SSL_CTX_load_verify_locations(sslCtx.get(), ca_file, ca_dir) != 1)
577577
{
578578
ERR_print_errors_cb(logSslError, NULL);
579-
throw std::runtime_error("Loading ca_file/ca_dir failed.");
579+
throw ConfigFileException(error);
580580
}
581581
}
582582

‎utils.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ std::string generateBadHttpRequestReponse(const std::string &msg);
8181
std::string generateWebsocketAnswer(const std::string &acceptString, const std::string &subprotocol);
8282

8383
void testSsl(const std::string &fullchain, const std::string &privkey);
84-
void testSslVerifyLocations(const std::string &caFile, const std::string &caDir);
84+
void testSslVerifyLocations(const std::string &caFile, const std::string &caDir, const std::string &error);
8585

8686
std::string formatString(const std::string str, ...);
8787

‎x509manager.cpp

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include "x509manager.h"
2+
#include <cassert>
3+
4+
X509Manager::X509Manager(const SSL *ssl)
5+
{
6+
#if OPENSSL_VERSION_NUMBER < 0x30000000L
7+
this->d = SSL_get_peer_certificate(ssl);
8+
#else
9+
this->d = SSL_get1_peer_certificate(ssl);
10+
#endif
11+
}
12+
13+
X509Manager::X509Manager(X509Manager &&other)
14+
{
15+
assert(this != &other);
16+
17+
X509_free(this->d);
18+
this->d = other.d;
19+
other.d = nullptr;
20+
}
21+
22+
X509Manager::~X509Manager()
23+
{
24+
X509_free(this->d);
25+
this->d = nullptr;
26+
}
27+
28+
X509 *X509Manager::get()
29+
{
30+
return this->d;
31+
}
32+
33+
X509Manager::operator bool() const
34+
{
35+
return d != nullptr;
36+
}

‎x509manager.h

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#ifndef X509MANAGER_H
2+
#define X509MANAGER_H
3+
4+
#include <openssl/ssl.h>
5+
6+
class X509Manager
7+
{
8+
X509 *d = nullptr;
9+
public:
10+
X509Manager(const X509Manager &other) = delete;
11+
X509Manager(X509Manager &&other);
12+
X509Manager(const SSL *ssl);
13+
~X509Manager();
14+
X509 *get();
15+
operator bool() const;
16+
17+
};
18+
19+
#endif // X509MANAGER_H

0 commit comments

Comments
 (0)
Please sign in to comment.