foundationdb/flow/MkCertCli.cpp

341 lines
14 KiB
C++

/*
* MkCertCli.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.
*/
#include <cstdlib>
#include <fstream>
#include <string>
#include <string_view>
#include <thread>
#include <fmt/format.h>
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/MkCert.h"
#include "flow/network.h"
#include "flow/Platform.h"
#include "flow/ScopeExit.h"
#include "SimpleOpt/SimpleOpt.h"
#include "flow/TLSConfig.actor.h"
#include "flow/Trace.h"
enum EMkCertOpt : int {
OPT_HELP,
OPT_SERVER_CHAIN_LEN,
OPT_CLIENT_CHAIN_LEN,
OPT_SERVER_CERT_FILE,
OPT_SERVER_KEY_FILE,
OPT_SERVER_CA_FILE,
OPT_CLIENT_CERT_FILE,
OPT_CLIENT_KEY_FILE,
OPT_CLIENT_CA_FILE,
OPT_EXPIRE_SERVER_CERT,
OPT_EXPIRE_CLIENT_CERT,
OPT_PRINT_SERVER_CERT,
OPT_PRINT_CLIENT_CERT,
OPT_PRINT_ARGUMENTS,
OPT_ENABLE_TRACE,
OPT_NO_SHARED_SERVER_CLIENT_CA,
};
CSimpleOpt::SOption gOptions[] = { { OPT_HELP, "--help", SO_NONE },
{ OPT_HELP, "-h", SO_NONE },
{ OPT_SERVER_CHAIN_LEN, "--server-chain-length", SO_REQ_SEP },
{ OPT_SERVER_CHAIN_LEN, "-S", SO_REQ_SEP },
{ OPT_CLIENT_CHAIN_LEN, "--client-chain-length", SO_REQ_SEP },
{ OPT_CLIENT_CHAIN_LEN, "-C", SO_REQ_SEP },
{ OPT_SERVER_CERT_FILE, "--server-cert-file", SO_REQ_SEP },
{ OPT_SERVER_KEY_FILE, "--server-key-file", SO_REQ_SEP },
{ OPT_SERVER_CA_FILE, "--server-ca-file", SO_REQ_SEP },
{ OPT_CLIENT_CERT_FILE, "--client-cert-file", SO_REQ_SEP },
{ OPT_CLIENT_KEY_FILE, "--client-key-file", SO_REQ_SEP },
{ OPT_CLIENT_CA_FILE, "--client-ca-file", SO_REQ_SEP },
{ OPT_EXPIRE_SERVER_CERT, "--expire-server-cert", SO_NONE },
{ OPT_EXPIRE_CLIENT_CERT, "--expire-client-cert", SO_NONE },
{ OPT_PRINT_SERVER_CERT, "--print-server-cert", SO_NONE },
{ OPT_PRINT_CLIENT_CERT, "--print-client-cert", SO_NONE },
{ OPT_PRINT_ARGUMENTS, "--print-args", SO_NONE },
{ OPT_ENABLE_TRACE, "--trace", SO_NONE },
{ OPT_NO_SHARED_SERVER_CLIENT_CA, "--no-shared-server-client-ca", SO_NONE },
SO_END_OF_OPTIONS };
template <size_t Len>
void printOptionUsage(std::string_view option, const char* (&&optionDescLines)[Len]) {
constexpr std::string_view optionIndent{ " " };
constexpr std::string_view descIndent{ " " };
fmt::print(stdout, "{}{}\n", optionIndent, option);
for (auto descLine : optionDescLines)
fmt::print(stdout, "{}{}\n", descIndent, descLine);
fmt::print("\n");
}
void printUsage(std::string_view binary) {
fmt::print(stdout,
"mkcert: FDB test certificate chain generator\n\n"
"Usage: {} [OPTIONS...]\n\n",
binary);
printOptionUsage("--server-chain-length LENGTH, -S LENGTH (default: 3)",
{ "Length of server certificate chain including root CA certificate." });
printOptionUsage("--client-chain-length LENGTH, -C LENGTH (default: 2)",
{ "Length of client certificate chain including root CA certificate.",
"Use zero-length to test to setup untrusted clients." });
printOptionUsage("--server-cert-file PATH (default: 'server_cert.pem')",
{ "Output filename for server certificate chain excluding root CA.",
"Intended for SERVERS to use as 'tls_certificate_file'.",
"Certificates are concatenated in leaf-to-CA order." });
printOptionUsage("--server-key-file PATH (default: 'server_key.pem')",
{ "Output filename for server private key matching its leaf certificate.",
"Intended for SERVERS to use as 'tls_key_file'" });
printOptionUsage("--server-ca-file PATH (default: 'server_ca.pem')",
{ "Output filename for server's root CA certificate.",
"Content same as '--server-cert-file' for '--server-chain-length' == 1.",
"Intended for CLIENTS to use as 'tls_ca_file': i.e. cert issuer to trust." });
printOptionUsage("--client-cert-file PATH (default: 'client_cert.pem')",
{ "Output filename for client certificate chain excluding root CA.",
"Intended for CLIENTS to use as 'tls_certificate_file'.",
"Certificates are concatenated in leaf-to-CA order." });
printOptionUsage("--client-key-file PATH (default: 'client_key.pem')",
{ "Output filename for client private key matching its leaf certificate.",
"Intended for CLIENTS to use as 'tls_key_file'" });
printOptionUsage("--client-ca-file PATH (default: 'client_ca.pem')",
{ "Output filename for client's root CA certificate.",
"Content same as '--client-cert-file' for '--client-chain-length' == 1.",
"Intended for SERVERS to use as 'tls_ca_file': i.e. cert issuer to trust." });
printOptionUsage("--expire-server-cert (default: no)",
{ "Deliberately expire server's leaf certificate for testing." });
printOptionUsage("--expire-client-cert (default: no)",
{ "Deliberately expire client's leaf certificate for testing." });
printOptionUsage("--print-server-cert (default: no)",
{ "Print generated server certificate chain including root in human readable form.",
"Printed certificates are in leaf-to-CA order.",
"If --print-client-cert is also used, server chain precedes client's." });
printOptionUsage("--print-client-cert (default: no)",
{ "Print generated client certificate chain including root in human readable form.",
"Printed certificates are in leaf-to-CA order.",
"If --print-server-cert is also used, server chain precedes client's." });
printOptionUsage("--print-args (default: no)", { "Print chain generation arguments." });
printOptionUsage("--no-shared-server-client-ca (default: no)",
{ "DISABLE the use of common root CA for client and server certificates." });
}
struct ChainSpec {
unsigned length;
std::string certFile;
std::string keyFile;
std::string caFile;
mkcert::ESide side;
bool expireLeaf;
void transformPathToAbs() {
certFile = abspath(certFile);
keyFile = abspath(keyFile);
caFile = abspath(caFile);
}
void print() {
fmt::print(stdout, "{}-side:\n", side == mkcert::ESide::Server ? "Server" : "Client");
fmt::print(stdout, " Chain length: {}\n", length);
fmt::print(stdout, " Certificate file: {}\n", certFile);
fmt::print(stdout, " Key file: {}\n", keyFile);
fmt::print(stdout, " CA file: {}\n", caFile);
fmt::print(stdout, " Expire cert: {}\n", expireLeaf);
}
mkcert::CertChainRef makeChain(Arena& arena, Optional<mkcert::CertAndKeyRef> rootCa = {});
};
mkcert::CertChainRef ChainSpec::makeChain(Arena& arena, Optional<mkcert::CertAndKeyRef> rootCa) {
auto checkStream = [](std::ofstream& fs, std::string_view filename) {
if (!fs) {
throw std::runtime_error(fmt::format("cannot open '{}' for writing", filename));
}
};
auto ofsCert = std::ofstream(certFile, std::ofstream::out | std::ofstream::trunc);
checkStream(ofsCert, certFile);
auto ofsKey = std::ofstream(keyFile, std::ofstream::out | std::ofstream::trunc);
checkStream(ofsKey, keyFile);
auto ofsCa = std::ofstream(caFile, std::ofstream::out | std::ofstream::trunc);
checkStream(ofsCa, caFile);
if (!length)
return {};
auto specs = mkcert::makeCertChainSpec(arena, length, side);
if (expireLeaf) {
specs[0].offsetNotBefore = -60l * 60 * 24 * 365;
specs[0].offsetNotAfter = -10l;
}
auto chain = mkcert::makeCertChain(arena, specs, rootCa);
auto ca = chain.back().certPem;
ofsCa.write(reinterpret_cast<char const*>(ca.begin()), ca.size());
auto chainMinusRoot = chain;
if (chainMinusRoot.size() > 1)
chainMinusRoot.pop_back();
auto cert = mkcert::concatCertChain(arena, chainMinusRoot);
ofsCert.write(reinterpret_cast<char const*>(cert.begin()), cert.size());
auto key = chain[0].privateKeyPem;
ofsKey.write(reinterpret_cast<char const*>(key.begin()), key.size());
return chain;
}
int main(int argc, char** argv) {
// default chain specs
auto serverArgs = ChainSpec{ 3u /*length*/, "server_cert.pem", "server_key.pem",
"server_ca.pem", mkcert::ESide::Server, false /* expireLeaf */ };
auto clientArgs = ChainSpec{ 2u /*length*/, "client_cert.pem", "client_key.pem",
"client_ca.pem", mkcert::ESide::Client, false /* expireLeaf */ };
auto printServerCert = false;
auto printClientCert = false;
auto printArguments = false;
auto enableTrace = false;
auto noSharedServerClientCa = false;
auto args = CSimpleOpt(argc, argv, gOptions, SO_O_EXACT | SO_O_HYPHEN_TO_UNDERSCORE);
while (args.Next()) {
if (auto err = args.LastError()) {
switch (err) {
case SO_ARG_INVALID_DATA:
fmt::print(stderr, "ERROR: invalid argument to option '{}'\n", args.OptionText());
return FDB_EXIT_ERROR;
case SO_ARG_INVALID:
fmt::print(stderr, "ERROR: argument given to no-argument option '{}'\n", args.OptionText());
return FDB_EXIT_ERROR;
case SO_ARG_MISSING:
fmt::print(stderr, "ERROR: argument missing for option '{}'\n", args.OptionText());
return FDB_EXIT_ERROR;
case SO_OPT_INVALID:
fmt::print(stderr, "ERROR: unknown option '{}'\n", args.OptionText());
return FDB_EXIT_ERROR;
default:
fmt::print(stderr, "ERROR: unknown error {} with option '{}'\n", err, args.OptionText());
return FDB_EXIT_ERROR;
}
} else {
auto const optId = args.OptionId();
switch (optId) {
case OPT_HELP:
printUsage(argv[0]);
return FDB_EXIT_SUCCESS;
case OPT_SERVER_CHAIN_LEN:
try {
serverArgs.length = std::stoul(args.OptionArg());
assert(serverArgs.length > 0);
} catch (std::exception const& ex) {
fmt::print(stderr, "ERROR: Invalid chain length ({})\n", ex.what());
return FDB_EXIT_ERROR;
}
break;
case OPT_CLIENT_CHAIN_LEN:
try {
clientArgs.length = std::stoul(args.OptionArg());
assert(clientArgs.length > 0);
} catch (std::exception const& ex) {
fmt::print(stderr, "ERROR: Invalid chain length ({})\n", ex.what());
return FDB_EXIT_ERROR;
}
break;
case OPT_SERVER_CERT_FILE:
serverArgs.certFile.assign(args.OptionArg());
break;
case OPT_SERVER_KEY_FILE:
serverArgs.keyFile.assign(args.OptionArg());
break;
case OPT_SERVER_CA_FILE:
serverArgs.caFile.assign(args.OptionArg());
break;
case OPT_CLIENT_CERT_FILE:
clientArgs.certFile.assign(args.OptionArg());
break;
case OPT_CLIENT_KEY_FILE:
clientArgs.keyFile.assign(args.OptionArg());
break;
case OPT_CLIENT_CA_FILE:
clientArgs.caFile.assign(args.OptionArg());
break;
case OPT_EXPIRE_SERVER_CERT:
serverArgs.expireLeaf = true;
break;
case OPT_EXPIRE_CLIENT_CERT:
clientArgs.expireLeaf = true;
break;
case OPT_PRINT_SERVER_CERT:
printServerCert = true;
break;
case OPT_PRINT_CLIENT_CERT:
printClientCert = true;
break;
case OPT_PRINT_ARGUMENTS:
printArguments = true;
break;
case OPT_ENABLE_TRACE:
enableTrace = true;
break;
case OPT_NO_SHARED_SERVER_CLIENT_CA:
noSharedServerClientCa = true;
break;
default:
fmt::print(stderr, "ERROR: Unknown option {}\n", args.OptionText());
return FDB_EXIT_ERROR;
}
}
}
// Need to involve flow for the TraceEvent.
try {
platformInit();
Error::init();
g_network = newNet2(TLSConfig());
if (enableTrace)
openTraceFile({}, 10 << 20, 10 << 20, ".", "mkcert");
auto thread = std::thread([]() { g_network->run(); });
auto cleanUpGuard = ScopeExit([&thread, enableTrace]() {
g_network->stop();
thread.join();
if (enableTrace)
closeTraceFile();
});
serverArgs.transformPathToAbs();
clientArgs.transformPathToAbs();
if (printArguments) {
serverArgs.print();
clientArgs.print();
}
auto arena = Arena();
auto serverChain = serverArgs.makeChain(arena);
// IMPORTANT: By default, use same root CA for server and client, such that servers can trust other servers
auto clientChain = clientArgs.makeChain(
arena, noSharedServerClientCa ? Optional<mkcert::CertAndKeyRef>() : serverChain.back());
if (printServerCert || printClientCert) {
if (printServerCert) {
for (auto i = 0; i < serverChain.size(); i++) {
mkcert::printCert(stdout, serverChain[i].certPem);
}
}
if (printClientCert) {
for (auto i = 0; i < clientChain.size(); i++) {
mkcert::printCert(stdout, clientChain[i].certPem);
}
}
} else {
fmt::print("OK\n");
}
return FDB_EXIT_SUCCESS;
} catch (const Error& e) {
fmt::print(stderr, "error: {}\n", e.name());
return FDB_EXIT_MAIN_ERROR;
} catch (const std::exception& e) {
fmt::print(stderr, "exception: {}\n", e.what());
return FDB_EXIT_MAIN_EXCEPTION;
}
}