Commit 3bfead73 authored by jan.koester's avatar jan.koester
Browse files

test

parent 2c6b39e2
Loading
Loading
Loading
Loading
+5 −0
Changes for src/api/JsonApiServer.cpp: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -334,6 +334,7 @@ void JsonApiController::registerRoutes() {
                 json_object_object_add(item, "acme_directory_url",
                                         json_object_new_string(r.acmeDirectoryUrl.c_str()));
                 json_object_object_add(item, "ca_plugin", json_object_new_string(r.caPlugin.c_str()));
                 json_object_object_add(item, "self_signed", json_object_new_boolean(r.selfSigned));
                 json_object_get(item.obj);
                 json_object_array_add(arr.obj, item.obj);
             }
@@ -355,6 +356,7 @@ void JsonApiController::registerRoutes() {
             rec.acmeEmail = getStringField(body, "acme_email");
             rec.acmeDirectoryUrl = getStringField(body, "acme_directory_url");
             rec.caPlugin = getStringField(body, "ca_plugin");
             rec.selfSigned = getBoolField(body, "self_signed");
             json_object *domainsArr = nullptr;
             if (json_object_object_get_ex(body, "domains", &domainsArr) && domainsArr) {
                 int n = json_object_array_length(domainsArr);
@@ -364,6 +366,9 @@ void JsonApiController::registerRoutes() {
                 }
             }
             if (rec.profile.empty()) return jsonError("profile is required");
             if (rec.acmeEnabled && rec.selfSigned) {
                 return jsonError("acme_enabled and self_signed are mutually exclusive");
             }
             if (rec.acmeEnabled && (rec.domains.empty() || rec.acmeEmail.empty())) {
                 return jsonError("acme_enabled requires at least one domain and acme_email");
             }
+5 −0
Changes for src/main.cpp: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -130,6 +130,11 @@ int main(int argc, char **argv) {
        proxyplus::tls::AcmeChallengeStore acmeChallenges;
        proxyplus::tls::CertificateManager certManager(cfg, configStore, pluginManager,
                                                        acmeChallenges);
        // Self-signed profiles must have a cert on disk before any listener binds below --
        // HttpD/SmtpD's constructor throws if tls_profile resolves to a missing file, and
        // (unlike ACME) there's no reason to defer this to the background checkLoop() since
        // it's a purely local, instant operation.
        certManager.provisionSelfSigned();

        std::string staticDir = PROXYPLUS_DEFAULT_STATIC_DIR;

+2 −0
Changes for src/store/ConfigStore.cpp: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -194,6 +194,7 @@ void encodeTlsProfile(ByteWriter &w, const TlsProfileRecord &r) {
    w.str(r.acmeEmail);
    w.str(r.acmeDirectoryUrl);
    w.str(r.caPlugin);
    w.u8(r.selfSigned ? 1 : 0);
}
TlsProfileRecord decodeTlsProfile(ByteReader &r) {
    TlsProfileRecord rec;
@@ -204,6 +205,7 @@ TlsProfileRecord decodeTlsProfile(ByteReader &r) {
    rec.acmeEmail = r.str();
    rec.acmeDirectoryUrl = r.str();
    rec.caPlugin = r.str();
    rec.selfSigned = r.u8() != 0;
    return rec;
}

+5 −0
Changes for src/store/Records.h: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -98,6 +98,11 @@ struct TlsProfileRecord {
    // "letsencrypt" or "zerossl"). "" is only valid when exactly one such plugin is loaded --
    // see CertificateManager::renew(), which refuses to guess when several are present.
    std::string caPlugin;
    // Locally generate (and, near expiry, regenerate) a self-signed certificate instead of
    // going through ACME -- for frontends with no public hostname to prove ownership of
    // (internal/LAN listeners, first-boot admin UI) where a browser TOFU warning is
    // acceptable. Mutually exclusive with acmeEnabled; see CertificateManager::provisionSelfSigned().
    bool selfSigned = false;
};

// A per-domain DKIM signing key, referenced by the MAIL FROM address's domain during outbound
+48 −9
Changes for src/tls/CertificateManager.cpp: 48 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@
#include <sys/stat.h>

#include <netplus/crypto/x509.h>
#include <netplus/utils/certgen.h>

namespace proxyplus::tls {

@@ -107,32 +108,70 @@ void CertificateManager::renew(const store::TlsProfileRecord &rec) {
        return;
    }

    writeCertKeyAndNotify(rec.profile, result.certificateChainPem, result.privateKeyPem);
}

// Writes a PEM cert+key pair to <dataPath>/tls/<profile>.{crt,key} and notifies every
// listener registered for that profile -- the tail end of both renew() (ACME) and
// generateSelfSigned() (below), which differ only in where the PEM data comes from.
void CertificateManager::writeCertKeyAndNotify(const std::string &profile,
                                                const std::string &certPem,
                                                const std::string &keyPem) {
    std::string dir = _cfg.dataPath + "/tls";
    ::mkdir(dir.c_str(), 0750);
    std::string certPath = dir + "/" + rec.profile + ".crt";
    std::string keyPath = dir + "/" + rec.profile + ".key";
    std::string certPath = dir + "/" + profile + ".crt";
    std::string keyPath = dir + "/" + profile + ".key";

    if (!writeFileAtomic(certPath, result.certificateChainPem) ||
        !writeFileAtomic(keyPath, result.privateKeyPem)) {
        std::cerr << "[tls] failed to write renewed certificate for '" << rec.profile << "'"
                   << std::endl;
    if (!writeFileAtomic(certPath, certPem) || !writeFileAtomic(keyPath, keyPem)) {
        std::cerr << "[tls] failed to write certificate for '" << profile << "'" << std::endl;
        return;
    }
    ::chmod(keyPath.c_str(), 0600);

    std::lock_guard lk(_callbackMutex);
    auto range = _reloadCallbacks.equal_range(rec.profile);
    auto range = _reloadCallbacks.equal_range(profile);
    size_t notified = 0;
    for (auto it = range.first; it != range.second; ++it, ++notified) it->second();
    std::cerr << "[tls] renewed '" << rec.profile << "', notified " << notified << " listener(s)"
    std::cerr << "[tls] wrote certificate for '" << profile << "', notified " << notified
               << " listener(s)" << std::endl;
}

// Self-signed generation is local and instant (no network round-trip, no rate limit like
// ACME), so unlike renew() it's also called synchronously at startup via
// provisionSelfSigned() -- see that method and docs/ARCHITECTURE.md.
void CertificateManager::generateSelfSigned(const store::TlsProfileRecord &rec) {
    netplus::CertGenConfig genCfg;
    genCfg.commonName = rec.domains.empty() ? rec.profile : rec.domains.front();
    genCfg.subjectAltNames = rec.domains;
    genCfg.validDays = 397; // browser-enforced maximum leaf lifetime (Apple/Chrome policy)

    netplus::CertKeyPair kp;
    if (!netplus::generateSelfSignedCert(genCfg, kp)) {
        std::cerr << "[tls] self-signed generation failed for '" << rec.profile << "'"
                   << std::endl;
        return;
    }

    std::cerr << "[tls] generated self-signed certificate for '" << rec.profile
               << "' (CN=" << genCfg.commonName << ")" << std::endl;
    writeCertKeyAndNotify(rec.profile, kp.certPem, kp.keyPem);
}

void CertificateManager::provisionSelfSigned() {
    for (const auto &rec : _configStore.listTlsProfiles()) {
        if (!rec.selfSigned) continue;
        if (needsRenewal(rec.profile)) generateSelfSigned(rec);
    }
}

void CertificateManager::checkLoop() {
    while (_running.load()) {
        for (const auto &rec : _configStore.listTlsProfiles()) {
            if (!rec.acmeEnabled) continue;
            if (rec.acmeEnabled) {
                if (needsRenewal(rec.profile)) renew(rec);
            } else if (rec.selfSigned) {
                if (needsRenewal(rec.profile)) generateSelfSigned(rec);
            }
        }
        for (int i = 0; i < kCheckIntervalSeconds && _running.load(); ++i) {
            std::this_thread::sleep_for(std::chrono::seconds(1));
Loading