Loading CMakeLists.txt +6 −28 Changes for CMakeLists.txt: 6 added lines, 28 removed lines. Original line number Diff line number Diff line Loading @@ -24,23 +24,11 @@ find_package(json-c REQUIRED) find_package(Threads REQUIRED) # --- Optional: OpenDKIM / OpenDMARC (see docs/ARCHITECTURE.md §8) --- find_library(OPENDKIM_LIB NAMES opendkim) find_path(OPENDKIM_INCLUDE_DIR NAMES dkim.h PATH_SUFFIXES opendkim) find_library(OPENDMARC_LIB NAMES opendmarc) find_path(OPENDMARC_INCLUDE_DIR NAMES dmarc.h PATH_SUFFIXES opendmarc) if(OPENDKIM_LIB AND OPENDKIM_INCLUDE_DIR) message(STATUS "proxyplus: OpenDKIM found, building real DkimAdapter") else() message(WARNING "proxyplus: libopendkim-dev not found -- DkimAdapter will be a no-op stub") endif() if(OPENDMARC_LIB AND OPENDMARC_INCLUDE_DIR) message(STATUS "proxyplus: OpenDMARC found, building real DmarcAdapter") else() message(WARNING "proxyplus: libopendmarc-dev not found -- DmarcAdapter will be a no-op stub") endif() # --- DKIM / DMARC (see docs/ARCHITECTURE.md §8) --- # Own RFC 6376 / RFC 7489 implementation on top of libnetplus's RSA/SHA256/ # base64 primitives and a small hand-rolled DNS TXT resolver -- no dependency # on libopendkim/libopendmarc (which had cross-platform header-layout issues # and a C++/C99-_Bool incompatibility not worth carrying). set(PROXYPLUS_CORE_SOURCES src/config/BootstrapConfig.cpp src/auth/AuthDBAdapter.cpp Loading @@ -50,6 +38,7 @@ set(PROXYPLUS_CORE_SOURCES src/http/HttpProxyServer.cpp src/smtp/SmtpProxyServer.cpp src/api/JsonApiServer.cpp src/security/DnsTxtResolver.cpp src/security/DkimAdapter.cpp src/security/DmarcAdapter.cpp src/tls/CertificateManager.cpp Loading @@ -74,17 +63,6 @@ target_link_libraries(proxyplus_core PUBLIC ${CMAKE_DL_LIBS} ) if(OPENDKIM_LIB AND OPENDKIM_INCLUDE_DIR) target_compile_definitions(proxyplus_core PRIVATE PROXYPLUS_WITH_OPENDKIM) target_include_directories(proxyplus_core PRIVATE ${OPENDKIM_INCLUDE_DIR}) target_link_libraries(proxyplus_core PRIVATE ${OPENDKIM_LIB}) endif() if(OPENDMARC_LIB AND OPENDMARC_INCLUDE_DIR) target_compile_definitions(proxyplus_core PRIVATE PROXYPLUS_WITH_OPENDMARC) target_include_directories(proxyplus_core PRIVATE ${OPENDMARC_INCLUDE_DIR}) target_link_libraries(proxyplus_core PRIVATE ${OPENDMARC_LIB}) endif() add_executable(proxyplus src/main.cpp) target_link_libraries(proxyplus PRIVATE proxyplus_core) target_compile_definitions(proxyplus PRIVATE Loading README.md +9 −6 Changes for README.md: 9 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ code against verified library APIs, not stubs pretending to be complete. Requires (all found via `find_package`, all already packaged on this system): `libconfplus-dev`, `libauthorize-dev` (ships `authdb`'s client headers), `libhttppp-dev`, `libsmtppp-dev`, `libparitypp-dev`, `libnetplus-dev`, `libcmdplus-dev`, `libuuidplus-dev`, `libjson-c-dev`. Optional: `libopendkim-dev`, `libopendmarc-dev` (see below). `libjson-c-dev`. ```sh mkdir build && cd build Loading Loading @@ -45,12 +45,15 @@ sudo make install # or rebuild the .deb via the usual debian/ packaging and re This was verified: relinking `proxyplus` against a freshly built `libsmtppp.so` from that tree succeeds with zero changes to ProxyPlus's own code. ### Optional: OpenDKIM / OpenDMARC ### DKIM / DMARC Neither `libopendkim-dev` nor `libopendmarc-dev` is installed in this environment. `src/security/DkimAdapter.cpp` / `DmarcAdapter.cpp` are written against the real upstream C APIs but compile to no-op stubs (`PROXYPLUS_WITH_OPENDKIM`/`PROXYPLUS_WITH_OPENDMARC` unset) until those packages are present -- `cmake` auto-detects and reports which mode is active. `src/security/DkimAdapter.cpp` / `DmarcAdapter.cpp` are ProxyPlus's own RFC 6376 / RFC 7489 implementation on top of `libnetplus`'s RSA/SHA256/base64 primitives and a small hand-rolled DNS TXT resolver (`DnsTxtResolver.cpp`) -- no `libopendkim`/`libopendmarc` dependency, so there is nothing extra to install. See the scope notes at the top of `DmarcAdapter.cpp` and docs/ARCHITECTURE.md §8 for what's simplified relative to the full RFCs (no organizational-domain fallback, relaxed alignment approximated without a Public Suffix List, `pct=` parsed but not sampled against). ## First-time setup Loading docs/ARCHITECTURE.md +12 −6 Changes for docs/ARCHITECTURE.md: 12 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -253,7 +253,9 @@ proxyplus/ api/ JsonApiServer – json-c REST-API, Permission-Middleware tls/ AcmeChallengeStore, CertificateManager – ACME-Erneuerungs-Orchestrierung lb/ LoadBalancer – Pool-Zielauswahl + passive Gesundheitsprüfung (§6b) security/ DkimAdapter, DmarcAdapter – optionale OpenDKIM/OpenDMARC-Wrapper security/ DkimAdapter, DmarcAdapter, DnsTxtResolver – eigene RFC-6376/7489-Implementierung auf libnetplus-Krypto (RSA/SHA256/base64) + hand-geschriebenem DNS-TXT-Lookup, keine Abhängigkeit von libopendkim/libopendmarc (§8) main.cpp – Bootstrap: Config laden → Single/Cluster → Listener starten admin/static/ – AJAX-Admin-UI (statisches HTML/JS gegen JsonApiServer) plugins/example_dnsbl/ – Beispielplugin (SMTP-Connection-Check via DNSBL) Loading @@ -274,11 +276,15 @@ proxyplus/ selbst, nicht in ProxyPlus. - `libparitypp`-basierte Replikation ist Last-Writer-Wins ohne echten Konsens; für Split-Brain-Schutz bräuchte es eine Membership-/Write-Ordering-Schicht. - OpenDKIM-/OpenDMARC-Header sind auf diesem System nicht installiert (`libopendkim-dev`/`libopendmarc-dev` fehlen) — die Adapter sind gegen die reale Upstream-API geschrieben, aber in diesem Environment nicht kompilierbar getestet; das CMake-Build behandelt sie als optional (`find_library`, degradiert zu einem No-Op-Stub-Target, wenn nicht gefunden). - DkimAdapter/DmarcAdapter sind eine eigene RFC-6376/7489-Implementierung (kein libopendkim/libopendmarc mehr — deren Header hatten je nach System unterschiedliche Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), SPF-Alignment vertraut dem `spfPass`-Flag des Callers ohne eigenen Domain-Vergleich (das Interface bekommt keine separate SPF-authentifizierte Domain). - Gruppen-Anlage (`addGroup`) fehlt aktuell im `authdb::client`-API (nur `listGroups()` lesend, `GPOadd` schreibend) — `scripts/authdb-bootstrap.sh` dokumentiert das explizit als authdb-seitige Lücke statt sie zu umgehen. src/security/DkimAdapter.cpp +353 −52 Changes for src/security/DkimAdapter.cpp: 353 added lines, 52 removed lines. Original line number Diff line number Diff line #include "DkimAdapter.h" #include "DnsTxtResolver.h" #include "TagValueParser.h" #ifdef PROXYPLUS_WITH_OPENDKIM #include <dkim.h> #endif #include <netplus/crypto/rsa.h> #include <netplus/crypto/rsa_pkcs1_sha256.h> #include <netplus/crypto/sha.h> #include <netplus/crypto/base64.h> #include <algorithm> #include <cctype> #include <vector> // Own DKIM (RFC 6376) implementation on top of libnetplus's RSA/SHA/base64 // primitives -- no dependency on libopendkim. Supports the rsa-sha256 // signing algorithm with relaxed or simple header/body canonicalization. // h= is a fixed candidate header set (see kSignedHeaders below); this covers // what most senders sign and is not meant to be user-configurable yet. namespace proxyplus::security { #ifdef PROXYPLUS_WITH_OPENDKIM namespace { struct DkimAdapter::Impl { DKIM_LIB *lib = nullptr; Impl() { lib = dkim_init(nullptr, nullptr); } ~Impl() { if (lib) dkim_close(lib); bool ieq(const std::string &a, const std::string &b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i]))) return false; } return true; } std::string toLower(const std::string &s) { std::string out = s; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return std::tolower(c); }); return out; } std::vector<uint8_t> toBytes(const std::string &s) { return std::vector<uint8_t>(s.begin(), s.end()); } std::string base64Encode(const std::vector<uint8_t> &data) { netplus::base64 enc; enc << std::vector<char>(data.begin(), data.end()); const auto &encoded = enc.encoded(); return std::string(encoded.begin(), encoded.end()); } std::vector<uint8_t> base64Decode(const std::string &b64) { netplus::base64 dec; dec.setEncodedData(b64); std::vector<char> out; dec >> out; return std::vector<uint8_t>(out.begin(), out.end()); } struct HeaderField { std::string name; // exact original case, e.g. "DKIM-Signature" std::string value; // substring after the first ':', folding preserved as embedded "\r\n" std::string fullOriginal; // "Name" + ":" + value, exact original bytes, no trailing CRLF }; // Splits an RFC 5322 message into its header fields (folding preserved) and // body. The header/body boundary is the first blank line; either bare LF or // CRLF line endings are accepted throughout. void splitMessage(const std::string &raw, std::vector<HeaderField> &headers, std::string &body) { size_t boundary = std::string::npos; size_t sepLen = 0; size_t p1 = raw.find("\r\n\r\n"); size_t p2 = raw.find("\n\n"); if (p1 != std::string::npos && (p2 == std::string::npos || p1 <= p2)) { boundary = p1; sepLen = 4; } else if (p2 != std::string::npos) { boundary = p2; sepLen = 2; } std::string headerBlock = (boundary == std::string::npos) ? raw : raw.substr(0, boundary); body = (boundary == std::string::npos) ? "" : raw.substr(boundary + sepLen); std::vector<std::string> lines; size_t pos = 0; while (pos <= headerBlock.size()) { size_t nl = headerBlock.find('\n', pos); std::string line = headerBlock.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); if (!line.empty() && line.back() == '\r') line.pop_back(); lines.push_back(line); if (nl == std::string::npos) break; pos = nl + 1; } for (const auto &line : lines) { if (line.empty()) continue; bool isContinuation = (line[0] == ' ' || line[0] == '\t'); if (isContinuation && !headers.empty()) { headers.back().value += "\r\n" + line; headers.back().fullOriginal += "\r\n" + line; } else { size_t colon = line.find(':'); if (colon == std::string::npos) continue; // malformed, skip headers.push_back({line.substr(0, colon), line.substr(colon + 1), line}); } } } // RFC 6376 3.4.2 relaxed header canonicalization. std::string canonHeaderRelaxed(const std::string &name, const std::string &value) { std::string unfolded; unfolded.reserve(value.size()); for (size_t i = 0; i < value.size();) { if (value.compare(i, 2, "\r\n") == 0) { i += 2; // delete the fold itself, keep whatever WSP follows } else { unfolded += value[i++]; } } std::string collapsed; bool inWs = false; for (char c : unfolded) { if (c == ' ' || c == '\t') { if (!inWs) { collapsed += ' '; inWs = true; } } else { collapsed += c; inWs = false; } } size_t b = collapsed.find_first_not_of(' '); size_t e = collapsed.find_last_not_of(' '); std::string trimmed = (b == std::string::npos) ? "" : collapsed.substr(b, e - b + 1); return toLower(name) + ":" + trimmed; } // RFC 6376 3.4.4 relaxed body canonicalization. std::string canonBodyRelaxed(const std::string &body) { std::vector<std::string> lines; size_t pos = 0; while (pos <= body.size()) { size_t nl = body.find('\n', pos); std::string line = body.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); if (!line.empty() && line.back() == '\r') line.pop_back(); std::string collapsed; bool inWs = false; for (char c : line) { if (c == ' ' || c == '\t') { if (!inWs) { collapsed += ' '; inWs = true; } } else { collapsed += c; inWs = false; } } if (!collapsed.empty() && collapsed.back() == ' ') collapsed.pop_back(); lines.push_back(collapsed); if (nl == std::string::npos) break; pos = nl + 1; } while (!lines.empty() && lines.back().empty()) lines.pop_back(); if (lines.empty()) return ""; std::string out; for (const auto &l : lines) { out += l; out += "\r\n"; } return out; } // RFC 6376 3.4.3 simple body canonicalization. std::string canonBodySimple(const std::string &body) { std::string s = body; while (s.size() >= 2 && s.compare(s.size() - 2, 2, "\r\n") == 0) s.resize(s.size() - 2); return s.empty() ? "\r\n" : s + "\r\n"; } std::string canonBody(const std::string &body, const std::string &algo) { return ieq(algo, "simple") ? canonBodySimple(body) : canonBodyRelaxed(body); } // The candidate set of header fields we sign, and (for verification) will // look up by name when reconstructing the signing input from an h= tag. const char *const kSignedHeaders[] = {"From", "To", "Subject", "Date", "Message-ID"}; const HeaderField *findHeader(const std::vector<HeaderField> &headers, const std::string &name) { for (const auto &h : headers) { if (ieq(h.name, name)) return &h; } return nullptr; } // Builds the exact bytes that get RSA-signed/verified: each signed header, // canonicalized and CRLF-terminated, in `order`, followed by the // DKIM-Signature header itself (canonicalized, value truncated right after // "b=", and with NO trailing CRLF -- RFC 6376 3.7). std::string buildSigningInput(const std::vector<HeaderField> &headers, const std::vector<std::string> &order, const std::string &headerAlgo, const std::string &dkimSigName, const std::string &dkimSigValueUpToB) { std::string input; for (const auto &name : order) { const HeaderField *h = findHeader(headers, name); if (!h) continue; if (ieq(headerAlgo, "simple")) { input += h->fullOriginal + "\r\n"; } else { input += canonHeaderRelaxed(h->name, h->value) + "\r\n"; } } if (ieq(headerAlgo, "simple")) { input += dkimSigName + ":" + dkimSigValueUpToB; } else { input += canonHeaderRelaxed(dkimSigName, dkimSigValueUpToB); } return input; } } // namespace struct DkimAdapter::Impl { netplus::rsa key; bool keyLoaded = false; }; DkimAdapter::DkimAdapter(std::string domain, std::string selector, std::string keyPath) : _impl(std::make_unique<Impl>()) , _domain(std::move(domain)) , _selector(std::move(selector)) , _keyPath(std::move(keyPath)) {} , _keyPath(std::move(keyPath)) { try { _impl->key = netplus::rsa(_keyPath); _impl->keyLoaded = true; } catch (const std::exception &) { _impl->keyLoaded = false; } } DkimAdapter::~DkimAdapter() = default; std::string DkimAdapter::sign(const std::string &rawMessage) { if (!_impl->lib) return ""; DKIM_STAT stat; DKIM *dkim = dkim_sign(_impl->lib, reinterpret_cast<const unsigned char *>("proxyplus"), nullptr, reinterpret_cast<const unsigned char *>(_keyPath.c_str()), reinterpret_cast<const unsigned char *>(_selector.c_str()), reinterpret_cast<const unsigned char *>(_domain.c_str()), DKIM_CANON_RELAXED, DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256, -1, &stat); if (!dkim) return ""; dkim_chunk(dkim, reinterpret_cast<unsigned char *>(const_cast<char *>(rawMessage.data())), rawMessage.size()); dkim_eom(dkim, nullptr); unsigned char hdrBuf[1024] = {0}; std::string result; if (dkim_getsighdr(dkim, hdrBuf, sizeof(hdrBuf), 0) == DKIM_STAT_OK) { result = "DKIM-Signature: " + std::string(reinterpret_cast<char *>(hdrBuf)) + "\r\n"; } dkim_free(dkim); return result; if (!_impl->keyLoaded) return ""; std::vector<HeaderField> headers; std::string body; splitMessage(rawMessage, headers, body); if (!findHeader(headers, "From")) return ""; // nothing meaningful to sign std::vector<std::string> order; for (const auto *name : kSignedHeaders) { if (findHeader(headers, name)) order.push_back(name); } std::string canonBodyStr = canonBodyRelaxed(body); std::string bh = base64Encode(netplus::sha256_hash(toBytes(canonBodyStr))); std::string hList; for (size_t i = 0; i < order.size(); ++i) { if (i) hList += ":"; hList += toLower(order[i]); } std::string tagsUpToB = " v=1; a=rsa-sha256; c=relaxed/relaxed; d=" + _domain + "; s=" + _selector + "; h=" + hList + "; bh=" + bh + "; b="; std::string signingInput = buildSigningInput(headers, order, "relaxed", "DKIM-Signature", tagsUpToB); std::vector<uint8_t> sig = netplus::rsa_pkcs1_sha256::sign(_impl->key, toBytes(signingInput)); if (sig.empty()) return ""; return "DKIM-Signature:" + tagsUpToB + base64Encode(sig) + "\r\n"; } DkimVerifyResult DkimAdapter::verify(const std::string &rawMessage) { DkimVerifyResult result; if (!_impl->lib) return result; DKIM_STAT stat; DKIM *dkim = dkim_verify(_impl->lib, reinterpret_cast<const unsigned char *>("proxyplus"), nullptr, &stat); if (!dkim) return result; dkim_chunk(dkim, reinterpret_cast<unsigned char *>(const_cast<char *>(rawMessage.data())), rawMessage.size()); DKIM_STAT eomStat = dkim_eom(dkim, nullptr); std::vector<HeaderField> headers; std::string body; splitMessage(rawMessage, headers, body); const HeaderField *sigHeader = findHeader(headers, "DKIM-Signature"); if (!sigHeader) return result; // present stays false result.present = true; result.valid = (eomStat == DKIM_STAT_OK); dkim_free(dkim); return result; std::string unfoldedSigValue; for (size_t i = 0; i < sigHeader->value.size();) { if (sigHeader->value.compare(i, 2, "\r\n") == 0) i += 2; else unfoldedSigValue += sigHeader->value[i++]; } auto tags = parseTagValueList(unfoldedSigValue); #else // !PROXYPLUS_WITH_OPENDKIM auto get = [&](const char *tag) -> std::string { auto it = tags.find(tag); return it == tags.end() ? "" : it->second; }; struct DkimAdapter::Impl {}; if (get("v") != "1" || get("a") != "rsa-sha256") return result; // unsupported algorithm DkimAdapter::DkimAdapter(std::string domain, std::string selector, std::string keyPath) : _impl(std::make_unique<Impl>()) , _domain(std::move(domain)) , _selector(std::move(selector)) , _keyPath(std::move(keyPath)) {} result.signingDomain = get("d"); result.selector = get("s"); if (result.signingDomain.empty() || result.selector.empty()) return result; DkimAdapter::~DkimAdapter() = default; std::string canon = get("c"); size_t slash = canon.find('/'); std::string headerAlgo = slash == std::string::npos ? canon : canon.substr(0, slash); std::string bodyAlgo = slash == std::string::npos ? canon : canon.substr(slash + 1); if (headerAlgo.empty()) headerAlgo = "simple"; if (bodyAlgo.empty()) bodyAlgo = "simple"; std::string bh = get("bh"); std::string bSig = get("b"); if (bh.empty() || bSig.empty()) return result; std::string actualBh = base64Encode(netplus::sha256_hash(toBytes(canonBody(body, bodyAlgo)))); if (actualBh != bh) return result; // body was altered in transit std::string DkimAdapter::sign(const std::string & /*rawMessage*/) { return ""; // libopendkim-dev not present at build time, see header comment std::vector<std::string> order; std::string hList = get("h"); size_t start = 0; while (start <= hList.size()) { size_t colon = hList.find(':', start); std::string name = hList.substr(start, colon == std::string::npos ? std::string::npos : colon - start); // RFC 6376 3.5 permits WSP around the colons separating h= entries. size_t nb = name.find_first_not_of(" \t"); size_t ne = name.find_last_not_of(" \t"); if (nb != std::string::npos) order.push_back(name.substr(nb, ne - nb + 1)); if (colon == std::string::npos) break; start = colon + 1; } if (order.empty()) return result; // The signature covers the DKIM-Signature header with its own value // truncated right after "b=" -- rebuild that exact substring from the // original header text. Scanned tag-boundary-aware (semicolon-delimited, // skipping leading FWS/fold markers) rather than a bare substring search, // so a "b=" occurring inside an earlier tag's value (e.g. i=) can't be // mistaken for the signature tag itself. size_t bValueEnd = std::string::npos; for (size_t pos = 0; pos <= sigHeader->value.size();) { size_t semi = sigHeader->value.find(';', pos); std::string segment = sigHeader->value.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos); size_t nb = segment.find_first_not_of(" \t\r\n"); if (nb != std::string::npos && segment.compare(nb, 2, "b=") == 0) { bValueEnd = pos + nb + 2; break; } if (semi == std::string::npos) break; pos = semi + 1; } if (bValueEnd == std::string::npos) return result; std::string valueUpToB = sigHeader->value.substr(0, bValueEnd); DkimVerifyResult DkimAdapter::verify(const std::string & /*rawMessage*/) { return {}; } std::string signingInput = buildSigningInput(headers, order, headerAlgo, sigHeader->name, valueUpToB); #endif std::vector<uint8_t> sigBytes = base64Decode(bSig); if (sigBytes.empty()) return result; std::string txtName = result.selector + "._domainkey." + result.signingDomain; std::vector<std::string> txtRecords = queryTxtRecords(txtName); if (txtRecords.empty()) return result; for (const auto &record : txtRecords) { auto keyTags = parseTagValueList(record); auto p = keyTags.find("p"); if (p == keyTags.end() || p->second.empty()) continue; std::vector<uint8_t> pubDer = base64Decode(p->second); if (pubDer.empty()) continue; try { netplus::rsa pubKey(pubDer); if (netplus::rsa_pkcs1_sha256::verify(pubKey, toBytes(signingInput), sigBytes)) { result.valid = true; return result; } } catch (const std::exception &) { continue; } } return result; } } // namespace proxyplus::security src/security/DkimAdapter.h +5 −7 Changes for src/security/DkimAdapter.h: 5 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -12,13 +12,11 @@ struct DkimVerifyResult { std::string selector; }; // Thin wrapper around OpenDKIM (libopendkim, dkim.h). NOT compiled in this environment -- // libopendkim-dev is not installed here (see docs/ARCHITECTURE.md §8). The interface and // the guarded implementation in DkimAdapter.cpp are written against OpenDKIM's real, // documented C API (dkim_init/dkim_sign/dkim_verify/dkim_chunk/dkim_eom/dkim_getsighdr), // but that code path has not been exercised by a compiler here. CMake only builds it when // PROXYPLUS_WITH_OPENDKIM is set (auto-detected via find_library), and otherwise links a // no-op stub so the rest of ProxyPlus builds regardless. // Own RFC 6376 (DKIM) implementation on top of libnetplus's RSA/SHA256/base64 // primitives (see DkimAdapter.cpp) -- no external DKIM library dependency. // Signs/verifies rsa-sha256 with relaxed or simple header/body // canonicalization; h= for signing is a fixed candidate header set. verify() // fetches the signer's public key itself via DnsTxtResolver. class DkimAdapter { public: // keyPath: PEM private key used for signing; selector/domain identify the DKIM record. Loading Loading
CMakeLists.txt +6 −28 Changes for CMakeLists.txt: 6 added lines, 28 removed lines. Original line number Diff line number Diff line Loading @@ -24,23 +24,11 @@ find_package(json-c REQUIRED) find_package(Threads REQUIRED) # --- Optional: OpenDKIM / OpenDMARC (see docs/ARCHITECTURE.md §8) --- find_library(OPENDKIM_LIB NAMES opendkim) find_path(OPENDKIM_INCLUDE_DIR NAMES dkim.h PATH_SUFFIXES opendkim) find_library(OPENDMARC_LIB NAMES opendmarc) find_path(OPENDMARC_INCLUDE_DIR NAMES dmarc.h PATH_SUFFIXES opendmarc) if(OPENDKIM_LIB AND OPENDKIM_INCLUDE_DIR) message(STATUS "proxyplus: OpenDKIM found, building real DkimAdapter") else() message(WARNING "proxyplus: libopendkim-dev not found -- DkimAdapter will be a no-op stub") endif() if(OPENDMARC_LIB AND OPENDMARC_INCLUDE_DIR) message(STATUS "proxyplus: OpenDMARC found, building real DmarcAdapter") else() message(WARNING "proxyplus: libopendmarc-dev not found -- DmarcAdapter will be a no-op stub") endif() # --- DKIM / DMARC (see docs/ARCHITECTURE.md §8) --- # Own RFC 6376 / RFC 7489 implementation on top of libnetplus's RSA/SHA256/ # base64 primitives and a small hand-rolled DNS TXT resolver -- no dependency # on libopendkim/libopendmarc (which had cross-platform header-layout issues # and a C++/C99-_Bool incompatibility not worth carrying). set(PROXYPLUS_CORE_SOURCES src/config/BootstrapConfig.cpp src/auth/AuthDBAdapter.cpp Loading @@ -50,6 +38,7 @@ set(PROXYPLUS_CORE_SOURCES src/http/HttpProxyServer.cpp src/smtp/SmtpProxyServer.cpp src/api/JsonApiServer.cpp src/security/DnsTxtResolver.cpp src/security/DkimAdapter.cpp src/security/DmarcAdapter.cpp src/tls/CertificateManager.cpp Loading @@ -74,17 +63,6 @@ target_link_libraries(proxyplus_core PUBLIC ${CMAKE_DL_LIBS} ) if(OPENDKIM_LIB AND OPENDKIM_INCLUDE_DIR) target_compile_definitions(proxyplus_core PRIVATE PROXYPLUS_WITH_OPENDKIM) target_include_directories(proxyplus_core PRIVATE ${OPENDKIM_INCLUDE_DIR}) target_link_libraries(proxyplus_core PRIVATE ${OPENDKIM_LIB}) endif() if(OPENDMARC_LIB AND OPENDMARC_INCLUDE_DIR) target_compile_definitions(proxyplus_core PRIVATE PROXYPLUS_WITH_OPENDMARC) target_include_directories(proxyplus_core PRIVATE ${OPENDMARC_INCLUDE_DIR}) target_link_libraries(proxyplus_core PRIVATE ${OPENDMARC_LIB}) endif() add_executable(proxyplus src/main.cpp) target_link_libraries(proxyplus PRIVATE proxyplus_core) target_compile_definitions(proxyplus PRIVATE Loading
README.md +9 −6 Changes for README.md: 9 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ code against verified library APIs, not stubs pretending to be complete. Requires (all found via `find_package`, all already packaged on this system): `libconfplus-dev`, `libauthorize-dev` (ships `authdb`'s client headers), `libhttppp-dev`, `libsmtppp-dev`, `libparitypp-dev`, `libnetplus-dev`, `libcmdplus-dev`, `libuuidplus-dev`, `libjson-c-dev`. Optional: `libopendkim-dev`, `libopendmarc-dev` (see below). `libjson-c-dev`. ```sh mkdir build && cd build Loading Loading @@ -45,12 +45,15 @@ sudo make install # or rebuild the .deb via the usual debian/ packaging and re This was verified: relinking `proxyplus` against a freshly built `libsmtppp.so` from that tree succeeds with zero changes to ProxyPlus's own code. ### Optional: OpenDKIM / OpenDMARC ### DKIM / DMARC Neither `libopendkim-dev` nor `libopendmarc-dev` is installed in this environment. `src/security/DkimAdapter.cpp` / `DmarcAdapter.cpp` are written against the real upstream C APIs but compile to no-op stubs (`PROXYPLUS_WITH_OPENDKIM`/`PROXYPLUS_WITH_OPENDMARC` unset) until those packages are present -- `cmake` auto-detects and reports which mode is active. `src/security/DkimAdapter.cpp` / `DmarcAdapter.cpp` are ProxyPlus's own RFC 6376 / RFC 7489 implementation on top of `libnetplus`'s RSA/SHA256/base64 primitives and a small hand-rolled DNS TXT resolver (`DnsTxtResolver.cpp`) -- no `libopendkim`/`libopendmarc` dependency, so there is nothing extra to install. See the scope notes at the top of `DmarcAdapter.cpp` and docs/ARCHITECTURE.md §8 for what's simplified relative to the full RFCs (no organizational-domain fallback, relaxed alignment approximated without a Public Suffix List, `pct=` parsed but not sampled against). ## First-time setup Loading
docs/ARCHITECTURE.md +12 −6 Changes for docs/ARCHITECTURE.md: 12 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -253,7 +253,9 @@ proxyplus/ api/ JsonApiServer – json-c REST-API, Permission-Middleware tls/ AcmeChallengeStore, CertificateManager – ACME-Erneuerungs-Orchestrierung lb/ LoadBalancer – Pool-Zielauswahl + passive Gesundheitsprüfung (§6b) security/ DkimAdapter, DmarcAdapter – optionale OpenDKIM/OpenDMARC-Wrapper security/ DkimAdapter, DmarcAdapter, DnsTxtResolver – eigene RFC-6376/7489-Implementierung auf libnetplus-Krypto (RSA/SHA256/base64) + hand-geschriebenem DNS-TXT-Lookup, keine Abhängigkeit von libopendkim/libopendmarc (§8) main.cpp – Bootstrap: Config laden → Single/Cluster → Listener starten admin/static/ – AJAX-Admin-UI (statisches HTML/JS gegen JsonApiServer) plugins/example_dnsbl/ – Beispielplugin (SMTP-Connection-Check via DNSBL) Loading @@ -274,11 +276,15 @@ proxyplus/ selbst, nicht in ProxyPlus. - `libparitypp`-basierte Replikation ist Last-Writer-Wins ohne echten Konsens; für Split-Brain-Schutz bräuchte es eine Membership-/Write-Ordering-Schicht. - OpenDKIM-/OpenDMARC-Header sind auf diesem System nicht installiert (`libopendkim-dev`/`libopendmarc-dev` fehlen) — die Adapter sind gegen die reale Upstream-API geschrieben, aber in diesem Environment nicht kompilierbar getestet; das CMake-Build behandelt sie als optional (`find_library`, degradiert zu einem No-Op-Stub-Target, wenn nicht gefunden). - DkimAdapter/DmarcAdapter sind eine eigene RFC-6376/7489-Implementierung (kein libopendkim/libopendmarc mehr — deren Header hatten je nach System unterschiedliche Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), SPF-Alignment vertraut dem `spfPass`-Flag des Callers ohne eigenen Domain-Vergleich (das Interface bekommt keine separate SPF-authentifizierte Domain). - Gruppen-Anlage (`addGroup`) fehlt aktuell im `authdb::client`-API (nur `listGroups()` lesend, `GPOadd` schreibend) — `scripts/authdb-bootstrap.sh` dokumentiert das explizit als authdb-seitige Lücke statt sie zu umgehen.
src/security/DkimAdapter.cpp +353 −52 Changes for src/security/DkimAdapter.cpp: 353 added lines, 52 removed lines. Original line number Diff line number Diff line #include "DkimAdapter.h" #include "DnsTxtResolver.h" #include "TagValueParser.h" #ifdef PROXYPLUS_WITH_OPENDKIM #include <dkim.h> #endif #include <netplus/crypto/rsa.h> #include <netplus/crypto/rsa_pkcs1_sha256.h> #include <netplus/crypto/sha.h> #include <netplus/crypto/base64.h> #include <algorithm> #include <cctype> #include <vector> // Own DKIM (RFC 6376) implementation on top of libnetplus's RSA/SHA/base64 // primitives -- no dependency on libopendkim. Supports the rsa-sha256 // signing algorithm with relaxed or simple header/body canonicalization. // h= is a fixed candidate header set (see kSignedHeaders below); this covers // what most senders sign and is not meant to be user-configurable yet. namespace proxyplus::security { #ifdef PROXYPLUS_WITH_OPENDKIM namespace { struct DkimAdapter::Impl { DKIM_LIB *lib = nullptr; Impl() { lib = dkim_init(nullptr, nullptr); } ~Impl() { if (lib) dkim_close(lib); bool ieq(const std::string &a, const std::string &b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i]))) return false; } return true; } std::string toLower(const std::string &s) { std::string out = s; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return std::tolower(c); }); return out; } std::vector<uint8_t> toBytes(const std::string &s) { return std::vector<uint8_t>(s.begin(), s.end()); } std::string base64Encode(const std::vector<uint8_t> &data) { netplus::base64 enc; enc << std::vector<char>(data.begin(), data.end()); const auto &encoded = enc.encoded(); return std::string(encoded.begin(), encoded.end()); } std::vector<uint8_t> base64Decode(const std::string &b64) { netplus::base64 dec; dec.setEncodedData(b64); std::vector<char> out; dec >> out; return std::vector<uint8_t>(out.begin(), out.end()); } struct HeaderField { std::string name; // exact original case, e.g. "DKIM-Signature" std::string value; // substring after the first ':', folding preserved as embedded "\r\n" std::string fullOriginal; // "Name" + ":" + value, exact original bytes, no trailing CRLF }; // Splits an RFC 5322 message into its header fields (folding preserved) and // body. The header/body boundary is the first blank line; either bare LF or // CRLF line endings are accepted throughout. void splitMessage(const std::string &raw, std::vector<HeaderField> &headers, std::string &body) { size_t boundary = std::string::npos; size_t sepLen = 0; size_t p1 = raw.find("\r\n\r\n"); size_t p2 = raw.find("\n\n"); if (p1 != std::string::npos && (p2 == std::string::npos || p1 <= p2)) { boundary = p1; sepLen = 4; } else if (p2 != std::string::npos) { boundary = p2; sepLen = 2; } std::string headerBlock = (boundary == std::string::npos) ? raw : raw.substr(0, boundary); body = (boundary == std::string::npos) ? "" : raw.substr(boundary + sepLen); std::vector<std::string> lines; size_t pos = 0; while (pos <= headerBlock.size()) { size_t nl = headerBlock.find('\n', pos); std::string line = headerBlock.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); if (!line.empty() && line.back() == '\r') line.pop_back(); lines.push_back(line); if (nl == std::string::npos) break; pos = nl + 1; } for (const auto &line : lines) { if (line.empty()) continue; bool isContinuation = (line[0] == ' ' || line[0] == '\t'); if (isContinuation && !headers.empty()) { headers.back().value += "\r\n" + line; headers.back().fullOriginal += "\r\n" + line; } else { size_t colon = line.find(':'); if (colon == std::string::npos) continue; // malformed, skip headers.push_back({line.substr(0, colon), line.substr(colon + 1), line}); } } } // RFC 6376 3.4.2 relaxed header canonicalization. std::string canonHeaderRelaxed(const std::string &name, const std::string &value) { std::string unfolded; unfolded.reserve(value.size()); for (size_t i = 0; i < value.size();) { if (value.compare(i, 2, "\r\n") == 0) { i += 2; // delete the fold itself, keep whatever WSP follows } else { unfolded += value[i++]; } } std::string collapsed; bool inWs = false; for (char c : unfolded) { if (c == ' ' || c == '\t') { if (!inWs) { collapsed += ' '; inWs = true; } } else { collapsed += c; inWs = false; } } size_t b = collapsed.find_first_not_of(' '); size_t e = collapsed.find_last_not_of(' '); std::string trimmed = (b == std::string::npos) ? "" : collapsed.substr(b, e - b + 1); return toLower(name) + ":" + trimmed; } // RFC 6376 3.4.4 relaxed body canonicalization. std::string canonBodyRelaxed(const std::string &body) { std::vector<std::string> lines; size_t pos = 0; while (pos <= body.size()) { size_t nl = body.find('\n', pos); std::string line = body.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); if (!line.empty() && line.back() == '\r') line.pop_back(); std::string collapsed; bool inWs = false; for (char c : line) { if (c == ' ' || c == '\t') { if (!inWs) { collapsed += ' '; inWs = true; } } else { collapsed += c; inWs = false; } } if (!collapsed.empty() && collapsed.back() == ' ') collapsed.pop_back(); lines.push_back(collapsed); if (nl == std::string::npos) break; pos = nl + 1; } while (!lines.empty() && lines.back().empty()) lines.pop_back(); if (lines.empty()) return ""; std::string out; for (const auto &l : lines) { out += l; out += "\r\n"; } return out; } // RFC 6376 3.4.3 simple body canonicalization. std::string canonBodySimple(const std::string &body) { std::string s = body; while (s.size() >= 2 && s.compare(s.size() - 2, 2, "\r\n") == 0) s.resize(s.size() - 2); return s.empty() ? "\r\n" : s + "\r\n"; } std::string canonBody(const std::string &body, const std::string &algo) { return ieq(algo, "simple") ? canonBodySimple(body) : canonBodyRelaxed(body); } // The candidate set of header fields we sign, and (for verification) will // look up by name when reconstructing the signing input from an h= tag. const char *const kSignedHeaders[] = {"From", "To", "Subject", "Date", "Message-ID"}; const HeaderField *findHeader(const std::vector<HeaderField> &headers, const std::string &name) { for (const auto &h : headers) { if (ieq(h.name, name)) return &h; } return nullptr; } // Builds the exact bytes that get RSA-signed/verified: each signed header, // canonicalized and CRLF-terminated, in `order`, followed by the // DKIM-Signature header itself (canonicalized, value truncated right after // "b=", and with NO trailing CRLF -- RFC 6376 3.7). std::string buildSigningInput(const std::vector<HeaderField> &headers, const std::vector<std::string> &order, const std::string &headerAlgo, const std::string &dkimSigName, const std::string &dkimSigValueUpToB) { std::string input; for (const auto &name : order) { const HeaderField *h = findHeader(headers, name); if (!h) continue; if (ieq(headerAlgo, "simple")) { input += h->fullOriginal + "\r\n"; } else { input += canonHeaderRelaxed(h->name, h->value) + "\r\n"; } } if (ieq(headerAlgo, "simple")) { input += dkimSigName + ":" + dkimSigValueUpToB; } else { input += canonHeaderRelaxed(dkimSigName, dkimSigValueUpToB); } return input; } } // namespace struct DkimAdapter::Impl { netplus::rsa key; bool keyLoaded = false; }; DkimAdapter::DkimAdapter(std::string domain, std::string selector, std::string keyPath) : _impl(std::make_unique<Impl>()) , _domain(std::move(domain)) , _selector(std::move(selector)) , _keyPath(std::move(keyPath)) {} , _keyPath(std::move(keyPath)) { try { _impl->key = netplus::rsa(_keyPath); _impl->keyLoaded = true; } catch (const std::exception &) { _impl->keyLoaded = false; } } DkimAdapter::~DkimAdapter() = default; std::string DkimAdapter::sign(const std::string &rawMessage) { if (!_impl->lib) return ""; DKIM_STAT stat; DKIM *dkim = dkim_sign(_impl->lib, reinterpret_cast<const unsigned char *>("proxyplus"), nullptr, reinterpret_cast<const unsigned char *>(_keyPath.c_str()), reinterpret_cast<const unsigned char *>(_selector.c_str()), reinterpret_cast<const unsigned char *>(_domain.c_str()), DKIM_CANON_RELAXED, DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256, -1, &stat); if (!dkim) return ""; dkim_chunk(dkim, reinterpret_cast<unsigned char *>(const_cast<char *>(rawMessage.data())), rawMessage.size()); dkim_eom(dkim, nullptr); unsigned char hdrBuf[1024] = {0}; std::string result; if (dkim_getsighdr(dkim, hdrBuf, sizeof(hdrBuf), 0) == DKIM_STAT_OK) { result = "DKIM-Signature: " + std::string(reinterpret_cast<char *>(hdrBuf)) + "\r\n"; } dkim_free(dkim); return result; if (!_impl->keyLoaded) return ""; std::vector<HeaderField> headers; std::string body; splitMessage(rawMessage, headers, body); if (!findHeader(headers, "From")) return ""; // nothing meaningful to sign std::vector<std::string> order; for (const auto *name : kSignedHeaders) { if (findHeader(headers, name)) order.push_back(name); } std::string canonBodyStr = canonBodyRelaxed(body); std::string bh = base64Encode(netplus::sha256_hash(toBytes(canonBodyStr))); std::string hList; for (size_t i = 0; i < order.size(); ++i) { if (i) hList += ":"; hList += toLower(order[i]); } std::string tagsUpToB = " v=1; a=rsa-sha256; c=relaxed/relaxed; d=" + _domain + "; s=" + _selector + "; h=" + hList + "; bh=" + bh + "; b="; std::string signingInput = buildSigningInput(headers, order, "relaxed", "DKIM-Signature", tagsUpToB); std::vector<uint8_t> sig = netplus::rsa_pkcs1_sha256::sign(_impl->key, toBytes(signingInput)); if (sig.empty()) return ""; return "DKIM-Signature:" + tagsUpToB + base64Encode(sig) + "\r\n"; } DkimVerifyResult DkimAdapter::verify(const std::string &rawMessage) { DkimVerifyResult result; if (!_impl->lib) return result; DKIM_STAT stat; DKIM *dkim = dkim_verify(_impl->lib, reinterpret_cast<const unsigned char *>("proxyplus"), nullptr, &stat); if (!dkim) return result; dkim_chunk(dkim, reinterpret_cast<unsigned char *>(const_cast<char *>(rawMessage.data())), rawMessage.size()); DKIM_STAT eomStat = dkim_eom(dkim, nullptr); std::vector<HeaderField> headers; std::string body; splitMessage(rawMessage, headers, body); const HeaderField *sigHeader = findHeader(headers, "DKIM-Signature"); if (!sigHeader) return result; // present stays false result.present = true; result.valid = (eomStat == DKIM_STAT_OK); dkim_free(dkim); return result; std::string unfoldedSigValue; for (size_t i = 0; i < sigHeader->value.size();) { if (sigHeader->value.compare(i, 2, "\r\n") == 0) i += 2; else unfoldedSigValue += sigHeader->value[i++]; } auto tags = parseTagValueList(unfoldedSigValue); #else // !PROXYPLUS_WITH_OPENDKIM auto get = [&](const char *tag) -> std::string { auto it = tags.find(tag); return it == tags.end() ? "" : it->second; }; struct DkimAdapter::Impl {}; if (get("v") != "1" || get("a") != "rsa-sha256") return result; // unsupported algorithm DkimAdapter::DkimAdapter(std::string domain, std::string selector, std::string keyPath) : _impl(std::make_unique<Impl>()) , _domain(std::move(domain)) , _selector(std::move(selector)) , _keyPath(std::move(keyPath)) {} result.signingDomain = get("d"); result.selector = get("s"); if (result.signingDomain.empty() || result.selector.empty()) return result; DkimAdapter::~DkimAdapter() = default; std::string canon = get("c"); size_t slash = canon.find('/'); std::string headerAlgo = slash == std::string::npos ? canon : canon.substr(0, slash); std::string bodyAlgo = slash == std::string::npos ? canon : canon.substr(slash + 1); if (headerAlgo.empty()) headerAlgo = "simple"; if (bodyAlgo.empty()) bodyAlgo = "simple"; std::string bh = get("bh"); std::string bSig = get("b"); if (bh.empty() || bSig.empty()) return result; std::string actualBh = base64Encode(netplus::sha256_hash(toBytes(canonBody(body, bodyAlgo)))); if (actualBh != bh) return result; // body was altered in transit std::string DkimAdapter::sign(const std::string & /*rawMessage*/) { return ""; // libopendkim-dev not present at build time, see header comment std::vector<std::string> order; std::string hList = get("h"); size_t start = 0; while (start <= hList.size()) { size_t colon = hList.find(':', start); std::string name = hList.substr(start, colon == std::string::npos ? std::string::npos : colon - start); // RFC 6376 3.5 permits WSP around the colons separating h= entries. size_t nb = name.find_first_not_of(" \t"); size_t ne = name.find_last_not_of(" \t"); if (nb != std::string::npos) order.push_back(name.substr(nb, ne - nb + 1)); if (colon == std::string::npos) break; start = colon + 1; } if (order.empty()) return result; // The signature covers the DKIM-Signature header with its own value // truncated right after "b=" -- rebuild that exact substring from the // original header text. Scanned tag-boundary-aware (semicolon-delimited, // skipping leading FWS/fold markers) rather than a bare substring search, // so a "b=" occurring inside an earlier tag's value (e.g. i=) can't be // mistaken for the signature tag itself. size_t bValueEnd = std::string::npos; for (size_t pos = 0; pos <= sigHeader->value.size();) { size_t semi = sigHeader->value.find(';', pos); std::string segment = sigHeader->value.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos); size_t nb = segment.find_first_not_of(" \t\r\n"); if (nb != std::string::npos && segment.compare(nb, 2, "b=") == 0) { bValueEnd = pos + nb + 2; break; } if (semi == std::string::npos) break; pos = semi + 1; } if (bValueEnd == std::string::npos) return result; std::string valueUpToB = sigHeader->value.substr(0, bValueEnd); DkimVerifyResult DkimAdapter::verify(const std::string & /*rawMessage*/) { return {}; } std::string signingInput = buildSigningInput(headers, order, headerAlgo, sigHeader->name, valueUpToB); #endif std::vector<uint8_t> sigBytes = base64Decode(bSig); if (sigBytes.empty()) return result; std::string txtName = result.selector + "._domainkey." + result.signingDomain; std::vector<std::string> txtRecords = queryTxtRecords(txtName); if (txtRecords.empty()) return result; for (const auto &record : txtRecords) { auto keyTags = parseTagValueList(record); auto p = keyTags.find("p"); if (p == keyTags.end() || p->second.empty()) continue; std::vector<uint8_t> pubDer = base64Decode(p->second); if (pubDer.empty()) continue; try { netplus::rsa pubKey(pubDer); if (netplus::rsa_pkcs1_sha256::verify(pubKey, toBytes(signingInput), sigBytes)) { result.valid = true; return result; } } catch (const std::exception &) { continue; } } return result; } } // namespace proxyplus::security
src/security/DkimAdapter.h +5 −7 Changes for src/security/DkimAdapter.h: 5 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -12,13 +12,11 @@ struct DkimVerifyResult { std::string selector; }; // Thin wrapper around OpenDKIM (libopendkim, dkim.h). NOT compiled in this environment -- // libopendkim-dev is not installed here (see docs/ARCHITECTURE.md §8). The interface and // the guarded implementation in DkimAdapter.cpp are written against OpenDKIM's real, // documented C API (dkim_init/dkim_sign/dkim_verify/dkim_chunk/dkim_eom/dkim_getsighdr), // but that code path has not been exercised by a compiler here. CMake only builds it when // PROXYPLUS_WITH_OPENDKIM is set (auto-detected via find_library), and otherwise links a // no-op stub so the rest of ProxyPlus builds regardless. // Own RFC 6376 (DKIM) implementation on top of libnetplus's RSA/SHA256/base64 // primitives (see DkimAdapter.cpp) -- no external DKIM library dependency. // Signs/verifies rsa-sha256 with relaxed or simple header/body // canonicalization; h= for signing is a fixed candidate header set. verify() // fetches the signer's public key itself via DnsTxtResolver. class DkimAdapter { public: // keyPath: PEM private key used for signing; selector/domain identify the DKIM record. Loading