|
|
@@ -0,0 +1,167 @@
|
|
|
+// v2.4 — self-signed TLS cert generation via OpenSSL 3 EVP API.
|
|
|
+
|
|
|
+#include "tls/cert_generator.hpp"
|
|
|
+
|
|
|
+#include <arpa/inet.h>
|
|
|
+#include <fstream>
|
|
|
+#include <stdexcept>
|
|
|
+#include <sys/stat.h>
|
|
|
+
|
|
|
+#include <openssl/bn.h>
|
|
|
+#include <openssl/err.h>
|
|
|
+#include <openssl/evp.h>
|
|
|
+#include <openssl/pem.h>
|
|
|
+#include <openssl/rsa.h>
|
|
|
+#include <openssl/x509.h>
|
|
|
+#include <openssl/x509v3.h>
|
|
|
+
|
|
|
+namespace fs = std::filesystem;
|
|
|
+
|
|
|
+namespace smartbotic::database::tls_util {
|
|
|
+
|
|
|
+namespace {
|
|
|
+
|
|
|
+[[noreturn]] void openssl_throw(const char* where) {
|
|
|
+ std::string msg = "tls_util::generateSelfSignedCert: ";
|
|
|
+ msg += where;
|
|
|
+ msg += ": ";
|
|
|
+ unsigned long e = ERR_get_error();
|
|
|
+ if (e) {
|
|
|
+ char buf[256];
|
|
|
+ ERR_error_string_n(e, buf, sizeof(buf));
|
|
|
+ msg += buf;
|
|
|
+ } else {
|
|
|
+ msg += "unknown OpenSSL error";
|
|
|
+ }
|
|
|
+ throw std::runtime_error(msg);
|
|
|
+}
|
|
|
+
|
|
|
+bool looks_like_ip(const std::string& s) {
|
|
|
+ in_addr v4;
|
|
|
+ in6_addr v6;
|
|
|
+ return inet_pton(AF_INET, s.c_str(), &v4) == 1
|
|
|
+ || inet_pton(AF_INET6, s.c_str(), &v6) == 1;
|
|
|
+}
|
|
|
+
|
|
|
+// RAII wrappers.
|
|
|
+struct EvpKeyDeleter { void operator()(EVP_PKEY* p) const { EVP_PKEY_free(p); } };
|
|
|
+struct X509Deleter { void operator()(X509* p) const { X509_free(p); } };
|
|
|
+struct EvpCtxDeleter { void operator()(EVP_PKEY_CTX* p) const { EVP_PKEY_CTX_free(p); } };
|
|
|
+struct BignumDeleter { void operator()(BIGNUM* p) const { BN_free(p); } };
|
|
|
+
|
|
|
+using EvpKeyPtr = std::unique_ptr<EVP_PKEY, EvpKeyDeleter>;
|
|
|
+using X509Ptr = std::unique_ptr<X509, X509Deleter>;
|
|
|
+using EvpCtxPtr = std::unique_ptr<EVP_PKEY_CTX, EvpCtxDeleter>;
|
|
|
+using BignumPtr = std::unique_ptr<BIGNUM, BignumDeleter>;
|
|
|
+
|
|
|
+EvpKeyPtr generate_rsa_key(int bits) {
|
|
|
+ EvpCtxPtr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr));
|
|
|
+ if (!ctx) openssl_throw("EVP_PKEY_CTX_new_id");
|
|
|
+ if (EVP_PKEY_keygen_init(ctx.get()) <= 0) openssl_throw("EVP_PKEY_keygen_init");
|
|
|
+ if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), bits) <= 0)
|
|
|
+ openssl_throw("EVP_PKEY_CTX_set_rsa_keygen_bits");
|
|
|
+ EVP_PKEY* raw = nullptr;
|
|
|
+ if (EVP_PKEY_keygen(ctx.get(), &raw) <= 0) openssl_throw("EVP_PKEY_keygen");
|
|
|
+ return EvpKeyPtr(raw);
|
|
|
+}
|
|
|
+
|
|
|
+void add_san(X509* cert, const std::string& bind) {
|
|
|
+ std::string san = "DNS:localhost,IP:127.0.0.1";
|
|
|
+ if (!bind.empty() && bind != "localhost" && bind != "127.0.0.1") {
|
|
|
+ san += ",";
|
|
|
+ san += (looks_like_ip(bind) ? "IP:" : "DNS:");
|
|
|
+ san += bind;
|
|
|
+ }
|
|
|
+ X509_EXTENSION* ext = X509V3_EXT_conf_nid(nullptr, nullptr,
|
|
|
+ NID_subject_alt_name,
|
|
|
+ san.c_str());
|
|
|
+ if (!ext) openssl_throw("X509V3_EXT_conf_nid (SAN)");
|
|
|
+ if (!X509_add_ext(cert, ext, -1)) {
|
|
|
+ X509_EXTENSION_free(ext);
|
|
|
+ openssl_throw("X509_add_ext (SAN)");
|
|
|
+ }
|
|
|
+ X509_EXTENSION_free(ext);
|
|
|
+}
|
|
|
+
|
|
|
+void write_pem_cert(const fs::path& path, X509* cert) {
|
|
|
+ std::FILE* f = std::fopen(path.c_str(), "wb");
|
|
|
+ if (!f) throw std::runtime_error("open cert file for write: " + path.string());
|
|
|
+ if (!PEM_write_X509(f, cert)) { std::fclose(f); openssl_throw("PEM_write_X509"); }
|
|
|
+ std::fclose(f);
|
|
|
+ ::chmod(path.c_str(), 0644);
|
|
|
+}
|
|
|
+
|
|
|
+void write_pem_key(const fs::path& path, EVP_PKEY* key) {
|
|
|
+ std::FILE* f = std::fopen(path.c_str(), "wb");
|
|
|
+ if (!f) throw std::runtime_error("open key file for write: " + path.string());
|
|
|
+ if (!PEM_write_PrivateKey(f, key, nullptr, nullptr, 0, nullptr, nullptr)) {
|
|
|
+ std::fclose(f);
|
|
|
+ openssl_throw("PEM_write_PrivateKey");
|
|
|
+ }
|
|
|
+ std::fclose(f);
|
|
|
+ ::chmod(path.c_str(), 0600);
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace
|
|
|
+
|
|
|
+GeneratedCert generateSelfSignedCert(const fs::path& out_dir,
|
|
|
+ const std::string& bind,
|
|
|
+ int days) {
|
|
|
+ std::error_code ec;
|
|
|
+ fs::create_directories(out_dir, ec);
|
|
|
+ if (ec) {
|
|
|
+ throw std::runtime_error("generateSelfSignedCert: cannot create '"
|
|
|
+ + out_dir.string() + "': " + ec.message());
|
|
|
+ }
|
|
|
+
|
|
|
+ EvpKeyPtr key = generate_rsa_key(2048);
|
|
|
+
|
|
|
+ X509Ptr cert(X509_new());
|
|
|
+ if (!cert) openssl_throw("X509_new");
|
|
|
+ X509_set_version(cert.get(), 2); // X.509 v3
|
|
|
+
|
|
|
+ // Serial — random 64-bit positive.
|
|
|
+ BignumPtr serial(BN_new());
|
|
|
+ if (!serial || !BN_rand(serial.get(), 63, BN_RAND_TOP_TWO, BN_RAND_BOTTOM_ANY))
|
|
|
+ openssl_throw("BN_rand");
|
|
|
+ ASN1_INTEGER* sn = ASN1_INTEGER_new();
|
|
|
+ if (!sn) openssl_throw("ASN1_INTEGER_new");
|
|
|
+ if (!BN_to_ASN1_INTEGER(serial.get(), sn)) {
|
|
|
+ ASN1_INTEGER_free(sn);
|
|
|
+ openssl_throw("BN_to_ASN1_INTEGER");
|
|
|
+ }
|
|
|
+ if (!X509_set_serialNumber(cert.get(), sn)) {
|
|
|
+ ASN1_INTEGER_free(sn);
|
|
|
+ openssl_throw("X509_set_serialNumber");
|
|
|
+ }
|
|
|
+ ASN1_INTEGER_free(sn);
|
|
|
+
|
|
|
+ X509_gmtime_adj(X509_get_notBefore(cert.get()), 0);
|
|
|
+ X509_gmtime_adj(X509_get_notAfter(cert.get()),
|
|
|
+ static_cast<long>(days) * 24L * 60L * 60L);
|
|
|
+
|
|
|
+ X509_set_pubkey(cert.get(), key.get());
|
|
|
+
|
|
|
+ X509_NAME* name = X509_get_subject_name(cert.get());
|
|
|
+ const std::string cn = bind.empty() ? std::string("smartbotic-database") : bind;
|
|
|
+ X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
|
|
+ reinterpret_cast<const unsigned char*>(cn.c_str()),
|
|
|
+ -1, -1, 0);
|
|
|
+ X509_set_issuer_name(cert.get(), name);
|
|
|
+
|
|
|
+ add_san(cert.get(), bind);
|
|
|
+
|
|
|
+ if (!X509_sign(cert.get(), key.get(), EVP_sha256()))
|
|
|
+ openssl_throw("X509_sign");
|
|
|
+
|
|
|
+ GeneratedCert out;
|
|
|
+ out.cert_path = out_dir / "auto_self_signed.pem";
|
|
|
+ out.key_path = out_dir / "auto_self_signed.key";
|
|
|
+
|
|
|
+ write_pem_cert(out.cert_path, cert.get());
|
|
|
+ write_pem_key(out.key_path, key.get());
|
|
|
+
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::database::tls_util
|