mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-21 11:22:26 +00:00
Merge branch 'develop' into ximinez/loanpay-assertion-develop
This commit is contained in:
@@ -1416,6 +1416,12 @@
|
||||
# in this section to a comma-separated list of the addresses
|
||||
# of your Clio servers, in order to bypass xrpld's rate limiting.
|
||||
#
|
||||
# TLS/SSL can be enabled for gRPC by specifying ssl_cert and ssl_key.
|
||||
# Both parameters must be provided together. The ssl_cert_chain parameter
|
||||
# is optional and provides intermediate CA certificates for the certificate
|
||||
# chain. The ssl_client_ca parameter is optional and enables mutual TLS
|
||||
# (client certificate verification).
|
||||
#
|
||||
# This port is commented out but can be enabled by removing
|
||||
# the '#' from each corresponding line including the entry under [server]
|
||||
#
|
||||
@@ -1465,11 +1471,74 @@ admin = 127.0.0.1
|
||||
protocol = ws
|
||||
send_queue_limit = 500
|
||||
|
||||
# gRPC TLS/SSL Configuration
|
||||
#
|
||||
# The gRPC port supports optional TLS/SSL encryption. When TLS is not
|
||||
# configured, the gRPC server will accept unencrypted connections.
|
||||
#
|
||||
# ssl_cert = <filename>
|
||||
# ssl_key = <filename>
|
||||
#
|
||||
# To enable TLS for gRPC, both ssl_cert and ssl_key must be specified.
|
||||
# If only one is provided, xrpld will fail to start.
|
||||
#
|
||||
# ssl_cert: Path to the server's SSL certificate file in PEM format.
|
||||
# ssl_key: Path to the server's SSL private key file in PEM format.
|
||||
#
|
||||
# When configured, the gRPC server will only accept TLS-encrypted
|
||||
# connections. Clients must use TLS (secure) channel credentials rather
|
||||
# than plaintext / insecure connections.
|
||||
#
|
||||
# ssl_cert_chain = <filename>
|
||||
#
|
||||
# Optional. Path to intermediate CA certificate(s) in PEM format that
|
||||
# complete the server's certificate chain.
|
||||
#
|
||||
# This file should contain the intermediate CA certificate(s) needed
|
||||
# to build a trust chain from the server certificate (ssl_cert) to a
|
||||
# root CA that clients trust. Multiple certificates should be
|
||||
# concatenated in PEM format.
|
||||
#
|
||||
# This is needed when your server certificate was signed by an
|
||||
# intermediate CA rather than directly by a root CA. Without this,
|
||||
# clients may fail to verify your server certificate.
|
||||
#
|
||||
# If not specified, only the server certificate from ssl_cert will be
|
||||
# presented to clients.
|
||||
#
|
||||
# ssl_client_ca = <filename>
|
||||
#
|
||||
# Optional. Path to a CA certificate file in PEM format for verifying
|
||||
# client certificates (mutual TLS / mTLS).
|
||||
#
|
||||
# When specified, the gRPC server will verify client certificates
|
||||
# against this CA. This enables mutual authentication where both the
|
||||
# server and client verify each other's identity.
|
||||
#
|
||||
# This is typically NOT needed for public-facing gRPC servers. Only
|
||||
# use this if you want to restrict access to clients with valid
|
||||
# certificates signed by the specified CA.
|
||||
#
|
||||
# If not specified, the server will use one-way TLS (server
|
||||
# authentication only) and will accept connections from any client.
|
||||
#
|
||||
[port_grpc]
|
||||
port = 50051
|
||||
ip = 127.0.0.1
|
||||
secure_gateway = 127.0.0.1
|
||||
|
||||
# Optional TLS/SSL configuration for gRPC
|
||||
# To enable TLS, uncomment and configure both ssl_cert and ssl_key:
|
||||
#ssl_cert = /etc/ssl/certs/grpc-server.crt
|
||||
#ssl_key = /etc/ssl/private/grpc-server.key
|
||||
|
||||
# Optional: Include intermediate CA certificates for complete certificate chain
|
||||
#ssl_cert_chain = /etc/ssl/certs/grpc-intermediate-ca.crt
|
||||
|
||||
# Optional: Enable mutual TLS (client certificate verification)
|
||||
# Uncomment to require and verify client certificates:
|
||||
#ssl_client_ca = /etc/ssl/certs/grpc-client-ca.crt
|
||||
|
||||
#[port_ws_public]
|
||||
#port = 6005
|
||||
#ip = 127.0.0.1
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace xrpl {
|
||||
class ValidPermissionedDEX
|
||||
{
|
||||
bool regularOffers_ = false;
|
||||
bool badHybrids_ = false;
|
||||
bool badHybridsOld_ = false; // pre-fixSecurity3_1_3: missing field/domain or size > 1
|
||||
bool badHybrids_ = false; // post-fixSecurity3_1_3: also catches size == 0 (size != 1)
|
||||
hash_set<uint256> domains_;
|
||||
|
||||
public:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -61,10 +62,26 @@ offerInDomain(
|
||||
if (sleOffer->getFieldH256(sfDomainID) != domainID)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
|
||||
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
|
||||
if (view.rules().enabled(fixSecurity3_1_3))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
// post-fixSecurity3_1_3: also catches empty sfAdditionalBooks (size == 0)
|
||||
if (sleOffer->isFlag(lsfHybrid) &&
|
||||
(!sleOffer->isFieldPresent(sfAdditionalBooks) ||
|
||||
sleOffer->getFieldArray(sfAdditionalBooks).size() != 1))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID
|
||||
<< " missing or malformed AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// pre-fixSecurity3_1_3: only check for missing sfAdditionalBooks
|
||||
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -40,11 +41,17 @@ ValidPermissionedDEX::visitEntry(
|
||||
regularOffers_ = true;
|
||||
}
|
||||
|
||||
// if a hybrid offer is missing domain or additional book, there's
|
||||
// something wrong
|
||||
// pre-fixSecurity3_1_3: hybrid offer missing domain, missing
|
||||
// sfAdditionalBooks, or sfAdditionalBooks has more than one entry
|
||||
if (after->isFlag(lsfHybrid) &&
|
||||
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
|
||||
after->getFieldArray(sfAdditionalBooks).size() > 1))
|
||||
badHybridsOld_ = true;
|
||||
|
||||
// post-fixSecurity3_1_3: same as above but also catches size == 0
|
||||
if (after->isFlag(lsfHybrid) &&
|
||||
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
|
||||
after->getFieldArray(sfAdditionalBooks).size() != 1))
|
||||
badHybrids_ = true;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +70,8 @@ ValidPermissionedDEX::finalize(
|
||||
|
||||
// For each offercreate transaction, check if
|
||||
// permissioned offers are valid
|
||||
if (txType == ttOFFER_CREATE && badHybrids_)
|
||||
bool const isMalformed = view.rules().enabled(fixSecurity3_1_3) ? badHybrids_ : badHybridsOld_;
|
||||
if (txType == ttOFFER_CREATE && isMalformed)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
|
||||
return false;
|
||||
|
||||
@@ -594,7 +594,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
|
||||
auto const cancelSequence = ctx_.tx[~sfOfferSequence];
|
||||
|
||||
// Note that we we use the value from the sequence or ticket as the
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// offer sequence. For more explanation see comments in SeqProxy.h.
|
||||
auto const offerSequence = ctx_.tx.getSeqValue();
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ EscrowCreate::doApply()
|
||||
return tecDST_TAG_NEEDED;
|
||||
}
|
||||
|
||||
// Create escrow in ledger. Note that we we use the value from the
|
||||
// Create escrow in ledger. Note that we use the value from the
|
||||
// sequence or ticket. For more explanation see comments in SeqProxy.h.
|
||||
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
|
||||
auto const slep = std::make_shared<SLE>(escrowKeylet);
|
||||
|
||||
@@ -134,7 +134,7 @@ PaymentChannelCreate::doApply()
|
||||
|
||||
// Create PayChan in ledger.
|
||||
//
|
||||
// Note that we we use the value from the sequence or ticket as the
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// payChan sequence. For more explanation see comments in SeqProxy.h.
|
||||
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
|
||||
auto const slep = std::make_shared<SLE>(payChanKeylet);
|
||||
|
||||
851
src/test/app/GRPCServerTLS_test.cpp
Normal file
851
src/test/app/GRPCServerTLS_test.cpp
Normal file
@@ -0,0 +1,851 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/get_ledger.pb.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
|
||||
#include <grpcpp/client_context.h>
|
||||
#include <grpcpp/create_channel.h>
|
||||
#include <grpcpp/grpcpp.h>
|
||||
#include <grpcpp/security/credentials.h>
|
||||
#include <grpcpp/support/status.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kCA_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"fAbAYqu0rfMFHUYjzIVnu8WRCC56qYHO5tU=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kSERVER_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kSERVER_KEY_CONTENT =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
"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\n"
|
||||
"S/RYUSUkZ4VvqFUfo7wT8x18urb87w==\n"
|
||||
"-----END PRIVATE KEY-----\n";
|
||||
|
||||
constexpr std::string_view kCLIENT_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFeDCCA2CgAwIBAgIJAIErcpMflkrSMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV\n"
|
||||
"BAYTAlVTMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9S\n"
|
||||
"aXBwbGVkIFRlc3QgQ0ExEDAOBgNVBAMMB1Rlc3QgQ0EwIBcNMjYwNDA5MTMyNTA3\n"
|
||||
"WhgPMjEyNjAzMTYxMzI1MDdaMFoxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0\n"
|
||||
"MQ0wCwYDVQQHDARUZXN0MRcwFQYDVQQKDA5SaXBwbGVkIENsaWVudDEUMBIGA1UE\n"
|
||||
"AwwLdGVzdC1jbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP\n"
|
||||
"QHttw3TLjOqYS3VkLF3KMRaP2ZtO6A1mXfTbqbKvD41Fazf/cM/v9lPMAlRd2SEY\n"
|
||||
"3MeE8KVddKJwsbF0kNgDkKB5D5V42WrTw5biFNMOeHAZMR/oWChIvZbbGbDxIdIO\n"
|
||||
"2+W3X0kjpa2eKcK9qBk8xoyIeilyXtleGWuWvHxiZP9iGHxaTWB+wIKUIK6vrEOb\n"
|
||||
"iO3P/9XPpHzsBt0HdTDh4V7fwnr2UndVeQyBwUwLn6pd73sTKBfA26YppRwDjPIj\n"
|
||||
"6NYtF3I28lRCUo+47TAVZM97gjN9oEwyHIVtOc7fnZPwtN26M5v5083SGXU1k/PN\n"
|
||||
"3xGAlDUiCF3RSMbBRylGgUVtsAD57i8tI2SqCr+ZG233VFiOdwTRKKVNTyMC9TCJ\n"
|
||||
"dCFFEDFDHTTSimKRQKy0Cm2qoL6JBkziAIiu/0Zzv9YjAAnRoJ2cweMXQ/9z1oWe\n"
|
||||
"EUZBLRsggYQ8FbM13FOkOs8IlzacSuhwrYKOq8LsMX4cH2mnn783FtXXqrL/xfL7\n"
|
||||
"11KhzGpZNrz187ilJ+ZsmP9D6vCBP/tR7V52dgtB6I291o8zxdH8GheIGenEFaZa\n"
|
||||
"oAwyN2FuJgXZqx9319I9gYerZ/BbUzA2MuOxFd0ywtdcTPqKiyAQ9rxQVCVQyYWj\n"
|
||||
"kfBEYRzWxjfj3XhNprxdm3cauz01NAoTDiz52dZhGQIDAQABo0IwQDAdBgNVHQ4E\n"
|
||||
"FgQUXVKwiGRrXC1sjK2D86jsjMVV0XgwHwYDVR0jBBgwFoAUDyuYV8S3U490Kn1c\n"
|
||||
"xqX04GuiBd8wDQYJKoZIhvcNAQELBQADggIBACpHTm9GZMZ7OPhqVo4VltVOW9a9\n"
|
||||
"LLDsVYmvpAF9+yjZGims6+p3f7eY+o+TRdUE4HEBCmH0UiFVODXCZSoqXo6y9xq7\n"
|
||||
"TS1dmXll1Sajbfi7YXsM8CAUb+cSsHtmT57JtbGicDiVXAqIOlT65yXkuujdcEa0\n"
|
||||
"OAw45vJDkWk/6nneFJKdTs7aT3fvIGTlMAxgMJngVsA8BRsX8TWoo05Lum8ClNgi\n"
|
||||
"s6mtl+nUvjOaM0omFL/K9kqLy7OJAbmE5xuhkC9q6Kn0pHBL4u0YSWaWTpyrvAX7\n"
|
||||
"BuOE0G1JezcCAcqJvXbKFvhnOSHTvzdlMgXhteGW8Uwgf8cGKtVLSwh6YTjI1XaL\n"
|
||||
"DkNZfJabAyH7BsGGbAd9Jts4h+4auPqHgcpEz16280oCgZdcfLSP0UKrfwYuXOar\n"
|
||||
"8KWlVRFl2NBpEJwRf2KjZFQUqYoX1MmfX0gyy+kk0ZP12L7oGNqAxkaWySfb4PSv\n"
|
||||
"Hsnb8iD6sIJQjZvZ/2wLV8xwFTbFjvGbmSx+XLnMUVV8cVAMUpZz5X2R9pBvpVi4\n"
|
||||
"KfUccTvIVA0p1wFSdWYQ0+QNxHxZGX1rin6KVUdV1z8K6J3FgGlRqzfz4bruGpXs\n"
|
||||
"6vX5vqF9KTFpwLTOxDU+kAoIfHowHeu/LQX1l+rk1ww2UZQ1zvgKb6fxWMtviq3F\n"
|
||||
"cTe8jkzRqYdUfAoV\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kCLIENT_KEY_CONTENT =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
"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\n"
|
||||
"+6f4hsbp29hg+BUZ+UPdk7QyCKpZD1A=\n"
|
||||
"-----END PRIVATE KEY-----\n";
|
||||
|
||||
/**
|
||||
* RAII helper for managing temporary TLS certificates in tests.
|
||||
*
|
||||
* Creates a temporary directory and writes test certificates to it.
|
||||
* Automatically cleans up the directory when destroyed.
|
||||
*/
|
||||
class TemporaryTLSCertificates
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view kCA_CERT_FILENAME = "ca.pem";
|
||||
static constexpr std::string_view kSERVER_CERT_FILENAME = "server_cert.pem";
|
||||
static constexpr std::string_view kSERVER_KEY_FILENAME = "server_key.pem";
|
||||
static constexpr std::string_view kCLIENT_CERT_FILENAME = "client_cert.pem";
|
||||
static constexpr std::string_view kCLIENT_KEY_FILENAME = "client_key.pem";
|
||||
static constexpr std::string_view kCERTS_DIR_PREFIX = "grpc_tls_test_";
|
||||
|
||||
TemporaryTLSCertificates()
|
||||
{
|
||||
auto tmpDir = std::filesystem::temp_directory_path();
|
||||
auto uniqueDirName =
|
||||
boost::filesystem::unique_path(std::string(kCERTS_DIR_PREFIX) + "%%%%%%%%");
|
||||
tempDir_ = tmpDir / uniqueDirName.string();
|
||||
std::filesystem::create_directories(tempDir_);
|
||||
|
||||
writeFile(tempDir_ / kCA_CERT_FILENAME, kCA_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kSERVER_CERT_FILENAME, kSERVER_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kSERVER_KEY_FILENAME, kSERVER_KEY_CONTENT);
|
||||
writeFile(tempDir_ / kCLIENT_CERT_FILENAME, kCLIENT_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kCLIENT_KEY_FILENAME, kCLIENT_KEY_CONTENT);
|
||||
}
|
||||
|
||||
virtual ~TemporaryTLSCertificates()
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(tempDir_, ec);
|
||||
}
|
||||
|
||||
TemporaryTLSCertificates(TemporaryTLSCertificates const&) = delete;
|
||||
TemporaryTLSCertificates&
|
||||
operator=(TemporaryTLSCertificates const&) = delete;
|
||||
TemporaryTLSCertificates(TemporaryTLSCertificates&&) = delete;
|
||||
TemporaryTLSCertificates&
|
||||
operator=(TemporaryTLSCertificates&&) = delete;
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getCACertPath() const
|
||||
{
|
||||
return tempDir_ / kCA_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getServerCertPath() const
|
||||
{
|
||||
return tempDir_ / kSERVER_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getServerKeyPath() const
|
||||
{
|
||||
return tempDir_ / kSERVER_KEY_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getClientCertPath() const
|
||||
{
|
||||
return tempDir_ / kCLIENT_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getClientKeyPath() const
|
||||
{
|
||||
return tempDir_ / kCLIENT_KEY_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getTempDir() const
|
||||
{
|
||||
return tempDir_;
|
||||
}
|
||||
|
||||
private:
|
||||
static void
|
||||
writeFile(std::filesystem::path const& path, std::string_view content)
|
||||
{
|
||||
std::ofstream file(path);
|
||||
if (!file)
|
||||
throw std::runtime_error("Failed to create file: " + path.string());
|
||||
file << content;
|
||||
if (!file)
|
||||
throw std::runtime_error("Failed to write file: " + path.string());
|
||||
}
|
||||
|
||||
std::filesystem::path tempDir_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
/**
|
||||
* Helper function to make a simple gRPC call to test connectivity.
|
||||
* Returns true if the call succeeded, false otherwise.
|
||||
*/
|
||||
bool
|
||||
makeTestGRPCCall(std::unique_ptr<org::xrpl::rpc::v1::XRPLedgerAPIService::Stub> const& stub)
|
||||
{
|
||||
grpc::ClientContext context;
|
||||
org::xrpl::rpc::v1::GetLedgerRequest const request;
|
||||
org::xrpl::rpc::v1::GetLedgerResponse response;
|
||||
|
||||
// Set a short deadline to avoid hanging on failed connections
|
||||
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(2));
|
||||
|
||||
grpc::Status const status = stub->GetLedger(&context, request, &response);
|
||||
return status.ok();
|
||||
}
|
||||
|
||||
class GRPCServerTLS_test : public beast::unit_test::suite, public TemporaryTLSCertificates
|
||||
{
|
||||
public:
|
||||
void
|
||||
testWithoutTLS()
|
||||
{
|
||||
testcase("GRPCServer without TLS");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config without TLS settings
|
||||
auto cfg = envconfig(addGrpcConfig);
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
// Test 1: Plaintext client should connect successfully
|
||||
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(makeTestGRPCCall(plaintextStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithValidTLS()
|
||||
{
|
||||
testcase("GRPCServer with valid TLS configuration (no mutual TLS)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with just server cert and key (no client verification)
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLS, getServerCertPath().string(), getServerKeyPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: Plaintext client should FAIL against TLS server
|
||||
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(plaintextStub));
|
||||
|
||||
// Test 2: TLS client with server CA should succeed
|
||||
grpc::SslCredentialsOptions sslOpts;
|
||||
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithMutualTLS()
|
||||
{
|
||||
testcase("GRPCServer with mutual TLS (client verification enabled)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with server cert, key, and CA for client verification
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLSAndClientCA,
|
||||
getServerCertPath().string(),
|
||||
getServerKeyPath().string(),
|
||||
getCACertPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
|
||||
grpc::SslCredentialsOptions sslOptsNoClient;
|
||||
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
|
||||
|
||||
// Test 2: TLS client WITH client certificate should succeed
|
||||
grpc::SslCredentialsOptions sslOptsWithClient;
|
||||
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
|
||||
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
|
||||
}
|
||||
|
||||
void
|
||||
testWithMissingKey()
|
||||
{
|
||||
testcase("GRPCServer with cert but no key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config with only cert (missing key)
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for incomplete TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithMissingCert()
|
||||
{
|
||||
testcase("GRPCServer with key but no cert");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config with only key (missing cert)
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
// Intentionally omit ssl_cert
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for incomplete TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithClientCAButNoTLS()
|
||||
{
|
||||
testcase("GRPCServer with ssl_client_ca but without both ssl_cert and ssl_key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test 1: ssl_client_ca specified without any TLS config
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit both ssl_cert and ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca without TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"ssl_client_ca requires both ssl_cert and ssl_key") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: ssl_client_ca with only ssl_cert (missing ssl_key)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca with only ssl_cert");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_cert is specified without ssl_key
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: ssl_client_ca with only ssl_key (missing ssl_cert)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit ssl_cert
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca with only ssl_key");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_key is specified without ssl_cert
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithCertChainButNoTLS()
|
||||
{
|
||||
testcase("GRPCServer with ssl_cert_chain but without both ssl_cert and ssl_key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test 1: ssl_cert_chain specified without any TLS config
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
|
||||
// Intentionally omit both ssl_cert and ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_cert_chain without TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"ssl_cert_chain requires both ssl_cert and ssl_key") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: ssl_cert_chain with only ssl_cert (missing ssl_key)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_cert_chain with only ssl_cert");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_cert is specified without ssl_key
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithCertChain()
|
||||
{
|
||||
testcase("GRPCServer with ssl_cert_chain for intermediate CA certificates");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with server cert, key, and cert chain (intermediate CA)
|
||||
// In this test, we use the CA cert as a stand-in for an intermediate CA cert
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLSAndCertChain,
|
||||
getServerCertPath().string(),
|
||||
getServerKeyPath().string(),
|
||||
getCACertPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test: TLS client should be able to connect (no client cert required)
|
||||
grpc::SslCredentialsOptions sslOpts;
|
||||
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
|
||||
|
||||
// Insecure client should fail
|
||||
auto insecureStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(insecureStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidCertFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent certificate file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", "/nonexistent/path/to/cert.pem");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidKeyFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent key file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", "/nonexistent/path/to/key.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidCertChainFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent cert chain file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", "/nonexistent/path/to/chain.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidClientCAFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent client CA file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", "/nonexistent/path/to/ca.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithEmptyClientCAFile()
|
||||
{
|
||||
testcase("GRPCServer with empty client CA file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create an empty file for client CA
|
||||
auto emptyCAPath = getTempDir() / "empty_ca.pem";
|
||||
std::ofstream emptyFile(emptyCAPath);
|
||||
emptyFile.close();
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", emptyCAPath.string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start due to empty CA file
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithBothCertChainAndClientCA()
|
||||
{
|
||||
testcase("GRPCServer with both cert chain and client CA (full mTLS with intermediates)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with all TLS features enabled: cert, key, cert_chain, and client_ca
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set(
|
||||
"ssl_cert_chain", getCACertPath().string()); // Using CA as intermediate
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server started successfully
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
|
||||
grpc::SslCredentialsOptions sslOptsNoClient;
|
||||
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
|
||||
|
||||
// Test 2: TLS client WITH client certificate should succeed
|
||||
grpc::SslCredentialsOptions sslOptsWithClient;
|
||||
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
|
||||
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testWithoutTLS();
|
||||
testWithValidTLS();
|
||||
testWithMutualTLS();
|
||||
testWithMissingKey();
|
||||
testWithMissingCert();
|
||||
testWithClientCAButNoTLS();
|
||||
testWithCertChainButNoTLS();
|
||||
testWithCertChain();
|
||||
testWithInvalidCertFile();
|
||||
testWithInvalidKeyFile();
|
||||
testWithInvalidCertChainFile();
|
||||
testWithInvalidClientCAFile();
|
||||
testWithEmptyClientCAFile();
|
||||
testWithBothCertChainAndClientCA();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(GRPCServerTLS, app, xrpl);
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
@@ -1774,8 +1774,10 @@ class Invariants_test : public beast::unit_test::suite
|
||||
using namespace test::jtx;
|
||||
|
||||
bool const fixPDEnabled = features[fixPermissionedDomainInvariant];
|
||||
bool const fixS313Enabled = features[fixSecurity3_1_3];
|
||||
|
||||
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fix" : "");
|
||||
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fixPD" : "") +
|
||||
std::string(fixS313Enabled ? " fixS313" : "");
|
||||
|
||||
doInvariantCheck(
|
||||
Env(*this, features),
|
||||
@@ -1863,6 +1865,45 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
// empty sfAdditionalBooks (size 0)
|
||||
{
|
||||
Env env1(*this, features);
|
||||
|
||||
Account const A1{"A1"};
|
||||
Account const A2{"A2"};
|
||||
env1.fund(XRP(1000), A1, A2);
|
||||
env1.close();
|
||||
|
||||
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, A1, A2);
|
||||
env1.close();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env1),
|
||||
A1,
|
||||
A2,
|
||||
fixS313Enabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
|
||||
: std::vector<std::string>{},
|
||||
[&pd1](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
Keylet const offerKey = keylet::offer(A2.id(), 10);
|
||||
auto sleOffer = std::make_shared<SLE>(offerKey);
|
||||
sleOffer->setAccountID(sfAccount, A2);
|
||||
sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10));
|
||||
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
|
||||
sleOffer->setFlag(lsfHybrid);
|
||||
sleOffer->setFieldH256(sfDomainID, pd1);
|
||||
|
||||
STArray const bookArr; // empty array, size 0
|
||||
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
|
||||
ac.view().insert(sleOffer);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttOFFER_CREATE, [&](STObject&) {}},
|
||||
fixS313Enabled
|
||||
? std::initializer_list<TER>{tecINVARIANT_FAILED, tecINVARIANT_FAILED}
|
||||
: std::initializer_list<TER>{tesSUCCESS, tesSUCCESS});
|
||||
}
|
||||
|
||||
// hybrid offer missing sfAdditionalBooks
|
||||
{
|
||||
Env env1(*this, features);
|
||||
@@ -4061,6 +4102,10 @@ public:
|
||||
testPermissionedDomainInvariants(defaultAmendments() - fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(defaultAmendments() | fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(defaultAmendments() - fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(
|
||||
(defaultAmendments() | fixPermissionedDomainInvariant) - fixSecurity3_1_3);
|
||||
testPermissionedDEX(
|
||||
defaultAmendments() - fixPermissionedDomainInvariant - fixSecurity3_1_3);
|
||||
testNoModifiedUnmodifiableFields();
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -28,6 +30,8 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
@@ -35,6 +39,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -1385,6 +1390,73 @@ class PermissionedDEX_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
|
||||
}
|
||||
|
||||
void
|
||||
testHybridMalformedOffer(FeatureBitset features)
|
||||
{
|
||||
bool const fixS313Enabled = features[fixSecurity3_1_3];
|
||||
|
||||
testcase << "Hybrid offer with empty AdditionalBooks"
|
||||
<< (fixS313Enabled ? " (fixSecurity3_1_3 enabled)"
|
||||
: " (fixSecurity3_1_3 disabled)");
|
||||
|
||||
// offerInDomain has two code paths gated by fixSecurity3_1_3:
|
||||
//
|
||||
// pre-fix: only rejects a hybrid offer when sfAdditionalBooks is
|
||||
// entirely absent — an empty array (size 0) passes through.
|
||||
// post-fix: also rejects a hybrid offer whose sfAdditionalBooks array
|
||||
// has size != 1 (i.e. 0 or >1 entries).
|
||||
//
|
||||
// We create a valid hybrid offer, then directly manipulate its SLE to
|
||||
// produce the size==0 case that cannot occur via normal transactions,
|
||||
// and verify that the two code paths produce the expected outcomes.
|
||||
//
|
||||
// Note: the PermissionedDEX invariant checker (ValidPermissionedDEX)
|
||||
// does not flag this malformation for ttPAYMENT — only for
|
||||
// ttOFFER_CREATE — so the without-fix payment completes as tesSUCCESS.
|
||||
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
// Create a valid hybrid offer (sfAdditionalBooks has exactly 1 entry)
|
||||
auto const bobOfferSeq{env.seq(bob)};
|
||||
env(offer(bob, XRP(10), USD(10)), txflags(tfHybrid), domain(domainID));
|
||||
env.close();
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
|
||||
// Directly manipulate the offer SLE in the open ledger so that
|
||||
// sfAdditionalBooks is present but empty (size 0). This is the
|
||||
// malformed state that fixSecurity3_1_3 is designed to catch.
|
||||
auto const offerKey = keylet::offer(bob.id(), bobOfferSeq);
|
||||
env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
|
||||
auto const sle = view.read(offerKey);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto replacement = std::make_shared<SLE>(*sle, sle->key());
|
||||
replacement->setFieldArray(sfAdditionalBooks, STArray{});
|
||||
view.rawReplace(replacement);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (fixS313Enabled)
|
||||
{
|
||||
// post-fixSecurity3_1_3: offerInDomain rejects the malformed
|
||||
// offer (size == 0), so no valid domain offer is found.
|
||||
env(pay(alice, carol, USD(10)),
|
||||
path(~USD),
|
||||
sendmax(XRP(10)),
|
||||
domain(domainID),
|
||||
ter(tecPATH_PARTIAL));
|
||||
}
|
||||
else
|
||||
{
|
||||
// pre-fixSecurity3_1_3: offerInDomain only checks for a missing
|
||||
// sfAdditionalBooks field; size == 0 passes through, so the
|
||||
// malformed offer is crossed and the payment succeeds.
|
||||
env(pay(alice, carol, USD(10)), path(~USD), sendmax(XRP(10)), domain(domainID));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1406,6 +1478,8 @@ public:
|
||||
testHybridBookStep(all);
|
||||
testHybridInvalidOffer(all);
|
||||
testHybridOfferDirectories(all);
|
||||
testHybridMalformedOffer(all);
|
||||
testHybridMalformedOffer(all - fixSecurity3_1_3);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -107,6 +107,52 @@ std::unique_ptr<Config> addGrpcConfig(std::unique_ptr<Config>);
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithSecureGateway(std::unique_ptr<Config>, std::string const& secureGateway);
|
||||
|
||||
/// @brief add a grpc address, port and TLS certificate/key paths to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with TLS enabled.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLS(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath);
|
||||
|
||||
/// @brief add a grpc address, port and TLS certificate/key/client CA paths to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with mutual TLS (client certificate verification) enabled.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
/// @param clientCAPath path to SSL client CA certificate file for mTLS
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndClientCA(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& clientCAPath);
|
||||
|
||||
/// @brief add a grpc address, port and TLS with server cert chain to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with TLS enabled and intermediate CA certificates.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
/// @param certChainPath path to SSL intermediate CA certificate(s) file
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndCertChain(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& certChainPath);
|
||||
|
||||
std::unique_ptr<Config>
|
||||
makeConfig(
|
||||
std::map<std::string, std::string> extraTxQ = {},
|
||||
|
||||
@@ -48,7 +48,7 @@ class JSONRPCClient : public AbstractClient
|
||||
continue;
|
||||
ParsedPort pp;
|
||||
parse_Port(pp, cfg[name], log);
|
||||
if (pp.protocol.count("http") == 0)
|
||||
if (not pp.protocol.contains("http"))
|
||||
continue;
|
||||
using namespace boost::asio::ip;
|
||||
if (pp.ip && pp.ip->is_unspecified())
|
||||
@@ -91,12 +91,6 @@ public:
|
||||
stream_.connect(ep_);
|
||||
}
|
||||
|
||||
~JSONRPCClient() override
|
||||
{
|
||||
// stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
|
||||
// stream_.close();
|
||||
}
|
||||
|
||||
/*
|
||||
Return value is an Object type with up to three keys:
|
||||
status
|
||||
|
||||
@@ -132,6 +132,49 @@ addGrpcConfigWithSecureGateway(std::unique_ptr<Config> cfg, std::string const& s
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLS(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndClientCA(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& clientCAPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", clientCAPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndCertChain(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& certChainPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", certChainPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
makeConfig(
|
||||
std::map<std::string, std::string> extraTxQ,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/core/CurrentThreadName.h>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/icl/interval_set.hpp>
|
||||
|
||||
#include <grpc/grpc_security_constants.h>
|
||||
#include <grpcpp/completion_queue.h>
|
||||
#include <grpcpp/security/server_credentials.h>
|
||||
#include <grpcpp/server_builder.h>
|
||||
@@ -387,6 +389,48 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
|
||||
Throw<std::runtime_error>("Error parsing secure_gateway section");
|
||||
}
|
||||
}
|
||||
|
||||
// Read TLS certificate configuration (optional)
|
||||
sslCertPath_ = section.get("ssl_cert");
|
||||
sslKeyPath_ = section.get("ssl_key");
|
||||
sslCertChainPath_ = section.get("ssl_cert_chain");
|
||||
sslClientCAPath_ = section.get("ssl_client_ca");
|
||||
|
||||
// If cert or key is specified, both must be specified
|
||||
if (sslCertPath_.has_value() || sslKeyPath_.has_value())
|
||||
{
|
||||
if (!sslCertPath_.has_value() || !sslKeyPath_.has_value())
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Both ssl_cert and ssl_key must be specified for gRPC TLS";
|
||||
Throw<std::runtime_error>("Incomplete TLS configuration for gRPC");
|
||||
}
|
||||
JLOG(journal_.info()) << "gRPC TLS enabled with certificate: " << *sslCertPath_;
|
||||
}
|
||||
|
||||
// Validate TLS configuration consistency: ssl_cert_chain only makes sense when TLS is
|
||||
// enabled
|
||||
if (sslCertChainPath_.has_value() &&
|
||||
(!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "ssl_cert_chain specified for gRPC without both ssl_cert and ssl_key; "
|
||||
<< "this is an invalid TLS configuration";
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid gRPC TLS configuration: ssl_cert_chain requires both ssl_cert and "
|
||||
"ssl_key");
|
||||
}
|
||||
|
||||
// Validate TLS configuration consistency: ssl_client_ca only makes sense when TLS is
|
||||
// enabled
|
||||
if (sslClientCAPath_.has_value() && (!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "ssl_client_ca specified for gRPC without both ssl_cert and ssl_key; "
|
||||
<< "this is an invalid TLS configuration";
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid gRPC TLS configuration: ssl_client_ca requires both ssl_cert and ssl_key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +602,104 @@ GRPCServerImpl::setupListeners()
|
||||
return requests;
|
||||
}
|
||||
|
||||
std::shared_ptr<grpc::ServerCredentials>
|
||||
GRPCServerImpl::createServerCredentials()
|
||||
{
|
||||
if (not sslCertPath_.has_value() or not sslKeyPath_.has_value())
|
||||
{
|
||||
JLOG(journal_.info()) << "Configuring gRPC server without TLS";
|
||||
return grpc::InsecureServerCredentials();
|
||||
}
|
||||
|
||||
JLOG(journal_.info()) << "Configuring gRPC server with TLS";
|
||||
|
||||
try
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
grpc::SslServerCredentialsOptions sslOpts;
|
||||
grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair;
|
||||
|
||||
std::string const certContents = getFileContents(ec, *sslCertPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to read gRPC SSL certificate file: " << *sslCertPath_
|
||||
<< " - " << ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string const keyContents = getFileContents(ec, *sslKeyPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to read gRPC SSL key file: " << *sslKeyPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
keyCertPair.private_key = keyContents;
|
||||
|
||||
// Read intermediate CA certificates for server certificate chain (optional)
|
||||
std::string certChainContents;
|
||||
if (sslCertChainPath_.has_value())
|
||||
{
|
||||
certChainContents = getFileContents(ec, *sslCertChainPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to read gRPC SSL cert chain file: " << *sslCertChainPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Read CA certificate for client verification (mTLS, optional)
|
||||
if (sslClientCAPath_.has_value())
|
||||
{
|
||||
auto const clientCAContents = getFileContents(ec, *sslClientCAPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to read gRPC SSL client CA file: " << *sslClientCAPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (clientCAContents.empty())
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Empty/truncated gRPC SSL client CA file: " << *sslClientCAPath_
|
||||
<< " - failed to configure mutual TLS"; // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
sslOpts.pem_root_certs = clientCAContents;
|
||||
sslOpts.client_certificate_request =
|
||||
GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY;
|
||||
JLOG(journal_.info()) << "gRPC mutual TLS enabled - client certificates will be "
|
||||
"required and verified";
|
||||
}
|
||||
|
||||
// Combine server cert with intermediate CA certs for complete chain
|
||||
keyCertPair.cert_chain = certContents;
|
||||
if (!certChainContents.empty())
|
||||
{
|
||||
keyCertPair.cert_chain += '\n' + certChainContents;
|
||||
JLOG(journal_.info()) << "gRPC server certificate chain configured with "
|
||||
"intermediate CA certificates"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
sslOpts.pem_key_cert_pairs.push_back(keyCertPair);
|
||||
|
||||
JLOG(journal_.info()) << "gRPC TLS credentials configured successfully"; // LCOV_EXCL_LINE
|
||||
return grpc::SslServerCredentials(sslOpts);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(journal_.error()) << "Exception while configuring gRPC TLS: "
|
||||
<< e.what(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
GRPCServerImpl::start()
|
||||
{
|
||||
@@ -565,24 +707,63 @@ GRPCServerImpl::start()
|
||||
if (serverAddress_.empty())
|
||||
return false;
|
||||
|
||||
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_;
|
||||
// Determine TLS mode for logging
|
||||
bool const tlsEnabled = sslCertPath_.has_value() && sslKeyPath_.has_value();
|
||||
bool const mtlsEnabled = tlsEnabled && sslClientCAPath_.has_value();
|
||||
|
||||
std::string tlsMode = "without TLS";
|
||||
if (mtlsEnabled)
|
||||
{
|
||||
tlsMode = "with mutual TLS (mTLS)";
|
||||
}
|
||||
else if (tlsEnabled)
|
||||
{
|
||||
tlsMode = "with TLS";
|
||||
}
|
||||
|
||||
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_ << " "
|
||||
<< tlsMode; // LCOV_EXCL_LINE
|
||||
|
||||
grpc::ServerBuilder builder;
|
||||
|
||||
// Listen on the given address without any authentication mechanism.
|
||||
// Actually binded port will be returned into "port" variable.
|
||||
int port = 0;
|
||||
builder.AddListeningPort(serverAddress_, grpc::InsecureServerCredentials(), &port);
|
||||
|
||||
// Create credentials (TLS or insecure) based on configuration
|
||||
auto credentials = createServerCredentials();
|
||||
if (!credentials)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to create gRPC server credentials for " << serverAddress_
|
||||
<< " (TLS mode: " << tlsMode
|
||||
<< ") - server will not start"; // LCOV_EXCL_LINE
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add listening port with appropriate credentials
|
||||
builder.AddListeningPort(serverAddress_, credentials, &port);
|
||||
|
||||
// Register "service_" as the instance through which we'll communicate with
|
||||
// clients. In this case it corresponds to an *asynchronous* service.
|
||||
builder.RegisterService(&service_);
|
||||
|
||||
// Get hold of the completion queue used for the asynchronous communication
|
||||
// with the gRPC runtime.
|
||||
cq_ = builder.AddCompletionQueue();
|
||||
|
||||
// Finally assemble the server.
|
||||
server_ = builder.BuildAndStart();
|
||||
serverPort_ = static_cast<std::uint16_t>(port);
|
||||
|
||||
if (serverPort_ != 0u)
|
||||
{
|
||||
JLOG(journal_.info()) << "gRPC server started successfully on port " << serverPort_;
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to start gRPC server at " << serverAddress_ << " (TLS mode: " << tlsMode
|
||||
<< "); Possible causes: address already in use, invalid address format, or permission "
|
||||
"denied"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return static_cast<bool>(serverPort_);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ private:
|
||||
|
||||
std::vector<boost::asio::ip::address> secureGatewayIPs_;
|
||||
|
||||
// TLS certificate paths
|
||||
std::optional<std::string> sslCertPath_;
|
||||
std::optional<std::string> sslKeyPath_;
|
||||
std::optional<std::string> sslCertChainPath_; // Intermediate CA certs for server cert chain
|
||||
std::optional<std::string>
|
||||
sslClientCAPath_; // CA cert for client certificate verification (mTLS)
|
||||
|
||||
beast::Journal journal_;
|
||||
|
||||
// typedef for function to bind a listener
|
||||
@@ -124,6 +131,10 @@ public:
|
||||
getEndpoint() const;
|
||||
|
||||
private:
|
||||
// Create server credentials (TLS or insecure) based on configuration
|
||||
std::shared_ptr<grpc::ServerCredentials>
|
||||
createServerCredentials();
|
||||
|
||||
// Class encompassing the state and logic needed to serve a request.
|
||||
template <class Request, class Response>
|
||||
class CallData : public Processor,
|
||||
|
||||
Reference in New Issue
Block a user