foundationdb/fdbrpc/tests/AuthzTlsTest.cpp

600 lines
20 KiB
C++

/*
* AuthzTlsTest.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _WIN32
#include <algorithm>
#include <array>
#include <cstring>
#include <ctime>
#include <iostream>
#include <string_view>
#include <thread>
#include <type_traits>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fmt/core.h>
#include "fdbrpc/fdbrpc.h"
#include "fdbrpc/FlowTransport.h"
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/MkCert.h"
#include "flow/ScopeExit.h"
#include "flow/TLSConfig.h"
using namespace std::literals::string_view_literals;
enum ExitCodes : int {
SUCCESS = 0,
MAIN_TEST_FAILED = 1,
CLIENT_PIPE_READ_ADDR_FAILED = 2,
CLIENT_FAILED = 3,
CLIENT_TEST_RESULT_MISMATCH = 4,
SERVER_BIND_ERROR = 5,
SERVER_STDOUT_REDIRECT_FAILED = 6,
WAITPID_ANY_STATUS = -1,
};
enum Role : uint8_t { MAIN, CLIENT, SERVER, UNDETERMINED, LAST };
constexpr std::array<std::string_view, Role::LAST> ROLE_STRING{ "MAIN"sv, "CLIENT"sv, "SERVER"sv, "UNDETERMINED"sv };
Role role = Role::MAIN;
template <>
struct fmt::formatter<Role> : fmt::formatter<std::string> {
auto format(Role role, fmt::format_context& ctx) const {
return fmt::format_to(ctx.out(), "{:^10}", ROLE_STRING[static_cast<int>(role)]);
}
};
template <class... Args>
void logRaw(const fmt::format_string<Args...>& fmt_str, Args&&... args) {
std::cout << fmt::format(fmt_str, std::forward<Args>(args)...);
std::cout.flush();
}
template <class... Args>
void log(const fmt::format_string<Args...>& fmt_str, Args&&... args) {
// NOTE: The fmt::formatter<Role> can do the padding, but not this fmt::format expression
std::cout << fmt::format("[{}] ", role);
logRaw(fmt_str, std::forward<Args>(args)...);
std::cout << std::endl;
}
enum ChainLength : int { NO_TLS = -1 };
template <>
struct fmt::formatter<ChainLength> : fmt::formatter<std::string> {
auto format(ChainLength value, fmt::format_context& ctx) const {
if (value == NO_TLS)
return fmt::format_to(ctx.out(), "NO_TLS");
else
return fmt::format_to(ctx.out(), "{}", static_cast<std::underlying_type_t<ChainLength>>(value));
}
};
template <>
struct fmt::formatter<std::vector<std::pair<ChainLength, ChainLength>>> : fmt::formatter<std::string> {
auto format(const std::vector<std::pair<ChainLength, ChainLength>>& entries, fmt::format_context& ctx) const {
fmt::format_to(ctx.out(), "[");
bool first = true;
for (const auto& entry : entries) {
fmt::format_to(ctx.out(), "{}{{ {}, {} }}", (first ? "" : ", "), entry.first, entry.second);
first = false;
}
return fmt::format_to(ctx.out(), "]");
}
};
std::string drainPipe(const int pipeFd) {
int readRc = 0;
std::string ret;
char buf[PIPE_BUF];
while ((readRc = ::read(pipeFd, buf, PIPE_BUF)) > 0) {
ret.append(buf, readRc);
}
if (readRc != 0) {
log("Unexpected error draining pipe: {}", strerror(errno));
throw std::runtime_error("pipe read error");
}
return ret;
}
struct TLSCreds {
bool noTls = false;
std::string certBytes;
std::string keyBytes;
std::string caBytes;
std::string password;
};
TLSCreds makeCreds(const ChainLength chainLen, const mkcert::ESide side, StringRef password = {}) {
if (chainLen == 0 || chainLen == NO_TLS) {
return TLSCreds{ chainLen == NO_TLS, "", "", "", "" };
}
auto arena = Arena();
auto ret = TLSCreds{};
if (!password.empty()) {
ret.password = password.toString();
auto certAndKeyPem = mkcert::makePasswCert(arena, password);
ret.certBytes = certAndKeyPem.certPem.toString();
ret.keyBytes = certAndKeyPem.privateKeyPem.toString();
ret.caBytes = ret.certBytes;
} else {
auto specs = mkcert::makeCertChainSpec(arena, std::labs(chainLen), side);
if (chainLen < 0) {
specs[0].offsetNotBefore = -60l * 60 * 24 * 365;
specs[0].offsetNotAfter = -10l; // cert that expired 10 seconds ago
}
auto chain = mkcert::makeCertChain(arena, specs, {} /* create root CA cert from spec*/);
if (chain.size() == 1) {
ret.certBytes = concatCertChain(arena, chain).toString();
} else {
auto nonRootChain = chain;
nonRootChain.pop_back();
ret.certBytes = concatCertChain(arena, nonRootChain).toString();
}
ret.caBytes = chain.back().certPem.toString();
ret.keyBytes = chain.front().privateKeyPem.toString();
}
return ret;
}
enum class Result : int { ERROR = 0, TRUSTED, UNTRUSTED, TIMEOUT, LAST };
constexpr std::array<std::string_view, static_cast<size_t>(Result::LAST)> RESULT_STRING{ "ERROR",
"TRUSTED",
"UNTRUSTED",
"TIMEOUT" };
template <>
struct fmt::formatter<Result> : fmt::formatter<std::string> {
auto format(const Result& r, fmt::format_context& ctx) const {
return fmt::format_to(ctx.out(), "{}", RESULT_STRING[static_cast<int>(r)]);
}
};
template <class T>
Future<T> stopNetworkAfter(Future<T> what, ExplicitVoid = {}) {
T t = co_await what;
g_network->stop();
co_return t;
}
// Reflective struct containing information about the requester from a server PoV
struct SessionInfo {
constexpr static FileIdentifier file_identifier = 1578312;
bool isPeerTrusted = false;
NetworkAddress peerAddress;
template <class Ar>
void serialize(Ar& ar) {
serializer(ar, isPeerTrusted, peerAddress);
}
};
struct SessionProbeRequest {
constexpr static FileIdentifier file_identifier = 1559713;
ReplyPromise<SessionInfo> reply{ PeerCompatibilityPolicy{ RequirePeer::AtLeast,
ProtocolVersion::withStableInterfaces() } };
bool verify() const { return true; }
template <class Ar>
void serialize(Ar& ar) {
serializer(ar, reply);
}
};
struct SessionProbeReceiver final : NetworkMessageReceiver {
SessionProbeReceiver() = default;
void receive(ArenaObjectReader& reader) override {
SessionProbeRequest req;
reader.deserialize(req);
SessionInfo res;
res.isPeerTrusted = FlowTransport::transport().currentDeliveryPeerIsTrusted();
res.peerAddress = FlowTransport::transport().currentDeliveryPeerAddress();
req.reply.send(res);
}
PeerCompatibilityPolicy peerCompatibilityPolicy() const override {
return PeerCompatibilityPolicy{ RequirePeer::AtLeast, ProtocolVersion::withStableInterfaces() };
}
bool isPublic() const override { return true; }
};
void runServer(const Endpoint& endpoint, int addrPipe, int completionPipe) {
auto realAddr = FlowTransport::transport().getLocalAddresses().address;
log("Listening at {}", realAddr.toString());
log("Endpoint token is {}", endpoint.token.toString());
static_assert(std::is_trivially_destructible_v<NetworkAddress>,
"NetworkAddress cannot be directly put on wire; need proper (de-)serialization");
// below writes/reads would block, but this is good enough for a test.
if (sizeof(realAddr) != ::write(addrPipe, &realAddr, sizeof(realAddr))) {
log("Failed to write server addr to pipe: {}", strerror(errno));
return;
}
if (sizeof(endpoint.token) != ::write(addrPipe, &endpoint.token, sizeof(endpoint.token))) {
log("Failed to write server endpoint to pipe: {}", strerror(errno));
return;
}
auto done = false;
if (sizeof(done) != ::read(completionPipe, &done, sizeof(done))) {
log("Failed to read completion flag from pipe: {}", strerror(errno));
return;
}
return;
}
Future<Void> waitAndPrintResponse(Future<SessionInfo> response, Result* rc) {
try {
SessionInfo info = co_await response;
log("Probe response: trusted={} peerAddress={}", info.isPeerTrusted, info.peerAddress.toString());
*rc = info.isPeerTrusted ? Result::TRUSTED : Result::UNTRUSTED;
} catch (Error& err) {
if (err.code() != error_code_operation_cancelled) {
log("Unexpected error: {}", err.what());
*rc = Result::ERROR;
} else {
log("Timed out");
*rc = Result::TIMEOUT;
}
}
}
// int runAsServer(TLSCreds creds, int addrPipe, int completionPipe, Result expect) {}
template <bool IsServer>
int runHost(TLSCreds creds, int addrPipe, int completionPipe, Result expect) {
auto tlsConfig = TLSConfig(IsServer ? TLSEndpointType::SERVER : TLSEndpointType::CLIENT);
bool const noTls = creds.noTls;
if (!noTls) {
tlsConfig.setCertificateBytes(creds.certBytes);
tlsConfig.setCABytes(creds.caBytes);
tlsConfig.setKeyBytes(creds.keyBytes);
tlsConfig.setPassword(creds.password);
}
g_network = newNet2(tlsConfig);
openTraceFile({}, 10 << 20, 10 << 20, ".", IsServer ? "authz_tls_unittest_server" : "authz_tls_unittest_client");
FlowTransport::createInstance(!IsServer, 1, WLTOKEN_RESERVED_COUNT);
auto& transport = FlowTransport::transport();
if constexpr (IsServer) {
auto addr = NetworkAddress::parse(noTls ? "127.0.0.1:0" : "127.0.0.1:0:tls");
auto endpoint = Endpoint();
auto receiver = SessionProbeReceiver();
try {
transport.bind(addr, addr);
} catch (const Error& err) {
log("CAUGHT Error in bind: code={} what={}", err.code(), err.what());
return SERVER_BIND_ERROR;
}
transport.addEndpoint(endpoint, &receiver, TaskPriority::ReadSocket);
auto thread = std::thread([]() {
g_network->run();
flushTraceFileVoid();
});
runServer(endpoint, addrPipe, completionPipe);
auto cleanupGuard = ScopeExit([&thread]() {
g_network->stop();
thread.join();
});
return SUCCESS;
} else {
auto dest = Endpoint();
auto& serverAddr = dest.addresses.address;
if (sizeof(serverAddr) != ::read(addrPipe, &serverAddr, sizeof(serverAddr))) {
log("Failed to read server addr from pipe: {}", strerror(errno));
return CLIENT_PIPE_READ_ADDR_FAILED;
}
if (noTls)
serverAddr.flags &= ~NetworkAddress::FLAG_TLS;
else
serverAddr.flags |= NetworkAddress::FLAG_TLS;
auto& token = dest.token;
if (sizeof(token) != ::read(addrPipe, &token, sizeof(token))) {
log("Failed to read server endpoint token from pipe: {}", strerror(errno));
return CLIENT_FAILED;
}
log("Server address is {}{}", serverAddr.toString(), noTls ? " (TLS suffix removed)" : "");
log("Server endpoint token is {}", token.toString());
auto sessionProbeReq = SessionProbeRequest{};
transport.sendUnreliable(SerializeSource(sessionProbeReq), dest, true /*openConnection*/);
log("Request is sent");
auto rc = SUCCESS;
auto result = Result::ERROR;
{
auto timeout = delay(expect == Result::TIMEOUT ? 0.5 : 5);
auto complete = waitAndPrintResponse(sessionProbeReq.reply.getFuture(), &result);
auto f = stopNetworkAfter(complete || timeout);
g_network->run();
}
auto done = true;
if (sizeof(done) != ::write(completionPipe, &done, sizeof(done))) {
log("Failed to signal server to terminate: {}", strerror(errno));
rc = CLIENT_FAILED;
}
if (rc == SUCCESS) {
if (expect != result) {
log("Test failed: expected {}, got {}", expect, result);
rc = CLIENT_TEST_RESULT_MISMATCH;
} else {
log("Response OK: got {} as expected", result);
}
}
return rc;
}
}
Result getExpectedResult(ChainLength serverChainLen, ChainLength clientChainLen) {
auto expect = Result::ERROR;
if (serverChainLen > 0) {
if (clientChainLen < 0) {
expect = Result::TIMEOUT;
} else if (clientChainLen > 0) {
expect = Result::TRUSTED;
} else if (clientChainLen == 0) {
expect = Result::UNTRUSTED;
}
} else if (serverChainLen == NO_TLS && clientChainLen == NO_TLS) {
expect = Result::TRUSTED;
} else {
expect = Result::TIMEOUT;
}
return expect;
}
std::pair<bool, std::string> waitPidStatusInterpreter(const char* procName, const int status) {
std::string prefix = fmt::format("{} subprocess ", procName);
std::string message;
if (WIFEXITED(status)) {
const auto exitStatus = WEXITSTATUS(status);
if (exitStatus == 0) {
return { true, fmt::format("{} waitpid() OK", prefix) };
}
message = fmt::format("{} exited with status {}", prefix, exitStatus);
} else if (WIFSIGNALED(status)) {
const auto signal = WTERMSIG(status);
message = fmt::format("{} killed by signal {} - {}", prefix, signal, strsignal(signal));
#ifdef WCOREDUMP
const auto coreDumped = WCOREDUMP(status);
if (coreDumped)
message.append(std::string_view(" (core dumped)"));
#endif // WCOREDUMP
} else if (WIFSTOPPED(status)) {
const auto signal = WSTOPSIG(status);
message = fmt::format("{} stopped by signal {} - {}", prefix, signal, strsignal(signal));
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wparentheses"
} else if (WIFCONTINUED(status)) {
#pragma clang diagnostic pop
message = fmt::format("{} continued by signal SIGCONT", prefix);
}
if (message.empty()) {
message = fmt::format("{} Unrecognized status {} (Check man 2 waitpid for more details)", prefix, status);
}
return { false, message };
}
bool waitPid(pid_t subProcPid, const char* procName, int expectStatus = WAITPID_ANY_STATUS) {
auto status = int{};
auto pid = ::waitpid(subProcPid, &status, 0);
if (pid < 0) {
log("{} subprocess waitpid() failed with {}", procName, strerror(errno));
return false;
} else {
auto [ok, message] = waitPidStatusInterpreter(procName, status);
log("{}", message);
return ok || (expectStatus != WAITPID_ANY_STATUS && WEXITSTATUS(status) == expectStatus);
}
}
int runTlsTest(ChainLength serverChainLen, ChainLength clientChainLen, std::string_view passwordTestCase = "") {
auto expect = Result::TRUSTED;
TLSCreds serverCreds;
TLSCreds clientCreds;
int expectStatusServer = WAITPID_ANY_STATUS;
int expectStatusClient = WAITPID_ANY_STATUS;
if (passwordTestCase.empty()) {
log("==== BEGIN TESTCASE ====");
expect = getExpectedResult(serverChainLen, clientChainLen);
log("Cert chain length: server={} client={}", serverChainLen, clientChainLen);
serverCreds = makeCreds(serverChainLen, mkcert::ESide::Server);
clientCreds = makeCreds(clientChainLen, mkcert::ESide::Client);
// make server and client trust each other
std::swap(serverCreds.caBytes, clientCreds.caBytes);
} else {
const auto password = "abc123"_sr;
serverCreds = makeCreds(serverChainLen, mkcert::ESide::Server, password);
clientCreds = serverCreds;
if (passwordTestCase == "client") {
log("==== BEGIN CLIENT BAD PASSWORD TESTCASE ====");
expect = Result::TIMEOUT;
clientCreds.password = "bad";
} else if (passwordTestCase == "server") {
log("==== BEGIN SERVER BAD PASSWORD TESTCASE ====");
serverCreds.password = "bad";
expectStatusServer = SERVER_BIND_ERROR;
expectStatusClient = CLIENT_PIPE_READ_ADDR_FAILED;
} else {
log("==== BEGIN PASSWORD PROTECTED TESTCASE ====");
}
}
auto clientPid = pid_t{};
auto serverPid = pid_t{};
int addrPipe[2], completionPipe[2], serverStdoutPipe[2], clientStdoutPipe[2];
if (::pipe(addrPipe) || ::pipe(completionPipe) || ::pipe(serverStdoutPipe) || ::pipe(clientStdoutPipe)) {
log("Pipe open failed: {}", strerror(errno));
return MAIN_TEST_FAILED;
}
auto ok = true;
{
serverPid = fork();
if (serverPid == -1) {
log("fork() for server subprocess failed: {}", strerror(errno));
return MAIN_TEST_FAILED;
} else if (serverPid == 0) {
role = Role::SERVER;
// server subprocess
::close(addrPipe[0]); // close address-in pipe (server writes its own address for client)
::close(
completionPipe[1]); // close completion-flag-out pipe (server awaits/reads completion flag from client)
::close(clientStdoutPipe[0]);
::close(clientStdoutPipe[1]);
::close(serverStdoutPipe[0]);
auto pipeCleanup = ScopeExit([&addrPipe, &completionPipe]() {
::close(addrPipe[1]);
::close(completionPipe[0]);
});
if (-1 == ::dup2(serverStdoutPipe[1], STDOUT_FILENO)) {
log("Failed to redirect server stdout to pipe: {}", strerror(errno));
::close(serverStdoutPipe[1]);
return SERVER_STDOUT_REDIRECT_FAILED;
}
_exit(runHost<true>(std::move(serverCreds), addrPipe[1], completionPipe[0], expect));
}
auto serverProcCleanup = ScopeExit([&ok, serverPid, expectStatusServer]() {
if (!waitPid(serverPid, "Server", expectStatusServer))
ok = false;
});
::close(addrPipe[1]);
::close(completionPipe[0]);
::close(serverStdoutPipe[1]);
clientPid = fork();
if (clientPid == -1) {
log("fork() for client subprocess failed: {}", strerror(errno));
return MAIN_TEST_FAILED;
} else if (clientPid == 0) {
role = Role::CLIENT;
::close(serverStdoutPipe[0]);
::close(clientStdoutPipe[0]);
auto pipeCleanup = ScopeExit([&addrPipe, &completionPipe]() {
::close(addrPipe[0]);
::close(completionPipe[1]);
});
if (-1 == ::dup2(clientStdoutPipe[1], STDOUT_FILENO)) {
log("Failed to redirect client stdout to pipe: {}", strerror(errno));
::close(clientStdoutPipe[1]);
return CLIENT_FAILED;
}
_exit(runHost<false>(std::move(clientCreds), addrPipe[0], completionPipe[1], expect));
}
auto clientProcCleanup = ScopeExit([&ok, clientPid, expectStatusClient]() {
if (!waitPid(clientPid, "Client", expectStatusClient))
ok = false;
});
}
// main process
::close(addrPipe[0]);
::close(completionPipe[1]);
::close(clientStdoutPipe[1]);
auto pipeCleanup = ScopeExit([&]() {
::close(serverStdoutPipe[0]);
::close(clientStdoutPipe[0]);
});
std::string const clientStdout = drainPipe(clientStdoutPipe[0]);
log("/// Begin Client STDOUT ///");
logRaw(fmt::runtime(clientStdout));
log("/// End Client STDOUT ///");
std::string const serverStdout = drainPipe(serverStdoutPipe[0]);
log("/// Begin Server STDOUT ///");
logRaw(fmt::runtime(serverStdout));
log("/// End Server STDOUT ///");
log(fmt::runtime(ok ? "OK" : "FAILED"));
return ok ? SUCCESS : MAIN_TEST_FAILED;
}
int main(int argc, char** argv) {
unsigned seed = std::time(nullptr);
if (argc > 1)
seed = std::stoul(argv[1]);
std::srand(seed);
log("Seed: {}", seed);
auto categoryToValue = [](int category) -> ChainLength {
if (category == 2 || category == -2) {
return static_cast<ChainLength>(category + std::rand() % 3);
} else {
return static_cast<ChainLength>(category);
}
};
std::vector<std::pair<ChainLength, ChainLength>> inputs;
std::vector<int> categories{ 0, NO_TLS, 1, -1, 2, -2 };
for (auto lhs : categories) {
for (auto rhs : categories) {
auto input = std::pair(categoryToValue(lhs), categoryToValue(rhs));
inputs.push_back(input);
}
}
std::vector<std::pair<ChainLength, ChainLength>> failed;
for (auto input : inputs) {
auto [serverChainLen, clientChainLen] = input;
if (runTlsTest(serverChainLen, clientChainLen))
failed.push_back({ serverChainLen, clientChainLen });
}
constexpr auto singleChainPair = std::pair(ChainLength(1), ChainLength(1));
inputs.insert(inputs.end(), 3, singleChainPair);
std::vector<std::string_view> failedPasswordTests;
for (const auto& testCase : std::array{ "no_bad_password", "client", "server" }) {
if (runTlsTest(singleChainPair.first, singleChainPair.second, testCase)) {
failed.push_back(singleChainPair);
failedPasswordTests.push_back(testCase);
}
}
if (!failed.empty()) {
if (!failedPasswordTests.empty()) {
for (const auto& test : failedPasswordTests) {
log(" {}, failed", test);
}
}
log("Test Failed: {}/{} cases: {}", failed.size(), inputs.size(), failed);
return MAIN_TEST_FAILED;
} else {
log("Test OK: {}/{} cases passed", inputs.size(), inputs.size());
return SUCCESS;
}
}
#else // _WIN32
#include <iostream>
int main() {
std::cerr << "TLS test is not supported in Windows" << std::endl;
return -1;
}
#endif // _WIN32