342 lines
14 KiB
C++
342 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", static_cast<int>(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;
|
|
}
|
|
}
|