373 lines
13 KiB
C++
373 lines
13 KiB
C++
/*
|
|
* 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/AutoCPointer.h"
|
|
#include "flow/IRandom.h"
|
|
#include "flow/MkCert.h"
|
|
#include "flow/PKey.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/rsa.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);
|
|
StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert);
|
|
|
|
struct CertAndKeyNative {
|
|
std::shared_ptr<X509> cert;
|
|
PrivateKey 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 = PrivateKey(PemEncoded{}, keyPem);
|
|
return ret;
|
|
}
|
|
|
|
PemType toPem(Arena& arena) {
|
|
auto ret = PemType{};
|
|
if (null())
|
|
return ret;
|
|
ASSERT(valid());
|
|
ret.certPem = writeX509CertPem(arena, cert);
|
|
ret.privateKeyPem = privateKey.writePem(arena);
|
|
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 = PrivateKey(PemEncoded{}, privateKeyPem);
|
|
auto bio = AutoCPointer(::BIO_new_fp(out, BIO_NOCLOSE), &::BIO_free);
|
|
OSSL_ASSERT(bio);
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_print_private(bio, key.nativeHandle(), 0, nullptr));
|
|
}
|
|
|
|
std::shared_ptr<X509> readX509CertPem(StringRef x509CertPem) {
|
|
ASSERT(!x509CertPem.empty());
|
|
auto bio_mem = AutoCPointer(::BIO_new_mem_buf(x509CertPem.begin(), x509CertPem.size()), &::BIO_free);
|
|
OSSL_ASSERT(bio_mem);
|
|
auto ret = ::PEM_read_bio_X509(bio_mem, nullptr, nullptr, nullptr);
|
|
OSSL_ASSERT(ret);
|
|
return std::shared_ptr<X509>(ret, &::X509_free);
|
|
}
|
|
|
|
StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert) {
|
|
auto mem = AutoCPointer(::BIO_new(::BIO_s_mem()), &::BIO_free);
|
|
OSSL_ASSERT(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));
|
|
}
|
|
|
|
PrivateKey makeEcP256() {
|
|
auto params = AutoCPointer(nullptr, &::EVP_PKEY_free);
|
|
{
|
|
auto paramsRaw = std::add_pointer_t<EVP_PKEY>();
|
|
auto pctx = AutoCPointer(::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), &::EVP_PKEY_CTX_free);
|
|
OSSL_ASSERT(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, ¶msRaw));
|
|
OSSL_ASSERT(paramsRaw);
|
|
params.reset(paramsRaw);
|
|
}
|
|
// keygen
|
|
auto kctx = AutoCPointer(::EVP_PKEY_CTX_new(params, nullptr), &::EVP_PKEY_CTX_free);
|
|
OSSL_ASSERT(kctx);
|
|
auto key = AutoCPointer(nullptr, &::EVP_PKEY_free);
|
|
auto keyRaw = std::add_pointer_t<EVP_PKEY>();
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_keygen_init(kctx));
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &keyRaw));
|
|
OSSL_ASSERT(keyRaw);
|
|
key.reset(keyRaw);
|
|
auto len = 0;
|
|
len = ::i2d_PrivateKey(key, nullptr);
|
|
ASSERT_LT(0, len);
|
|
auto tmpArena = Arena();
|
|
auto buf = new (tmpArena) uint8_t[len];
|
|
auto out = std::add_pointer_t<uint8_t>(buf);
|
|
len = ::i2d_PrivateKey(key, &out);
|
|
return PrivateKey(DerEncoded{}, StringRef(buf, len));
|
|
}
|
|
|
|
PrivateKey makeRsa4096Bit() {
|
|
auto kctx = AutoCPointer(::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), &::EVP_PKEY_CTX_free);
|
|
OSSL_ASSERT(kctx);
|
|
auto key = AutoCPointer(nullptr, &::EVP_PKEY_free);
|
|
auto keyRaw = std::add_pointer_t<EVP_PKEY>();
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_keygen_init(kctx));
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_CTX_set_rsa_keygen_bits(kctx, 4096));
|
|
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &keyRaw));
|
|
OSSL_ASSERT(keyRaw);
|
|
key.reset(keyRaw);
|
|
auto len = 0;
|
|
len = ::i2d_PrivateKey(key, nullptr);
|
|
ASSERT_LT(0, len);
|
|
auto tmpArena = Arena();
|
|
auto buf = new (tmpArena) uint8_t[len];
|
|
auto out = std::add_pointer_t<uint8_t>(buf);
|
|
len = ::i2d_PrivateKey(key, &out);
|
|
return PrivateKey(DerEncoded{}, StringRef(buf, len));
|
|
}
|
|
|
|
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 keypair = makeEcP256();
|
|
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, keypair.nativeHandle()));
|
|
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();
|
|
}
|
|
#ifdef OPENSSL_IS_BORINGSSL
|
|
auto ext = ::X509V3_EXT_nconf_nid(nullptr, &ctx, extNid, const_cast<char*>(extValue.c_str()));
|
|
#else
|
|
auto ext = ::X509V3_EXT_nconf_nid(nullptr, &ctx, extNid, extValue.c_str());
|
|
#endif
|
|
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 ? keypair.nativeHandle() : issuer.privateKey.nativeHandle()), ::EVP_sha256()));
|
|
auto ret = CertAndKeyNative{};
|
|
ret.cert = smartX;
|
|
ret.privateKey = keypair;
|
|
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*/);
|
|
}
|
|
|
|
StringRef CertKind::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);
|
|
}
|
|
}
|
|
|
|
} // namespace mkcert
|