浏览代码

v2.4 Stage C: TLS listener + self-signed cert generation

For listeners with tls.enabled: load cert+key from disk OR if missing
and auto_self_signed_if_missing=true, generate a 10-year RSA-2048
self-signed cert via the OpenSSL 3 EVP API to
<dataDir>/tls/auto_self_signed.{pem,key}. Listener builds
grpc::SslServerCredentials with the resolved key+cert pair.

## tls/cert_generator.{hpp,cpp}

- 2048-bit RSA via EVP_PKEY_CTX_new_id(EVP_PKEY_RSA) + EVP_PKEY_keygen
- X.509 v3 cert with CN=bind, SubjectAltName(DNS:localhost,
  IP:127.0.0.1, plus the bind as DNS or IP depending on whether it
  parses as an IP), SHA-256 signed.
- Random 63-bit positive serial via BN_rand.
- Files written: cert mode 0644, key mode 0600.

## Loud WARN on auto-generation

  TLS listener 127.0.0.1:9443: no operator cert at '...'; auto-
  generated self-signed cert at .../auto_self_signed.pem. OK for dev.
  For production, drop a real cert + key at cert_path/key_path.

## Smoke

  $ openssl s_client -connect 127.0.0.1:9443
    subject=CN=127.0.0.1
    verify error:num=18:self-signed certificate

Stage D adds the auth interceptor on top of this. Tests 14/14 green.
fszontagh 1 月之前
父节点
当前提交
9d6ad72f91
共有 4 个文件被更改,包括 262 次插入6 次删除
  1. 1 0
      service/CMakeLists.txt
  2. 56 6
      service/src/database_service.cpp
  3. 167 0
      service/src/tls/cert_generator.cpp
  4. 38 0
      service/src/tls/cert_generator.hpp

+ 1 - 0
service/CMakeLists.txt

@@ -65,6 +65,7 @@ set(DATABASE_SERVICE_SOURCES
     src/storage/document_store_lmdb.cpp
     src/storage/migrate_v1_to_v2.cpp
     src/storage/project_store.cpp
+    src/tls/cert_generator.cpp
 )
 
 # Create executable

+ 56 - 6
service/src/database_service.cpp

@@ -4,6 +4,10 @@
 #include "storage/document_store.hpp"
 #include "storage/document_store_lmdb.hpp"
 #include "storage/dual_write_mirror.hpp"
+#include "tls/cert_generator.hpp"
+
+#include <fstream>
+#include <sstream>
 #include "storage/lmdb_env.hpp"
 #include "storage/migrate_v1_to_v2.hpp"
 #include "storage/project_store.hpp"
@@ -1124,12 +1128,58 @@ void DatabaseService::startGrpcServer() {
 
         grpc::ServerBuilder builder;
 
-        // v2.4 Stage B — InsecureServerCredentials for every listener
-        // regardless of `tls.enabled`. Stage C swaps in SslServerCredentials
-        // when the listener is configured for TLS; Stage D adds the
-        // per-listener auth interceptor.
-        std::shared_ptr<grpc::ServerCredentials> creds =
-            grpc::InsecureServerCredentials();
+        // v2.4 — choose credentials per listener. Plaintext for
+        // tls.enabled=false; SslServerCredentials with the operator's
+        // cert+key (or an auto-generated self-signed pair) for true.
+        std::shared_ptr<grpc::ServerCredentials> creds;
+        if (listener.tls.enabled) {
+            std::filesystem::path cert_path = listener.tls.cert_path;
+            std::filesystem::path key_path  = listener.tls.key_path;
+
+            // Resolve cert+key. If either is missing/unreadable AND
+            // auto_self_signed_if_missing is set, generate a pair.
+            const bool cert_ok = !cert_path.empty()
+                                  && std::filesystem::exists(cert_path);
+            const bool key_ok  = !key_path.empty()
+                                  && std::filesystem::exists(key_path);
+            if (!cert_ok || !key_ok) {
+                if (!listener.tls.auto_self_signed_if_missing) {
+                    throw std::runtime_error(
+                        "TLS listener " + addr +
+                        ": cert or key missing and auto_self_signed_if_missing=false");
+                }
+                const auto out_dir = config_.dataDirectory / "tls";
+                auto gen = smartbotic::database::tls_util::generateSelfSignedCert(
+                    out_dir, listener.bind);
+                cert_path = gen.cert_path;
+                key_path  = gen.key_path;
+                spdlog::warn(
+                    "TLS listener {}: no operator cert at '{}' (or key at '{}'); "
+                    "auto-generated self-signed cert at '{}' (key '{}'). "
+                    "OK for dev. For production, drop a real cert + key at "
+                    "the configured cert_path/key_path.",
+                    addr,
+                    listener.tls.cert_path.string(),
+                    listener.tls.key_path.string(),
+                    cert_path.string(),
+                    key_path.string());
+            }
+
+            auto slurp = [](const std::filesystem::path& p) {
+                std::ifstream f(p);
+                if (!f) throw std::runtime_error("read " + p.string());
+                std::stringstream ss; ss << f.rdbuf();
+                return ss.str();
+            };
+            grpc::SslServerCredentialsOptions sslOpts;
+            grpc::SslServerCredentialsOptions::PemKeyCertPair kp;
+            kp.private_key  = slurp(key_path);
+            kp.cert_chain   = slurp(cert_path);
+            sslOpts.pem_key_cert_pairs.push_back(std::move(kp));
+            creds = grpc::SslServerCredentials(sslOpts);
+        } else {
+            creds = grpc::InsecureServerCredentials();
+        }
         builder.AddListeningPort(addr, creds);
 
         builder.RegisterService(storageImpl_.get());

+ 167 - 0
service/src/tls/cert_generator.cpp

@@ -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

+ 38 - 0
service/src/tls/cert_generator.hpp

@@ -0,0 +1,38 @@
+// v2.4 — self-signed TLS cert generation for listeners with
+// tls.enabled and tls.auto_self_signed_if_missing.
+//
+// On first boot of a TLS-enabled listener whose configured cert+key
+// don't exist on disk, the server calls generateSelfSignedCert() to
+// write a 10-year RSA-2048 cert to <dataDir>/tls/auto_self_signed.*.
+// Operators who run their own PKI override by dropping their real
+// cert at the configured cert_path; the auto-generated one is then
+// ignored.
+
+#pragma once
+
+#include <filesystem>
+#include <string>
+
+namespace smartbotic::database::tls_util {
+
+struct GeneratedCert {
+    std::filesystem::path cert_path;
+    std::filesystem::path key_path;
+};
+
+// Generate a self-signed RSA-2048 cert + key for the given bind address.
+//
+// `bind` becomes the cert's CN and is included in SubjectAltName along
+// with `DNS:localhost`, `IP:127.0.0.1`. If `bind` parses as an IPv4 or
+// IPv6 address it's added as an IP SAN; otherwise as a DNS SAN.
+//
+// Files written:
+//   <out_dir>/auto_self_signed.pem  (cert, mode 0644)
+//   <out_dir>/auto_self_signed.key  (private key, mode 0600)
+//
+// Throws std::runtime_error on any OpenSSL or filesystem failure.
+GeneratedCert generateSelfSignedCert(const std::filesystem::path& out_dir,
+                                      const std::string& bind,
+                                      int days = 3650);
+
+}  // namespace smartbotic::database::tls_util