Merge pull request #7088 from sfc-gh-jshim/mtls-test-helpers

mTLS test helpers
This commit is contained in:
Junhyun Shim 2022-05-12 14:35:23 +02:00 committed by GitHub
commit 0dbbadfd77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 934 additions and 71 deletions

View File

@ -24,7 +24,9 @@
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/IRandom.h"
#include "flow/MkCert.h"
#include "flow/Platform.h"
#include "flow/ScopeExit.h"
#include "flow/Trace.h"
#include "flow/UnitTest.h"
#include <type_traits>
@ -38,16 +40,6 @@
namespace {
template <typename Func>
class ExitGuard {
std::decay_t<Func> fn;
public:
ExitGuard(Func&& fn) : fn(std::forward<Func>(fn)) {}
~ExitGuard() { fn(); }
};
[[noreturn]] void traceAndThrow(const char* type) {
auto te = TraceEvent(SevWarnAlways, type);
te.suppressFor(60);
@ -56,63 +48,11 @@ public:
0,
};
::ERR_error_string_n(err, buf, sizeof(buf));
te.detail("OpenSSLError", buf);
te.detail("OpenSSLError", static_cast<const char*>(buf));
}
throw digital_signature_ops_error();
}
struct KeyPairRef {
StringRef privateKey;
StringRef publicKey;
};
Standalone<KeyPairRef> generateEcdsaKeyPair() {
auto params = std::add_pointer_t<EVP_PKEY>();
{
auto pctx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
ASSERT(pctx);
auto ctxGuard = ExitGuard([pctx]() { ::EVP_PKEY_CTX_free(pctx); });
ASSERT_LT(0, ::EVP_PKEY_paramgen_init(pctx));
ASSERT_LT(0, ::EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1));
ASSERT_LT(0, ::EVP_PKEY_paramgen(pctx, &params));
ASSERT(params);
}
auto paramsGuard = ExitGuard([params]() { ::EVP_PKEY_free(params); });
// keygen
auto kctx = ::EVP_PKEY_CTX_new(params, nullptr);
ASSERT(kctx);
auto kctxGuard = ExitGuard([kctx]() { ::EVP_PKEY_CTX_free(kctx); });
auto key = std::add_pointer_t<EVP_PKEY>();
{
ASSERT_LT(0, ::EVP_PKEY_keygen_init(kctx));
ASSERT_LT(0, ::EVP_PKEY_keygen(kctx, &key));
}
ASSERT(key);
auto keyGuard = ExitGuard([key]() { ::EVP_PKEY_free(key); });
auto ret = Standalone<KeyPairRef>{};
auto& arena = ret.arena();
{
auto len = 0;
len = ::i2d_PrivateKey(key, nullptr);
ASSERT_LT(0, len);
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(key, &out);
ret.privateKey = StringRef(buf, len);
}
{
auto len = 0;
len = ::i2d_PUBKEY(key, nullptr);
ASSERT_LT(0, len);
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PUBKEY(key, &out);
ret.publicKey = StringRef(buf, len);
}
return ret;
}
} // namespace
Standalone<SignedAuthTokenRef> signToken(AuthTokenRef token, StringRef keyName, StringRef privateKeyDer) {
@ -127,11 +67,11 @@ Standalone<SignedAuthTokenRef> signToken(AuthTokenRef token, StringRef keyName,
if (!key) {
traceAndThrow("SignTokenBadKey");
}
auto keyGuard = ExitGuard([key]() { ::EVP_PKEY_free(key); });
auto keyGuard = ScopeExit([key]() { ::EVP_PKEY_free(key); });
auto mdctx = ::EVP_MD_CTX_create();
if (!mdctx)
traceAndThrow("SignTokenInitFail");
auto mdctxGuard = ExitGuard([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
auto mdctxGuard = ScopeExit([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
if (1 != ::EVP_DigestSignInit(mdctx, nullptr, ::EVP_sha256() /*Parameterize?*/, nullptr, key))
traceAndThrow("SignTokenInitFail");
if (1 != ::EVP_DigestSignUpdate(mdctx, tokenStr.begin(), tokenStr.size()))
@ -153,11 +93,11 @@ bool verifyToken(SignedAuthTokenRef signedToken, StringRef publicKeyDer) {
auto key = ::d2i_PUBKEY(nullptr, &rawPubKeyDer, publicKeyDer.size());
if (!key)
traceAndThrow("VerifyTokenBadKey");
auto keyGuard = ExitGuard([key]() { ::EVP_PKEY_free(key); });
auto keyGuard = ScopeExit([key]() { ::EVP_PKEY_free(key); });
auto mdctx = ::EVP_MD_CTX_create();
if (!mdctx)
traceAndThrow("VerifyTokenInitFail");
auto mdctxGuard = ExitGuard([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
auto mdctxGuard = ScopeExit([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
if (1 != ::EVP_DigestVerifyInit(mdctx, nullptr, ::EVP_sha256(), nullptr, key))
traceAndThrow("VerifyTokenInitFail");
if (1 != ::EVP_DigestVerifyUpdate(mdctx, signedToken.token.begin(), signedToken.token.size()))
@ -182,7 +122,8 @@ void forceLinkTokenSignTests() {}
TEST_CASE("/fdbrpc/TokenSign") {
const auto numIters = 100;
for (auto i = 0; i < numIters; i++) {
auto keyPair = generateEcdsaKeyPair();
auto kpArena = Arena();
auto keyPair = mkcert::KeyPairRef::make(kpArena);
auto token = Standalone<AuthTokenRef>{};
auto& arena = token.arena();
auto& rng = *deterministicRandom();
@ -209,15 +150,15 @@ TEST_CASE("/fdbrpc/TokenSign") {
token.tenants.push_back(arena, genRandomStringRef());
}
auto keyName = genRandomStringRef();
auto signedToken = signToken(token, keyName, keyPair.privateKey);
const auto verifyExpectOk = verifyToken(signedToken, keyPair.publicKey);
auto signedToken = signToken(token, keyName, keyPair.privateKeyDer);
const auto verifyExpectOk = verifyToken(signedToken, keyPair.publicKeyDer);
ASSERT(verifyExpectOk);
// try tampering with signed token by adding one more tenant
token.tenants.push_back(arena, genRandomStringRef());
auto writer = ObjectWriter([&arena](size_t len) { return new (arena) uint8_t[len]; }, IncludeVersion());
writer.serialize(token);
signedToken.token = writer.toStringRef();
const auto verifyExpectFail = verifyToken(signedToken, keyPair.publicKey);
const auto verifyExpectFail = verifyToken(signedToken, keyPair.publicKeyDer);
ASSERT(!verifyExpectFail);
}
printf("%d runs OK\n", numIters);

View File

@ -48,6 +48,8 @@ set(FLOW_SRCS
Knobs.cpp
Knobs.h
MetricSample.h
MkCert.h
MkCert.cpp
Net2.actor.cpp
Net2Packet.cpp
Net2Packet.h
@ -56,6 +58,7 @@ set(FLOW_SRCS
Platform.h
Profiler.actor.cpp
Profiler.h
ScopeExit.h
SendBufferIterator.h
SignalSafeUnwind.cpp
SignalSafeUnwind.h
@ -193,3 +196,6 @@ if(APPLE)
target_link_libraries(flow PRIVATE ${IO_KIT} ${CORE_FOUNDATION})
target_link_libraries(flow_sampling PRIVATE ${IO_KIT} ${CORE_FOUNDATION})
endif()
add_executable(mkcert MkCertCli.cpp)
target_link_libraries(mkcert PUBLIC fmt::fmt flow)

378
flow/MkCert.cpp Normal file
View File

@ -0,0 +1,378 @@
/*
* MkCert.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 "flow/Arena.h"
#include "flow/IRandom.h"
#include "flow/MkCert.h"
#include "flow/ScopeExit.h"
#include <limits>
#include <memory>
#include <string>
#include <cstring>
#include <openssl/bio.h>
#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/objects.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
namespace {
[[noreturn]] void traceAndThrow(const char* condition, int line) {
auto te = TraceEvent(SevWarnAlways, "MkCertOrKeyError");
te.suppressFor(10).detail("Line", line).detail("Condition", condition);
if (auto err = ::ERR_get_error()) {
char buf[256]{
0,
};
::ERR_error_string_n(err, buf, sizeof(buf));
te.detail("OpenSSLError", static_cast<const char*>(buf));
}
throw tls_error();
}
} // anonymous namespace
#define OSSL_ASSERT(condition) \
do { \
if (!(condition)) \
traceAndThrow(#condition, __LINE__); \
} while (false)
namespace mkcert {
// Helper functions working with OpenSSL native types
std::shared_ptr<X509> readX509CertPem(StringRef x509CertPem);
std::shared_ptr<EVP_PKEY> readPrivateKeyPem(StringRef privateKeyPem);
std::shared_ptr<EVP_PKEY> readPrivateKeyPem(StringRef privateKeyPem);
std::shared_ptr<EVP_PKEY> makeEllipticCurveKeyPairNative();
StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert);
StringRef writePrivateKeyPem(Arena& arena, const std::shared_ptr<EVP_PKEY>& nativePrivateKey);
struct CertAndKeyNative {
std::shared_ptr<X509> cert;
std::shared_ptr<EVP_PKEY> privateKey;
bool valid() const noexcept { return cert && privateKey; }
// self-signed cert case
bool null() const noexcept { return !cert && !privateKey; }
using SelfType = CertAndKeyNative;
using PemType = CertAndKeyRef;
static SelfType fromPem(PemType certAndKey) {
auto ret = SelfType{};
if (certAndKey.empty())
return ret;
auto [certPem, keyPem] = certAndKey;
// either both set or both unset
ASSERT(!certPem.empty() && !keyPem.empty());
ret.cert = readX509CertPem(certPem);
ret.privateKey = readPrivateKeyPem(keyPem);
return ret;
}
PemType toPem(Arena& arena) {
auto ret = PemType{};
if (null())
return ret;
ASSERT(valid());
ret.certPem = writeX509CertPem(arena, cert);
ret.privateKeyPem = writePrivateKeyPem(arena, privateKey);
return ret;
}
};
CertAndKeyNative makeCertNative(CertSpecRef spec, CertAndKeyNative issuer);
void printCert(FILE* out, StringRef certPem) {
auto x = readX509CertPem(certPem);
OSSL_ASSERT(0 < ::X509_print_fp(out, x.get()));
}
void printPrivateKey(FILE* out, StringRef privateKeyPem) {
auto key = readPrivateKeyPem(privateKeyPem);
auto bio = ::BIO_new_fp(out, BIO_NOCLOSE);
OSSL_ASSERT(bio);
auto bioGuard = ScopeExit([bio]() { ::BIO_free(bio); });
OSSL_ASSERT(0 < ::EVP_PKEY_print_private(bio, key.get(), 0, nullptr));
}
std::shared_ptr<EVP_PKEY> makeEllipticCurveKeyPairNative() {
auto params = std::add_pointer_t<EVP_PKEY>();
{
auto pctx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
OSSL_ASSERT(pctx);
auto ctxGuard = ScopeExit([pctx]() { ::EVP_PKEY_CTX_free(pctx); });
OSSL_ASSERT(0 < ::EVP_PKEY_paramgen_init(pctx));
OSSL_ASSERT(0 < ::EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1));
OSSL_ASSERT(0 < ::EVP_PKEY_paramgen(pctx, &params));
OSSL_ASSERT(params);
}
auto paramsGuard = ScopeExit([params]() { ::EVP_PKEY_free(params); });
// keygen
auto kctx = ::EVP_PKEY_CTX_new(params, nullptr);
OSSL_ASSERT(kctx);
auto kctxGuard = ScopeExit([kctx]() { ::EVP_PKEY_CTX_free(kctx); });
auto key = std::add_pointer_t<EVP_PKEY>();
OSSL_ASSERT(0 < ::EVP_PKEY_keygen_init(kctx));
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &key));
OSSL_ASSERT(key);
return std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
}
std::shared_ptr<X509> readX509CertPem(StringRef x509CertPem) {
ASSERT(!x509CertPem.empty());
auto bio_mem = ::BIO_new_mem_buf(x509CertPem.begin(), x509CertPem.size());
OSSL_ASSERT(bio_mem);
auto bioGuard = ScopeExit([bio_mem]() { ::BIO_free(bio_mem); });
auto ret = ::PEM_read_bio_X509(bio_mem, nullptr, nullptr, nullptr);
OSSL_ASSERT(ret);
return std::shared_ptr<X509>(ret, &::X509_free);
}
std::shared_ptr<EVP_PKEY> readPrivateKeyPem(StringRef privateKeyPem) {
ASSERT(!privateKeyPem.empty());
auto bio_mem = ::BIO_new_mem_buf(privateKeyPem.begin(), privateKeyPem.size());
OSSL_ASSERT(bio_mem);
auto bioGuard = ScopeExit([bio_mem]() { ::BIO_free(bio_mem); });
auto ret = ::PEM_read_bio_PrivateKey(bio_mem, nullptr, nullptr, nullptr);
OSSL_ASSERT(ret);
return std::shared_ptr<EVP_PKEY>(ret, &::EVP_PKEY_free);
}
StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert) {
auto mem = ::BIO_new(::BIO_s_mem());
OSSL_ASSERT(mem);
auto memGuard = ScopeExit([mem]() { ::BIO_free(mem); });
OSSL_ASSERT(::PEM_write_bio_X509(mem, nativeCert.get()));
auto bioBuf = std::add_pointer_t<char>{};
auto const len = ::BIO_get_mem_data(mem, &bioBuf);
ASSERT_GT(len, 0);
auto buf = new (arena) uint8_t[len];
::memcpy(buf, bioBuf, len);
return StringRef(buf, static_cast<int>(len));
}
StringRef writePrivateKeyPem(Arena& arena, const std::shared_ptr<EVP_PKEY>& nativePrivateKey) {
auto mem = ::BIO_new(::BIO_s_mem());
OSSL_ASSERT(mem);
auto memGuard = ScopeExit([mem]() { ::BIO_free(mem); });
OSSL_ASSERT(::PEM_write_bio_PrivateKey(mem, nativePrivateKey.get(), nullptr, nullptr, 0, 0, nullptr));
auto bioBuf = std::add_pointer_t<char>{};
auto const len = ::BIO_get_mem_data(mem, &bioBuf);
ASSERT_GT(len, 0);
auto buf = new (arena) uint8_t[len];
::memcpy(buf, bioBuf, len);
return StringRef(buf, static_cast<int>(len));
}
KeyPairRef KeyPairRef::make(Arena& arena) {
auto keypair = makeEllipticCurveKeyPairNative();
auto ret = KeyPairRef{};
{
auto len = 0;
len = ::i2d_PrivateKey(keypair.get(), nullptr);
ASSERT_LT(0, len);
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(keypair.get(), &out);
ret.privateKeyDer = StringRef(buf, len);
}
{
auto len = 0;
len = ::i2d_PUBKEY(keypair.get(), nullptr);
ASSERT_LT(0, len);
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PUBKEY(keypair.get(), &out);
ret.publicKeyDer = StringRef(buf, len);
}
return ret;
}
CertAndKeyNative makeCertNative(CertSpecRef spec, CertAndKeyNative issuer) {
// issuer key/cert must be both set or both null (self-signed case)
ASSERT(issuer.valid() || issuer.null());
auto const isSelfSigned = issuer.null();
auto nativeKeyPair = makeEllipticCurveKeyPairNative();
auto newX = ::X509_new();
OSSL_ASSERT(newX);
auto x509Guard = ScopeExit([&newX]() {
if (newX)
::X509_free(newX);
});
auto smartX = std::shared_ptr<X509>(newX, &::X509_free);
newX = nullptr;
auto x = smartX.get();
OSSL_ASSERT(0 < ::X509_set_version(x, 2 /*X509_VERSION_3*/));
auto serialPtr = ::X509_get_serialNumber(x);
OSSL_ASSERT(serialPtr);
OSSL_ASSERT(0 < ::ASN1_INTEGER_set(serialPtr, spec.serialNumber));
auto notBefore = ::X509_getm_notBefore(x);
OSSL_ASSERT(notBefore);
OSSL_ASSERT(::X509_gmtime_adj(notBefore, spec.offsetNotBefore));
auto notAfter = ::X509_getm_notAfter(x);
OSSL_ASSERT(notAfter);
OSSL_ASSERT(::X509_gmtime_adj(notAfter, spec.offsetNotAfter));
OSSL_ASSERT(0 < ::X509_set_pubkey(x, nativeKeyPair.get()));
auto subjectName = ::X509_get_subject_name(x);
OSSL_ASSERT(subjectName);
for (const auto& entry : spec.subjectName) {
// field names are expected to null-terminate
auto fieldName = entry.field.toString();
OSSL_ASSERT(0 <
::X509_NAME_add_entry_by_txt(
subjectName, fieldName.c_str(), MBSTRING_ASC, entry.bytes.begin(), entry.bytes.size(), -1, 0));
}
auto issuerName = ::X509_get_issuer_name(x);
OSSL_ASSERT(issuerName);
OSSL_ASSERT(::X509_set_issuer_name(x, (isSelfSigned ? subjectName : ::X509_get_subject_name(issuer.cert.get()))));
auto ctx = X509V3_CTX{};
X509V3_set_ctx_nodb(&ctx);
::X509V3_set_ctx(&ctx, (isSelfSigned ? x : issuer.cert.get()), x, nullptr, nullptr, 0);
for (const auto& entry : spec.extensions) {
// extension field names and values are expected to null-terminate
auto extName = entry.field.toString();
auto extValue = entry.bytes.toString();
auto extNid = ::OBJ_txt2nid(extName.c_str());
if (extNid == NID_undef) {
TraceEvent(SevWarnAlways, "MkCertInvalidExtName").suppressFor(10).detail("Name", extName);
throw tls_error();
}
auto ext = ::X509V3_EXT_conf_nid(nullptr, &ctx, extNid, extValue.c_str());
OSSL_ASSERT(ext);
auto extGuard = ScopeExit([ext]() { ::X509_EXTENSION_free(ext); });
OSSL_ASSERT(::X509_add_ext(x, ext, -1));
}
OSSL_ASSERT(::X509_sign(x, (isSelfSigned ? nativeKeyPair.get() : issuer.privateKey.get()), ::EVP_sha256()));
auto ret = CertAndKeyNative{};
ret.cert = smartX;
ret.privateKey = nativeKeyPair;
return ret;
}
CertAndKeyRef CertAndKeyRef::make(Arena& arena, CertSpecRef spec, CertAndKeyRef issuerPem) {
auto issuer = CertAndKeyNative::fromPem(issuerPem);
auto newCertAndKey = makeCertNative(spec, issuer);
return newCertAndKey.toPem(arena);
}
CertSpecRef CertSpecRef::make(Arena& arena, CertKind kind) {
auto spec = CertSpecRef{};
spec.serialNumber = static_cast<long>(deterministicRandom()->randomInt64(0, 1e10));
spec.offsetNotBefore = 0; // now
spec.offsetNotAfter = 60 * 60 * 24 * 365; // 1 year from now
auto& subject = spec.subjectName;
subject.push_back(arena, { "countryName"_sr, "DE"_sr });
subject.push_back(arena, { "localityName"_sr, "Berlin"_sr });
subject.push_back(arena, { "organizationName"_sr, "FoundationDB"_sr });
subject.push_back(arena, { "commonName"_sr, kind.getCommonName("FDB Testing Services"_sr, arena) });
auto& ext = spec.extensions;
if (kind.isCA()) {
ext.push_back(arena, { "basicConstraints"_sr, "critical, CA:TRUE"_sr });
ext.push_back(arena, { "keyUsage"_sr, "critical, digitalSignature, keyCertSign, cRLSign"_sr });
} else {
ext.push_back(arena, { "basicConstraints"_sr, "critical, CA:FALSE"_sr });
ext.push_back(arena, { "keyUsage"_sr, "critical, digitalSignature, keyEncipherment"_sr });
ext.push_back(arena, { "extendedKeyUsage"_sr, "serverAuth, clientAuth"_sr });
}
ext.push_back(arena, { "subjectKeyIdentifier"_sr, "hash"_sr });
if (!kind.isRootCA())
ext.push_back(arena, { "authorityKeyIdentifier"_sr, "keyid, issuer"_sr });
return spec;
}
StringRef concatCertChain(Arena& arena, CertChainRef chain) {
auto len = 0;
for (const auto& entry : chain) {
len += entry.certPem.size();
}
if (len == 0)
return StringRef();
auto buf = new (arena) uint8_t[len];
auto offset = 0;
for (auto const& entry : chain) {
::memcpy(&buf[offset], entry.certPem.begin(), entry.certPem.size());
offset += entry.certPem.size();
}
UNSTOPPABLE_ASSERT(offset == len);
return StringRef(buf, len);
}
CertChainRef makeCertChain(Arena& arena, VectorRef<CertSpecRef> specs, CertAndKeyRef rootAuthority) {
ASSERT_GT(specs.size(), 0);
// if rootAuthority is empty, use last element in specs to make root CA
auto const needRootCA = rootAuthority.empty();
if (needRootCA) {
int const chainLength = specs.size();
auto chain = new (arena) CertAndKeyRef[chainLength];
auto caNative = makeCertNative(specs.back(), CertAndKeyNative{} /* empty issuer == self-signed */);
chain[chainLength - 1] = caNative.toPem(arena);
for (auto i = chainLength - 2; i >= 0; i--) {
auto cnkNative = makeCertNative(specs[i], caNative);
chain[i] = cnkNative.toPem(arena);
caNative = cnkNative;
}
return CertChainRef(chain, chainLength);
} else {
int const chainLength = specs.size() + 1; /* account for deep-copied rootAuthority */
auto chain = new (arena) CertAndKeyRef[chainLength];
auto caNative = CertAndKeyNative::fromPem(rootAuthority);
chain[chainLength - 1] = rootAuthority.deepCopy(arena);
for (auto i = chainLength - 2; i >= 0; i--) {
auto cnkNative = makeCertNative(specs[i], caNative);
chain[i] = cnkNative.toPem(arena);
caNative = cnkNative;
}
return CertChainRef(chain, chainLength);
}
}
VectorRef<CertSpecRef> makeCertChainSpec(Arena& arena, unsigned length, ESide side) {
if (!length)
return {};
auto specs = new (arena) CertSpecRef[length];
auto const isServerSide = side == ESide::Server;
for (auto i = 0u; i < length; i++) {
auto kind = CertKind{};
if (i == 0u)
kind = isServerSide ? CertKind(Server{}) : CertKind(Client{});
else if (i == length - 1)
kind = isServerSide ? CertKind(ServerRootCA{}) : CertKind(ClientRootCA{});
else
kind = isServerSide ? CertKind(ServerIntermediateCA{ i }) : CertKind(ClientIntermediateCA{ i });
specs[i] = CertSpecRef::make(arena, kind);
}
return VectorRef<CertSpecRef>(specs, length);
}
CertChainRef makeCertChain(Arena& arena, unsigned length, ESide side) {
if (!length)
return {};
// temporary arena for writing up specs
auto tmpArena = Arena();
auto specs = makeCertChainSpec(tmpArena, length, side);
return makeCertChain(arena, specs, {} /*root*/);
}
} // namespace mkcert

176
flow/MkCert.h Normal file
View File

@ -0,0 +1,176 @@
/*
* MkCert.h
*
* 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 MKCERT_H
#define MKCERT_H
#include "flow/Arena.h"
#include "flow/Error.h"
#include <fmt/format.h>
#include <string>
#include <type_traits>
#include <variant>
namespace mkcert {
void printCert(FILE* out, StringRef certPem);
void printPrivateKey(FILE* out, StringRef privateKeyPem);
struct KeyPairRef {
using SelfType = KeyPairRef;
// Make new Elliptic Curve private-public key pair in DER
static SelfType make(Arena& arena);
StringRef privateKeyDer;
StringRef publicKeyDer;
};
struct Asn1EntryRef {
// field must match one of ASN.1 object short/long names: e.g. "C", "countryName", "CN", "commonName",
// "subjectAltName", ...
StringRef field;
StringRef bytes;
};
struct ServerRootCA {};
struct ServerIntermediateCA {
unsigned level;
};
struct Server {};
struct ClientRootCA {};
struct ClientIntermediateCA {
unsigned level;
};
struct Client {};
struct CertKind {
CertKind() noexcept = default;
template <class Kind>
CertKind(Kind kind) noexcept : value(std::in_place_type<Kind>, kind) {}
template <class Kind>
bool is() const noexcept {
return std::holds_alternative<Kind>(value);
}
template <class Kind>
Kind const& get() const {
return std::get<Kind>(value);
}
bool isServerSide() const noexcept { return is<ServerRootCA>() || is<ServerIntermediateCA>() || is<Server>(); }
bool isClientSide() const noexcept { return !isServerSide(); }
bool isRootCA() const noexcept { return is<ServerRootCA>() || is<ClientRootCA>(); }
bool isIntermediateCA() const noexcept { return is<ServerIntermediateCA>() || is<ClientIntermediateCA>(); }
bool isLeaf() const noexcept { return is<Server>() || is<Client>(); }
bool isCA() const noexcept { return !isLeaf(); }
StringRef getCommonName(StringRef prefix, Arena& arena) const {
auto const side = std::string(isClientSide() ? " Client" : " Server");
if (isIntermediateCA()) {
auto const level = isClientSide() ? get<ClientIntermediateCA>().level : get<ServerIntermediateCA>().level;
return prefix.withSuffix(fmt::format("{} Intermediate {}", side, level), arena);
} else if (isRootCA()) {
return prefix.withSuffix(fmt::format("{} Root", side), arena);
} else {
return prefix.withSuffix(side, arena);
}
}
std::variant<ServerRootCA, ServerIntermediateCA, Server, ClientRootCA, ClientIntermediateCA, Client> value;
};
struct CertSpecRef {
using SelfType = CertSpecRef;
long serialNumber;
// offset in number of seconds relative to now, i.e. cert creation
long offsetNotBefore;
long offsetNotAfter;
VectorRef<Asn1EntryRef> subjectName;
// time offset relative to time of cert creation (now)
VectorRef<Asn1EntryRef> extensions;
// make test-only sample certificate whose fields are inferred from CertKind
static SelfType make(Arena& arena, CertKind kind);
};
struct CertAndKeyRef {
using SelfType = CertAndKeyRef;
StringRef certPem;
StringRef privateKeyPem;
void printCert(FILE* out) {
if (!certPem.empty()) {
::mkcert::printCert(out, certPem);
}
}
void printPrivateKey(FILE* out) {
if (!privateKeyPem.empty()) {
::mkcert::printPrivateKey(out, privateKeyPem);
}
}
bool empty() const noexcept { return certPem.empty() && privateKeyPem.empty(); }
SelfType deepCopy(Arena& arena) {
auto ret = SelfType{};
if (!certPem.empty())
ret.certPem = StringRef(arena, certPem);
if (!privateKeyPem.empty())
ret.privateKeyPem = StringRef(arena, privateKeyPem);
return ret;
}
// Empty (default) issuer produces a self-signed certificate
static SelfType make(Arena& arena, CertSpecRef spec, CertAndKeyRef issuer);
};
using CertChainRef = VectorRef<CertAndKeyRef>;
// Concatenate chain of PEMs to one StringRef
StringRef concatCertChain(Arena& arena, CertChainRef chain);
enum class ESide : int { Server, Client };
// Generate a chain of valid cert specs that have consistent subject/issuer names and
// is valid for typical server/client TLS scenario
// The 'side' parameter makes a difference in the commonName ("CN") field of the produced specs
VectorRef<CertSpecRef> makeCertChainSpec(Arena& arena, unsigned length, ESide side);
// For empty (default) rootAuthority, the last item in specs is used to generate rootAuthority
// Otherwise, rootAuthority is deep-copied to first element of returned chain
CertChainRef makeCertChain(Arena& arena, VectorRef<CertSpecRef> specs, CertAndKeyRef rootAuthority);
// Make stub cert chain of given length inc. root authority
// Note: side does not imply anything different other than the choice of common names
CertChainRef makeCertChain(Arena& arena, unsigned depth, ESide side);
} // namespace mkcert
#endif /*MKCERT_H*/

326
flow/MkCertCli.cpp Normal file
View File

@ -0,0 +1,326 @@
/*
* 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 "flow/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,
};
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 },
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." });
}
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);
};
mkcert::CertChainRef ChainSpec::makeChain(Arena& arena) {
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, {} /*generate root CA*/);
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());
ofsCert.close();
ofsKey.close();
ofsCa.close();
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 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());
} 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());
} 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;
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());
openTraceFile(NetworkAddress(), 10 << 20, 10 << 20, ".", "mkcert");
auto thread = std::thread([]() {
TraceEvent::setNetworkThread();
g_network->run();
});
auto cleanUpGuard = ScopeExit([&thread]() {
flushTraceFileVoid();
g_network->stop();
thread.join();
});
serverArgs.transformPathToAbs();
clientArgs.transformPathToAbs();
if (printArguments) {
serverArgs.print();
clientArgs.print();
}
auto arena = Arena();
auto serverChain = serverArgs.makeChain(arena);
auto clientChain = clientArgs.makeChain(arena);
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;
}
}

36
flow/ScopeExit.h Normal file
View File

@ -0,0 +1,36 @@
/*
* ScopeExit.h
*
* 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 FLOW_SCOPE_EXIT_H
#define FLOW_SCOPE_EXIT_H
#pragma once
// Execute lambda as this object goes out of scope
template <typename Func>
class ScopeExit {
std::decay_t<Func> fn;
public:
ScopeExit(Func&& fn) : fn(std::forward<Func>(fn)) {}
~ScopeExit() { fn(); }
};
#endif /*FLOW_SCOPE_EXIT_H*/