/* * MkCertCli.cpp * * This source file is part of the FoundationDB open source project * * Copyright 2013-2024 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 #include #include #include #include #include #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 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 rootCa = {}); }; mkcert::CertChainRef ChainSpec::makeChain(Arena& arena, Optional 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(ca.begin()), ca.size()); auto chainMinusRoot = chain; if (chainMinusRoot.size() > 1) chainMinusRoot.pop_back(); auto cert = mkcert::concatCertChain(arena, chainMinusRoot); ofsCert.write(reinterpret_cast(cert.begin()), cert.size()); auto key = chain[0].privateKeyPem; ofsKey.write(reinterpret_cast(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(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() : 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; } }