Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions examples/helloworld/producer.m.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <rmqt_exchange.h>
#include <rmqt_message.h>
#include <rmqt_result.h>
#include <rmqt_securityparameters.h>
#include <rmqt_vhostinfo.h>

#include <bsl_memory.h>
Expand All @@ -21,13 +22,21 @@ using namespace BloombergLP;
int main(int argc, char** argv)
{
if (argc < 2) {
std::cerr << "USAGE: " << argv[0] << " <amqp uri>\n";
std::cerr << "USAGE: " << argv[0] << " <amqp[s] uri> [ca-cert-path]\n";
return 1;
}
rmqa::RabbitContext rabbit;

// An amqps:// uri needs a certificate authority to verify the broker's
// certificate against
bsl::shared_ptr<rmqt::SecurityParameters> securityParameters;
if (argc > 2) {
securityParameters =
bsl::make_shared<rmqt::SecurityParameters>(argv[2]);
}

bsl::optional<rmqt::VHostInfo> vhostInfo =
rmqa::ConnectionString::parse(argv[1]);
rmqa::ConnectionString::parse(argv[1], securityParameters);

if (!vhostInfo) {
std::cerr << "Failed to parse connection string: " << argv[1] << "\n";
Expand Down
65 changes: 65 additions & 0 deletions src/rmq/rmqio/rmqio_asioresolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ void handleTLSHandshake(boost::system::error_code error,
}
}

class CertificateVerifier {
public:
explicit CertificateVerifier(const bsl::string& host)
: d_host(host)
{
}

bool operator()(bool preverified,
boost::asio::ssl::verify_context& ctx) const
{
return AsioResolver::verifyCertificate(d_host, preverified, ctx);
}

private:
bsl::string d_host;
};

void startTLSHandshake(
const bsl::string& host,
boost::system::error_code error,
Expand All @@ -148,6 +165,33 @@ void startTLSHandshake(
<< endpoint->endpoint().port()
<< ", starting TLS Handshake";

boost::system::error_code verifyError;
socketWrapper->socket().set_verify_callback(CertificateVerifier(host),
verifyError);
if (verifyError) {
BALL_LOG_ERROR << "Error setting host name verification for ["
<< host << "]: " << augmentTlsError(verifyError);
onFail(Resolver::ERROR_HANDSHAKE);
return; // RETURN
}

// The host resolved to reach this point, so anything which doesn't
// parse as an IP address is a DNS name
boost::system::error_code parseError;
boost::asio::ip::make_address(host.c_str(), parseError);
const bool hostIsDnsName = static_cast<bool>(parseError);

// Only DNS names are sent as SNI, so the broker can select the
// matching certificate. RFC 6066 forbids IP literals here
if (hostIsDnsName &&
!SSL_set_tlsext_host_name(socketWrapper->socket().native_handle(),
host.c_str())) {
BALL_LOG_ERROR << "Error setting TLS SNI host name for [" << host
<< "]";
onFail(Resolver::ERROR_HANDSHAKE);
return; // RETURN
}

socketWrapper->socket().async_handshake(
boost::asio::ssl::stream_base::client,
bdlf::BindUtil::bind(&handleTLSHandshake,
Expand Down Expand Up @@ -505,6 +549,27 @@ bool AsioResolver::logCertVerificationFailure(
return preverified;
}

bool AsioResolver::verifyCertificate(const bsl::string& host,
bool preverified,
boost::asio::ssl::verify_context& ctx)
{
if (!logCertVerificationFailure(preverified, ctx)) {
return false;
}

const boost::asio::ssl::host_name_verification hostNameVerifier(
host.c_str());

if (!hostNameVerifier(preverified, ctx)) {
BALL_LOG_ERROR << "Certificate verification failed: certificate "
"identity does not match host name: "
<< host;
return false;
}

return true;
}

void AsioResolver::shuffleResolverResults(
AsioResolver::results_type& resolverResults,
bool shuffleConnectionEndpoints,
Expand Down
4 changes: 4 additions & 0 deletions src/rmq/rmqio/rmqio_asioresolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ class AsioResolver : public Resolver,
logCertVerificationFailure(bool preverified,
boost::asio::ssl::verify_context& ctx);

static bool verifyCertificate(const bsl::string& host,
bool preverified,
boost::asio::ssl::verify_context& ctx);

private:
explicit AsioResolver(AsioEventLoop& eventloop,
bool shuffleConnectionEndpoints);
Expand Down
101 changes: 101 additions & 0 deletions src/tests/rmqio/rmqio_asioresolver.t.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <gtest/gtest.h>

#include <openssl/x509.h>
#include <openssl/x509v3.h>

#include <bsl_cstdio.h>
#include <bsl_memory.h>
Expand Down Expand Up @@ -96,6 +97,39 @@ class CertificateGuard {
X509* d_cert;
};

bool addSubjectAltName(X509* cert, const char* subjectAltName)
{
X509_EXTENSION* ext =
X509V3_EXT_conf_nid(0, 0, NID_subject_alt_name, subjectAltName);
if (!ext) {
return false;
}

const int rc = X509_add_ext(cert, ext, -1);
X509_EXTENSION_free(ext);

return rc == 1;
}

/// Verifies `host` against a certificate presenting `subjectAltName`, as if
/// the chain itself had already verified successfully.
bool verifyHost(const char* subjectAltName, const bsl::string& host)
{
StoreContextGuard storeCtx;
EXPECT_THAT(storeCtx.get(), NotNull());
EXPECT_THAT(X509_STORE_CTX_init(storeCtx.get(), 0, 0, 0), Eq(1));

CertificateGuard cert;
EXPECT_THAT(cert.get(), NotNull());
EXPECT_TRUE(addSubjectAltName(cert.get(), subjectAltName));

X509_STORE_CTX_set_current_cert(storeCtx.get(), cert.get());

boost::asio::ssl::verify_context ctx(storeCtx.get());

return AsioResolver::verifyCertificate(host, true, ctx);
}

} // namespace

TEST_F(ResolverTests, Breathing)
Expand Down Expand Up @@ -151,6 +185,73 @@ TEST_F(ResolverTests, LogCertVerificationPreverifiedPassesThrough)
EXPECT_TRUE(AsioResolver::logCertVerificationFailure(true, ctx));
}

TEST_F(ResolverTests, VerifyCertificateExactHostName)
{
EXPECT_TRUE(verifyHost("DNS:one.example.com", "one.example.com"));
EXPECT_FALSE(verifyHost("DNS:one.example.com", "two.example.com"));
}

TEST_F(ResolverTests, VerifyCertificateWildcardMatchesAnySingleLabel)
{
EXPECT_TRUE(verifyHost("DNS:*.example.com", "one.example.com"));
EXPECT_TRUE(verifyHost("DNS:*.example.com", "two.example.com"));
}

TEST_F(ResolverTests, VerifyCertificateWildcardIsScopedToItsOwnDomain)
{
EXPECT_FALSE(verifyHost("DNS:*.example.com", "one.not-example.com"));

EXPECT_TRUE(verifyHost("DNS:*.not-example.com", "one.not-example.com"));
EXPECT_FALSE(verifyHost("DNS:*.not-example.com", "one.example.com"));
}

TEST_F(ResolverTests, VerifyCertificateWildcardMatchesOneLabelOnly)
{
EXPECT_FALSE(verifyHost("DNS:*.example.com", "one.two.example.com"));
EXPECT_FALSE(verifyHost("DNS:*.example.com", "example.com"));
}

TEST_F(ResolverTests, VerifyCertificateMatchesAnySanEntry)
{
const char* san = "DNS:one.example.com,DNS:two.example.com";

EXPECT_TRUE(verifyHost(san, "one.example.com"));
EXPECT_TRUE(verifyHost(san, "two.example.com"));
EXPECT_FALSE(verifyHost(san, "one.not-example.com"));
}

TEST_F(ResolverTests, VerifyCertificateIpHostRequiresIpSan)
{
// Connecting to an IP rather than a name is verified against the
// certificate's iPAddress SAN. A DNS SAN does not cover it, so such
// connections are intentionally rejected
EXPECT_FALSE(verifyHost("DNS:one.example.com", "10.20.30.40"));

EXPECT_TRUE(
verifyHost("DNS:one.example.com,IP:10.20.30.40", "10.20.30.40"));
EXPECT_FALSE(
verifyHost("DNS:one.example.com,IP:10.20.30.40", "10.20.30.99"));
}

TEST_F(ResolverTests, VerifyCertificateDoesNotOverrideChainFailure)
{
StoreContextGuard storeCtx;
ASSERT_THAT(storeCtx.get(), NotNull());
ASSERT_THAT(X509_STORE_CTX_init(storeCtx.get(), 0, 0, 0), Eq(1));

CertificateGuard cert;
ASSERT_THAT(cert.get(), NotNull());
ASSERT_TRUE(addSubjectAltName(cert.get(), "DNS:one.example.com"));

X509_STORE_CTX_set_current_cert(storeCtx.get(), cert.get());

boost::asio::ssl::verify_context ctx(storeCtx.get());

// A matching host name must not rescue a chain which failed to verify
EXPECT_FALSE(
AsioResolver::verifyCertificate("one.example.com", false, ctx));
}

TEST_F(ResolverTests, badresolve)
{
using bdlf::PlaceHolders::_1;
Expand Down
Loading