Compare commits

..

2 Commits

Author SHA1 Message Date
Ed Hennis
7c248f3fe6 Merge branch 'develop' into ximinez/loanpay-fee-overflow-develop 2026-04-20 11:38:49 -04:00
Ed Hennis
669617af99 Cap the base fee for LoanPay based on loanMaximumPaymentsPerTransaction 2026-04-17 19:11:18 -04:00
16 changed files with 26 additions and 1363 deletions

View File

@@ -1416,12 +1416,6 @@
# in this section to a comma-separated list of the addresses # in this section to a comma-separated list of the addresses
# of your Clio servers, in order to bypass xrpld's rate limiting. # 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 # This port is commented out but can be enabled by removing
# the '#' from each corresponding line including the entry under [server] # the '#' from each corresponding line including the entry under [server]
# #
@@ -1471,74 +1465,11 @@ admin = 127.0.0.1
protocol = ws protocol = ws
send_queue_limit = 500 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_grpc]
port = 50051 port = 50051
ip = 127.0.0.1 ip = 127.0.0.1
secure_gateway = 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_ws_public]
#port = 6005 #port = 6005
#ip = 127.0.0.1 #ip = 127.0.0.1

View File

@@ -11,8 +11,7 @@ namespace xrpl {
class ValidPermissionedDEX class ValidPermissionedDEX
{ {
bool regularOffers_ = false; bool regularOffers_ = false;
bool badHybridsOld_ = false; // pre-fixSecurity3_1_3: missing field/domain or size > 1 bool badHybrids_ = false;
bool badHybrids_ = false; // post-fixSecurity3_1_3: also catches size == 0 (size != 1)
hash_set<uint256> domains_; hash_set<uint256> domains_;
public: public:

View File

@@ -6,7 +6,6 @@
#include <xrpl/ledger/ReadView.h> #include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h> #include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/protocol/AccountID.h> #include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> #include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h> #include <xrpl/protocol/SField.h>
@@ -62,26 +61,10 @@ offerInDomain(
if (sleOffer->getFieldH256(sfDomainID) != domainID) if (sleOffer->getFieldH256(sfDomainID) != domainID)
return false; // LCOV_EXCL_LINE return false; // LCOV_EXCL_LINE
if (view.rules().enabled(fixSecurity3_1_3)) if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
{ {
// post-fixSecurity3_1_3: also catches empty sfAdditionalBooks (size == 0) JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
if (sleOffer->isFlag(lsfHybrid) && return false; // LCOV_EXCL_LINE
(!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); return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID);

View File

@@ -3,7 +3,6 @@
#include <xrpl/basics/Log.h> #include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h> #include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h> #include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> #include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h> #include <xrpl/protocol/SField.h>
@@ -41,17 +40,11 @@ ValidPermissionedDEX::visitEntry(
regularOffers_ = true; regularOffers_ = true;
} }
// pre-fixSecurity3_1_3: hybrid offer missing domain, missing // if a hybrid offer is missing domain or additional book, there's
// sfAdditionalBooks, or sfAdditionalBooks has more than one entry // something wrong
if (after->isFlag(lsfHybrid) && if (after->isFlag(lsfHybrid) &&
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) || (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
after->getFieldArray(sfAdditionalBooks).size() > 1)) 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; badHybrids_ = true;
} }
} }
@@ -70,8 +63,7 @@ ValidPermissionedDEX::finalize(
// For each offercreate transaction, check if // For each offercreate transaction, check if
// permissioned offers are valid // permissioned offers are valid
bool const isMalformed = view.rules().enabled(fixSecurity3_1_3) ? badHybrids_ : badHybridsOld_; if (txType == ttOFFER_CREATE && badHybrids_)
if (txType == ttOFFER_CREATE && isMalformed)
{ {
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
return false; return false;

View File

@@ -594,7 +594,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
auto const cancelSequence = ctx_.tx[~sfOfferSequence]; auto const cancelSequence = ctx_.tx[~sfOfferSequence];
// Note that we use the value from the sequence or ticket as the // Note that we we use the value from the sequence or ticket as the
// offer sequence. For more explanation see comments in SeqProxy.h. // offer sequence. For more explanation see comments in SeqProxy.h.
auto const offerSequence = ctx_.tx.getSeqValue(); auto const offerSequence = ctx_.tx.getSeqValue();

View File

@@ -439,7 +439,7 @@ EscrowCreate::doApply()
return tecDST_TAG_NEEDED; return tecDST_TAG_NEEDED;
} }
// Create escrow in ledger. Note that we use the value from the // Create escrow in ledger. Note that we we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h. // sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue()); Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(escrowKeylet); auto const slep = std::make_shared<SLE>(escrowKeylet);

View File

@@ -132,6 +132,9 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
auto const regularPayment = roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) + auto const regularPayment = roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) +
loanSle->at(sfLoanServiceFee); loanSle->at(sfLoanServiceFee);
if (view.rules().enabled(fixSecurity3_1_3) &&
amount >= regularPayment * loanMaximumPaymentsPerTransaction)
return loanMaximumPaymentsPerTransaction * normalCost;
// If making an overpayment, count it as a full payment because it will do // If making an overpayment, count it as a full payment because it will do
// about the same amount of work, if not more. // about the same amount of work, if not more.

View File

@@ -134,7 +134,7 @@ PaymentChannelCreate::doApply()
// Create PayChan in ledger. // Create PayChan in ledger.
// //
// Note that we use the value from the sequence or ticket as the // Note that we we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h. // payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue()); Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet); auto const slep = std::make_shared<SLE>(payChanKeylet);

View File

@@ -1,851 +0,0 @@
#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"
"MIIFhjCCA26gAwIBAgIJAL9P70zX30oiMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV\n"
"BAYTAlVTMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9S\n"
"aXBwbGVkIFRlc3QgQ0ExEDAOBgNVBAMMB1Rlc3QgQ0EwIBcNMjYwNDA5MTMyNTA2\n"
"WhgPMjEyNjAzMTYxMzI1MDZaMFcxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0\n"
"MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9SaXBwbGVkIFRlc3QgQ0ExEDAOBgNV\n"
"BAMMB1Rlc3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzOJ5s\n"
"dy1O0GN/kmbeWf5DmFbQBSS9FRKxh6/o9V9BqRBQfECrVK9T5Y4FYrGGtmUW3YEV\n"
"uMDZ5q6rvBT2zrrzPXnWA5Pb4I4mKqC/yk5L7Mm8A9xQsNoRzgTyl/NuHiXKn+yQ\n"
"FuidA6U36qwIAcDR7gLqrJ1ud/ng9f9Q4k6+IItY/XGhcz4nKlQq9jpzmfdSlBkU\n"
"hXsIsdNtC+UGlQMCMX2jwysIFfjjCOMlH7KFQ3dKodhsW+Ym6AsPwyRGCgNXO/zd\n"
"Fqt1MIMs1F7r40DtfVO3R7w4/2SblcceZlsDrYQUbJnH+sEPWO0SGGo6Y7Ohs09+\n"
"aJSOAGGQVgTSLuAcFtR4BXD0GLn39+10PDvHGOsMJKL1de1f96s8kPlifQ5AGWuc\n"
"xy6XsupGSa0F8LozwQmKD7hVkyladUTWFPknz5tsPEVApTO0U8Vuknuhyovo6+mx\n"
"qBoSD32NwHveFz3jWqfj0CGX9BwL9AOpMabDhROVQfyM5GrLeLOOdgOnsBXJYYdW\n"
"MeJwz6BH30q9yvEd9Ti26jSk3fM8WPuEkZzNNp8STEMyDrfhaKOe5fGPWLnqMQAf\n"
"yMCDLwB1WqIN1Q6gOELb3rxyYDVH/5x6/JXosdUe1qx/tzvRoSWxxssRRd2Em+e+\n"
"MUFLXz+9D6kZ9XCuP/mLyRGW6LEiwwQkGKMnzwIDAQABo1MwUTAPBgNVHRMBAf8E\n"
"BTADAQH/MB0GA1UdDgQWBBQPK5hXxLdTj3QqfVzGpfTga6IF3zAfBgNVHSMEGDAW\n"
"gBQPK5hXxLdTj3QqfVzGpfTga6IF3zANBgkqhkiG9w0BAQsFAAOCAgEAa06whkqv\n"
"KmdT1HVhkV7AkWEAeHMWPLLaaFbcwble7a1Vizh6GjCyNpLtoN+mtwqwiOdsIlRE\n"
"42pWILc6CuuX0ae0nHSrcQS5mq8ZKSMr1xTo9RSfBq7CDfdyquxzG83HhpdApViZ\n"
"87Bjy3WoRuomM+YiONfUVdCbC5ZmXW/z+xrXJ+JqIXrtv66sZxpQIR0+ShnWT0DE\n"
"w9jB5fxjydPFwEudYi4z9XjEZaZJ1f8VNWDuUvi3yTJtTlNaWnKveudtDZBw/fA+\n"
"MBFd9ccYVhGQPxOs6S0Ev6q5IjcnzGeEBNZOjgjQk9aFrAs2Iiy018AbYQj5XD64\n"
"hHyiNgyPjl/VgXJE1Xl3lXGpiiJlXctgnCd3UGMfKznhBIpDT13i2CmHFyR3uk7o\n"
"UOZUXCnbnmgthejmFxB35Wf5TmGaYubtRMfCPHGNbQD+7Kg2+8eel3J3JSuG6RQ8\n"
"hwNyHHQnaPVUSANItJ4cMe5DutM0vUCMkJbajL+fjC5SdsTcGfR2VmAFqulNDXjH\n"
"sGWBiWVNsgddax63m6kL9UOeE+8pu8yStKZ4mVn2EjE9eJk4vyZt4BaI6sDUMlke\n"
"S9OjcI5iYlxXNgbRQBtwK70+c3D3JoRPREkTRPPwC4NiAFed7UwXSMh5nWbpt/dq\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"
"MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDPQHttw3TLjOqY\n"
"S3VkLF3KMRaP2ZtO6A1mXfTbqbKvD41Fazf/cM/v9lPMAlRd2SEY3MeE8KVddKJw\n"
"sbF0kNgDkKB5D5V42WrTw5biFNMOeHAZMR/oWChIvZbbGbDxIdIO2+W3X0kjpa2e\n"
"KcK9qBk8xoyIeilyXtleGWuWvHxiZP9iGHxaTWB+wIKUIK6vrEObiO3P/9XPpHzs\n"
"Bt0HdTDh4V7fwnr2UndVeQyBwUwLn6pd73sTKBfA26YppRwDjPIj6NYtF3I28lRC\n"
"Uo+47TAVZM97gjN9oEwyHIVtOc7fnZPwtN26M5v5083SGXU1k/PN3xGAlDUiCF3R\n"
"SMbBRylGgUVtsAD57i8tI2SqCr+ZG233VFiOdwTRKKVNTyMC9TCJdCFFEDFDHTTS\n"
"imKRQKy0Cm2qoL6JBkziAIiu/0Zzv9YjAAnRoJ2cweMXQ/9z1oWeEUZBLRsggYQ8\n"
"FbM13FOkOs8IlzacSuhwrYKOq8LsMX4cH2mnn783FtXXqrL/xfL711KhzGpZNrz1\n"
"87ilJ+ZsmP9D6vCBP/tR7V52dgtB6I291o8zxdH8GheIGenEFaZaoAwyN2FuJgXZ\n"
"qx9319I9gYerZ/BbUzA2MuOxFd0ywtdcTPqKiyAQ9rxQVCVQyYWjkfBEYRzWxjfj\n"
"3XhNprxdm3cauz01NAoTDiz52dZhGQIDAQABAoICADTppZmUeVEunQZc3Y/BtABX\n"
"IAeB6yDuJd2ox0b9wFzpf4vln9pblvsQzLwdLCT5tnV+iIHsXovJp19WPpQgFsZy\n"
"OkYuMF82Qwvlt7Po1Smwng4QeLD9MOvBW658lKw7kkGw6qkybp3nQrhKuSlqrWbS\n"
"2jZN2h8VEDHyE4HchXUpi/ojfjwf3S7/P1dKMM8xD+G5x91+17u3px0rc2rgBKbm\n"
"vy4pnPMegtETopnOG/grv3dUGPv/FHFsorOnL8vIRFnerC+++K4GmHSGV6NDCy+r\n"
"GT3TNAoyzsFMftQwGh0FQiwGQUW0v3G9HaMyVLZlG63H8dP+AsK5mBpCllvqKyMb\n"
"TQcS8mTBYAvBgiKqZBy6cwbnLaN5hYftDTg4zS62LVZzNlaMeTFGGYINDrth2S6X\n"
"+qH2GcXAUqd8aYIz/BLimCGhZMFQ0hAFCcq72Lh8UJsvvf9ng8Di/6oiZFJeN4nf\n"
"/LHUjlOyBqj8prTh0UCBjM0hdzzs96K+e3eBGjFHdVrdK5QytKZh1KTBSu0t64b2\n"
"0MSW5+2vFbdaQT5jed2lyh9YMmtGJV07T+5LKjWQGcJcc53DLA6uQ8lQuckQReE4\n"
"VzTWoG0eKEvk8ahltbl+0Gk7+fQlsMD5VizbET7EDOoiFPT3SpA/5dybXglNSuH4\n"
"9T65s7Xj2/zD8khLb3CxAoIBAQDwV3OQ66kqIC554Emz7F9ZNInMx4Vjuatd4wxe\n"
"WMT1Vlyg0ZeNSgdfggPmfntDW56NZ/h7q9F9feGfF3OogfZXVv2NzsynAOS4DR1c\n"
"0JR8/y7NG8vxHmDkNVJ3YkHfNYqK3x+sMCoXF0jDdaILXaP0nzAdcnLrRLyU9F3r\n"
"RVJpyaMWt9mtnRzlf1PTlc9WQ99MYuMfqxFj/zBFddnNFiI0FaG3/3Xdg6EH9x42\n"
"/2GXT/TlSUQo4e6Dh9mGhupUYzJt+AqjCnFA2n+D68QIdVq8ykOqGvnpwmfF94qt\n"
"8xfrKhI4zskj4N0X/xwByfEBOkU8nI8zP8PdVqKCbCRG1Z93AoIBAQDcwSRpPD5J\n"
"dmfXY2MGHvGQiJme/3YGPhcA15fQVRzWuZtn1PHULlI2V62NintzTUhjmv6SkGyX\n"
"6ze4RSCxrRFJumJwev5HtohQ7DH/nDtg+Y9Ewn32ehSEotycz1HUskOtgtLOQjwY\n"
"m66gTx6OzG4T6G2YRHcK8hFk9eLR0t2fIqPtu6APfRuo5OowiuYVzRKOplzh2J05\n"
"Q1TCJ4QL6geJQ/MzxVx33yopXWfRxZekG7ri4OJTIv8zj1Ocrytgz4hxAc8xJEf5\n"
"Z50k4JaWGBy+O/mKZ9sOGsolNv/FMUauE2EjSeNWNgvCFFvh4hDUciIakPzEeslp\n"
"hZdZCG9IV8fvAoIBAQDoDbfSbAc4Wjwlhq4C362sJrMKGnarNADGtMsjaRg6PTlQ\n"
"OS3XyGtYBuOXL/X5skNjCsj7N4kcXmdywST1xQ3BhIdp3QryEEXFgzwfenB0Q7q/\n"
"ZSBDXW51yRonlKI/TqXGseoVyadKBjxGJJTh3nbIYM8HD5Lvn71pIIxx9cu9wmcK\n"
"L1cobvMQjyCzwQigpQW77hqXYAd5glHsLv6tKrq5iU1Mp4X46/eWBj6RIYDrpNKy\n"
"c0wxIPu22XrojelAs0pkrUIv64wv7weBqyjqdcy3TZ+JZWR5FDA4D2tByt4EO+m+\n"
"GcJRNvKiEbnL7FwbMFTbUdpdxCpr0hM0VA+uqOG/AoIBAB24JuXABYawWSSHLdKq\n"
"Ic1ahowASmxmuYQUgky62KoTzNc6tN/i6JCGV0gh56LLOb6nJDSpGuWM9jBpphAl\n"
"g5lQbWZFOKyA53M1iTmnV9sjXeVc5cZkAxUkM90skBC5eyEF5sl740lQ1D6iyDNj\n"
"VEJ73R1NwlUH582WyNWEtO9yo20jAFZ1el7PirPET1uKA0CPJxwEpI4MAYIt/bn4\n"
"5NDXBAvpOxysP6nX+F0mY9blINDgg7e7k23mktQaRRXAetbz7mfoQYRTLbXEQqGs\n"
"V1pJCrxWZQhOFP7Tm7V5f9F5rG8qyF9X4VdclE4huDBRuUOoV09AVJNPN+P1nb24\n"
"i6MCggEBAIHUb8G0QKM4LPfdUmv575YmbnYY+Y3O982+jjRg4uAkYHnEkNfL6FKE\n"
"6ot7vcwDTN2Ccw6UKZU8GvyAQOGotmj6Nkgny2wFnEfoTzJaENjhPlnCHD9LDCps\n"
"w/tuoCHOUyyEb/Ygc+4xTsc0W3y2dbaYcg1qvLeIFuVZBNvY1XNlVf40/sVoiyet\n"
"Abh2yPwqOgOu8FpK4gcM8iSwL/xhEJJgT2wE+1MyHOd8KKklFHR7dF2WX1dF0Sif\n"
"cerPwqKXCvWh7og0RIJXe24fymMxtIsURBer9a3bPzUPVQoOXki4/u/kdEGH66GH\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

View File

@@ -1774,10 +1774,8 @@ class Invariants_test : public beast::unit_test::suite
using namespace test::jtx; using namespace test::jtx;
bool const fixPDEnabled = features[fixPermissionedDomainInvariant]; bool const fixPDEnabled = features[fixPermissionedDomainInvariant];
bool const fixS313Enabled = features[fixSecurity3_1_3];
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fixPD" : "") + testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fix" : "");
std::string(fixS313Enabled ? " fixS313" : "");
doInvariantCheck( doInvariantCheck(
Env(*this, features), Env(*this, features),
@@ -1865,45 +1863,6 @@ class Invariants_test : public beast::unit_test::suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED}); {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 // hybrid offer missing sfAdditionalBooks
{ {
Env env1(*this, features); Env env1(*this, features);
@@ -4102,10 +4061,6 @@ public:
testPermissionedDomainInvariants(defaultAmendments() - fixPermissionedDomainInvariant); testPermissionedDomainInvariants(defaultAmendments() - fixPermissionedDomainInvariant);
testPermissionedDEX(defaultAmendments() | fixPermissionedDomainInvariant); testPermissionedDEX(defaultAmendments() | fixPermissionedDomainInvariant);
testPermissionedDEX(defaultAmendments() - fixPermissionedDomainInvariant); testPermissionedDEX(defaultAmendments() - fixPermissionedDomainInvariant);
testPermissionedDEX(
(defaultAmendments() | fixPermissionedDomainInvariant) - fixSecurity3_1_3);
testPermissionedDEX(
defaultAmendments() - fixPermissionedDomainInvariant - fixSecurity3_1_3);
testNoModifiedUnmodifiableFields(); testNoModifiedUnmodifiableFields();
testValidPseudoAccounts(); testValidPseudoAccounts();
testValidLoanBroker(); testValidLoanBroker();

View File

@@ -20,8 +20,6 @@
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.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/Book.h>
#include <xrpl/protocol/Feature.h> #include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
@@ -30,8 +28,6 @@
#include <xrpl/protocol/LedgerFormats.h> #include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h> #include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h> #include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h> #include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h> #include <xrpl/protocol/TxFlags.h>
@@ -39,7 +35,6 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -1390,73 +1385,6 @@ class PermissionedDEX_test : public beast::unit_test::suite
BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); 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: public:
void void
run() override run() override
@@ -1478,8 +1406,6 @@ public:
testHybridBookStep(all); testHybridBookStep(all);
testHybridInvalidOffer(all); testHybridInvalidOffer(all);
testHybridOfferDirectories(all); testHybridOfferDirectories(all);
testHybridMalformedOffer(all);
testHybridMalformedOffer(all - fixSecurity3_1_3);
} }
}; };

View File

@@ -107,52 +107,6 @@ std::unique_ptr<Config> addGrpcConfig(std::unique_ptr<Config>);
std::unique_ptr<Config> std::unique_ptr<Config>
addGrpcConfigWithSecureGateway(std::unique_ptr<Config>, std::string const& secureGateway); 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> std::unique_ptr<Config>
makeConfig( makeConfig(
std::map<std::string, std::string> extraTxQ = {}, std::map<std::string, std::string> extraTxQ = {},

View File

@@ -48,7 +48,7 @@ class JSONRPCClient : public AbstractClient
continue; continue;
ParsedPort pp; ParsedPort pp;
parse_Port(pp, cfg[name], log); parse_Port(pp, cfg[name], log);
if (not pp.protocol.contains("http")) if (pp.protocol.count("http") == 0)
continue; continue;
using namespace boost::asio::ip; using namespace boost::asio::ip;
if (pp.ip && pp.ip->is_unspecified()) if (pp.ip && pp.ip->is_unspecified())
@@ -91,6 +91,12 @@ public:
stream_.connect(ep_); 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: Return value is an Object type with up to three keys:
status status

View File

@@ -132,49 +132,6 @@ addGrpcConfigWithSecureGateway(std::unique_ptr<Config> cfg, std::string const& s
return cfg; 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> std::unique_ptr<Config>
makeConfig( makeConfig(
std::map<std::string, std::string> extraTxQ, std::map<std::string, std::string> extraTxQ,

View File

@@ -8,7 +8,6 @@
#include <xrpld/rpc/detail/Handler.h> #include <xrpld/rpc/detail/Handler.h>
#include <xrpl/basics/BasicConfig.h> #include <xrpl/basics/BasicConfig.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/Log.h> #include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h> #include <xrpl/basics/contract.h>
#include <xrpl/beast/core/CurrentThreadName.h> #include <xrpl/beast/core/CurrentThreadName.h>
@@ -28,7 +27,6 @@
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/icl/interval_set.hpp> #include <boost/icl/interval_set.hpp>
#include <grpc/grpc_security_constants.h>
#include <grpcpp/completion_queue.h> #include <grpcpp/completion_queue.h>
#include <grpcpp/security/server_credentials.h> #include <grpcpp/security/server_credentials.h>
#include <grpcpp/server_builder.h> #include <grpcpp/server_builder.h>
@@ -389,48 +387,6 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
Throw<std::runtime_error>("Error parsing secure_gateway section"); 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");
}
} }
} }
@@ -602,104 +558,6 @@ GRPCServerImpl::setupListeners()
return requests; 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 bool
GRPCServerImpl::start() GRPCServerImpl::start()
{ {
@@ -707,63 +565,24 @@ GRPCServerImpl::start()
if (serverAddress_.empty()) if (serverAddress_.empty())
return false; return false;
// Determine TLS mode for logging JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_;
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; grpc::ServerBuilder builder;
// Listen on the given address without any authentication mechanism.
// Actually binded port will be returned into "port" variable.
int port = 0; 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 // Register "service_" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *asynchronous* service. // clients. In this case it corresponds to an *asynchronous* service.
builder.RegisterService(&service_); builder.RegisterService(&service_);
// Get hold of the completion queue used for the asynchronous communication // Get hold of the completion queue used for the asynchronous communication
// with the gRPC runtime. // with the gRPC runtime.
cq_ = builder.AddCompletionQueue(); cq_ = builder.AddCompletionQueue();
// Finally assemble the server. // Finally assemble the server.
server_ = builder.BuildAndStart(); server_ = builder.BuildAndStart();
serverPort_ = static_cast<std::uint16_t>(port); 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_); return static_cast<bool>(serverPort_);
} }

View File

@@ -66,13 +66,6 @@ private:
std::vector<boost::asio::ip::address> secureGatewayIPs_; 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_; beast::Journal journal_;
// typedef for function to bind a listener // typedef for function to bind a listener
@@ -131,10 +124,6 @@ public:
getEndpoint() const; getEndpoint() const;
private: 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. // Class encompassing the state and logic needed to serve a request.
template <class Request, class Response> template <class Request, class Response>
class CallData : public Processor, class CallData : public Processor,