514 lines
16 KiB
C++
514 lines
16 KiB
C++
/*
|
|
* AuthzTlsTest.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2022 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 <cstring>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <fmt/format.h>
|
|
#include <limits>
|
|
#include <unistd.h>
|
|
#include <string_view>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <type_traits>
|
|
#include "flow/Arena.h"
|
|
#include "flow/Error.h"
|
|
#include "flow/MkCert.h"
|
|
#include "flow/ScopeExit.h"
|
|
#include "flow/TLSConfig.actor.h"
|
|
#include "fdbrpc/fdbrpc.h"
|
|
#include "fdbrpc/FlowTransport.h"
|
|
#include "flow/actorcompiler.h" // This must be the last #include.
|
|
|
|
std::FILE* outp = stdout;
|
|
|
|
enum ChainLength : int {
|
|
NO_TLS = std::numeric_limits<int>::min(),
|
|
};
|
|
|
|
template <class... Args>
|
|
void logRaw(Args&&... args) {
|
|
auto buf = fmt::memory_buffer{};
|
|
fmt::format_to(std::back_inserter(buf), std::forward<Args>(args)...);
|
|
fmt::print(outp, "{}", std::string_view(buf.data(), buf.size()));
|
|
}
|
|
|
|
template <class... Args>
|
|
void logWithPrefix(const char* prefix, Args&&... args) {
|
|
auto buf = fmt::memory_buffer{};
|
|
fmt::format_to(std::back_inserter(buf), std::forward<Args>(args)...);
|
|
fmt::print(outp, "{}{}\n", prefix, std::string_view(buf.data(), buf.size()));
|
|
}
|
|
|
|
template <class... Args>
|
|
void logc(Args&&... args) {
|
|
logWithPrefix("[CLIENT] ", std::forward<Args>(args)...);
|
|
}
|
|
|
|
template <class... Args>
|
|
void logs(Args&&... args) {
|
|
logWithPrefix("[SERVER] ", std::forward<Args>(args)...);
|
|
}
|
|
|
|
template <class... Args>
|
|
void logm(Args&&... args) {
|
|
logWithPrefix("[ MAIN ] ", std::forward<Args>(args)...);
|
|
}
|
|
|
|
std::string drainPipe(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) {
|
|
logm("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;
|
|
};
|
|
|
|
TLSCreds makeCreds(ChainLength chainLen, mkcert::ESide side) {
|
|
if (chainLen == 0 || chainLen == NO_TLS) {
|
|
return TLSCreds{ chainLen == NO_TLS, "", "", "" };
|
|
}
|
|
auto arena = Arena();
|
|
auto ret = TLSCreds{};
|
|
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,
|
|
};
|
|
|
|
template <>
|
|
struct fmt::formatter<Result> {
|
|
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { return ctx.begin(); }
|
|
|
|
template <class FormatContext>
|
|
auto format(const Result& r, FormatContext& ctx) -> decltype(ctx.out()) {
|
|
if (r == Result::TRUSTED)
|
|
return fmt::format_to(ctx.out(), "TRUSTED");
|
|
else if (r == Result::UNTRUSTED)
|
|
return fmt::format_to(ctx.out(), "UNTRUSTED");
|
|
else if (r == Result::TIMEOUT)
|
|
return fmt::format_to(ctx.out(), "TIMEOUT");
|
|
else
|
|
return fmt::format_to(ctx.out(), "ERROR");
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct fmt::formatter<ChainLength> {
|
|
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { return ctx.begin(); }
|
|
|
|
template <class FormatContext>
|
|
auto format(ChainLength value, FormatContext& ctx) -> decltype(ctx.out()) {
|
|
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>>> {
|
|
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { return ctx.begin(); }
|
|
|
|
template <class FormatContext>
|
|
auto format(const std::vector<std::pair<ChainLength, ChainLength>>& entries, FormatContext& ctx)
|
|
-> decltype(ctx.out()) {
|
|
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(), "]");
|
|
}
|
|
};
|
|
|
|
ACTOR template <class T>
|
|
Future<T> stopNetworkAfter(Future<T> what) {
|
|
T t = wait(what);
|
|
g_network->stop();
|
|
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() {}
|
|
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;
|
|
logs("Listening at {}", realAddr.toString());
|
|
logs("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))) {
|
|
logs("Failed to write server addr to pipe: {}", strerror(errno));
|
|
return;
|
|
}
|
|
if (sizeof(endpoint.token) != ::write(addrPipe, &endpoint.token, sizeof(endpoint.token))) {
|
|
logs("Failed to write server endpoint to pipe: {}", strerror(errno));
|
|
return;
|
|
}
|
|
auto done = false;
|
|
if (sizeof(done) != ::read(completionPipe, &done, sizeof(done))) {
|
|
logs("Failed to read completion flag from pipe: {}", strerror(errno));
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
ACTOR Future<Void> waitAndPrintResponse(Future<SessionInfo> response, Result* rc) {
|
|
try {
|
|
SessionInfo info = wait(response);
|
|
logc("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) {
|
|
logc("Unexpected error: {}", err.what());
|
|
*rc = Result::ERROR;
|
|
} else {
|
|
logc("Timed out");
|
|
*rc = Result::TIMEOUT;
|
|
}
|
|
}
|
|
return Void();
|
|
}
|
|
|
|
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);
|
|
}
|
|
g_network = newNet2(tlsConfig);
|
|
openTraceFile(NetworkAddress(),
|
|
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();
|
|
auto listenFuture = transport.bind(addr, addr);
|
|
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 0;
|
|
} else {
|
|
auto dest = Endpoint();
|
|
auto& serverAddr = dest.addresses.address;
|
|
if (sizeof(serverAddr) != ::read(addrPipe, &serverAddr, sizeof(serverAddr))) {
|
|
logc("Failed to read server addr from pipe: {}", strerror(errno));
|
|
return 1;
|
|
}
|
|
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))) {
|
|
logc("Failed to read server endpoint token from pipe: {}", strerror(errno));
|
|
return 2;
|
|
}
|
|
logc("Server address is {}{}", serverAddr.toString(), noTls ? " (TLS suffix removed)" : "");
|
|
logc("Server endpoint token is {}", token.toString());
|
|
auto sessionProbeReq = SessionProbeRequest{};
|
|
transport.sendUnreliable(SerializeSource(sessionProbeReq), dest, true /*openConnection*/);
|
|
logc("Request is sent");
|
|
auto rc = 0;
|
|
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))) {
|
|
logc("Failed to signal server to terminate: {}", strerror(errno));
|
|
rc = 4;
|
|
}
|
|
if (rc == 0) {
|
|
if (expect != result) {
|
|
logc("Test failed: expected {}, got {}", expect, result);
|
|
rc = 5;
|
|
} else {
|
|
logc("Response OK: got {} as expected", result);
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
Result getExpectedResult(ChainLength serverChainLen, ChainLength clientChainLen) {
|
|
auto expect = Result::ERROR;
|
|
if (serverChainLen > 0) {
|
|
if (clientChainLen == NO_TLS || 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;
|
|
}
|
|
|
|
bool waitPid(pid_t subProcPid, const char* procName) {
|
|
auto status = int{};
|
|
auto pid = ::waitpid(subProcPid, &status, 0);
|
|
if (pid < 0) {
|
|
logm("{} subprocess waitpid() failed with {}", procName, strerror(errno));
|
|
return false;
|
|
} else {
|
|
if (status != 0) {
|
|
logm("{} subprocess had error: rc={}", procName, status);
|
|
return false;
|
|
} else {
|
|
logm("{} subprocess waitpid() OK", procName);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
int runTlsTest(ChainLength serverChainLen, ChainLength clientChainLen) {
|
|
logm("==== BEGIN TESTCASE ====");
|
|
auto const expect = getExpectedResult(serverChainLen, clientChainLen);
|
|
using namespace std::literals::string_literals;
|
|
logm("Cert chain length: server={} client={}", serverChainLen, clientChainLen);
|
|
auto arena = Arena();
|
|
auto serverCreds = makeCreds(serverChainLen, mkcert::ESide::Server);
|
|
auto clientCreds = makeCreds(clientChainLen, mkcert::ESide::Client);
|
|
// make server and client trust each other
|
|
std::swap(serverCreds.caBytes, clientCreds.caBytes);
|
|
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)) {
|
|
logm("Pipe open failed: {}", strerror(errno));
|
|
return 1;
|
|
}
|
|
auto ok = true;
|
|
{
|
|
serverPid = fork();
|
|
if (serverPid == -1) {
|
|
logm("fork() for server subprocess failed: {}", strerror(errno));
|
|
return 1;
|
|
} else if (serverPid == 0) {
|
|
// 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)) {
|
|
logs("Failed to redirect server stdout to pipe: {}", strerror(errno));
|
|
::close(serverStdoutPipe[1]);
|
|
return 1;
|
|
}
|
|
_exit(runHost<true>(std::move(serverCreds), addrPipe[1], completionPipe[0], expect));
|
|
}
|
|
auto serverProcCleanup = ScopeExit([&ok, serverPid]() {
|
|
if (!waitPid(serverPid, "Server"))
|
|
ok = false;
|
|
});
|
|
clientPid = fork();
|
|
if (clientPid == -1) {
|
|
logm("fork() for client subprocess failed: {}", strerror(errno));
|
|
return 1;
|
|
} else if (clientPid == 0) {
|
|
::close(addrPipe[1]);
|
|
::close(completionPipe[0]);
|
|
::close(serverStdoutPipe[0]);
|
|
::close(serverStdoutPipe[1]);
|
|
::close(clientStdoutPipe[0]);
|
|
auto pipeCleanup = ScopeExit([&addrPipe, &completionPipe]() {
|
|
::close(addrPipe[0]);
|
|
::close(completionPipe[1]);
|
|
});
|
|
if (-1 == ::dup2(clientStdoutPipe[1], STDOUT_FILENO)) {
|
|
logs("Failed to redirect client stdout to pipe: {}", strerror(errno));
|
|
::close(clientStdoutPipe[1]);
|
|
return 1;
|
|
}
|
|
_exit(runHost<false>(std::move(clientCreds), addrPipe[0], completionPipe[1], expect));
|
|
}
|
|
auto clientProcCleanup = ScopeExit([&ok, clientPid]() {
|
|
if (!waitPid(clientPid, "Client"))
|
|
ok = false;
|
|
});
|
|
}
|
|
// main process
|
|
::close(addrPipe[0]);
|
|
::close(addrPipe[1]);
|
|
::close(completionPipe[0]);
|
|
::close(completionPipe[1]);
|
|
::close(serverStdoutPipe[1]);
|
|
::close(clientStdoutPipe[1]);
|
|
auto pipeCleanup = ScopeExit([&]() {
|
|
::close(serverStdoutPipe[0]);
|
|
::close(clientStdoutPipe[0]);
|
|
});
|
|
std::string const clientStdout = drainPipe(clientStdoutPipe[0]);
|
|
logm("/// Begin Client STDOUT ///");
|
|
logRaw(clientStdout);
|
|
logm("/// End Client STDOUT ///");
|
|
std::string const serverStdout = drainPipe(serverStdoutPipe[0]);
|
|
logm("/// Begin Server STDOUT ///");
|
|
logRaw(serverStdout);
|
|
logm("/// End Server STDOUT ///");
|
|
logm(ok ? "OK" : "FAILED");
|
|
return !ok;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
unsigned seed = std::time(nullptr);
|
|
if (argc > 1)
|
|
seed = std::stoul(argv[1]);
|
|
std::srand(seed);
|
|
logm("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 });
|
|
}
|
|
if (!failed.empty()) {
|
|
logm("Test Failed: {}/{} cases: {}", failed.size(), inputs.size(), failed);
|
|
return 1;
|
|
} else {
|
|
logm("Test OK: {}/{} cases passed", inputs.size(), inputs.size());
|
|
return 0;
|
|
}
|
|
}
|
|
#else // _WIN32
|
|
|
|
int main() {
|
|
return 0;
|
|
}
|
|
#endif // _WIN32
|