Commit 411da8b5 authored by jan.koester's avatar jan.koester
Browse files

opendkim/opendmarc replaced own solution

parent 80c738ec
Loading
Loading
Loading
Loading
+6 −28
Changes for CMakeLists.txt: 6 added lines, 28 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
+9 −6
Changes for README.md: 9 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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

+12 −6
Changes for docs/ARCHITECTURE.md: 12 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -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)
@@ -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.
+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
+5 −7
Changes for src/security/DkimAdapter.h: 5 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -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