Commit 14230ff0 authored by jan.koester's avatar jan.koester
Browse files

test

parent f265acc9
Loading
Loading
Loading
Loading
Loading
+15 −0
Changes for data/dirviews.js: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -587,6 +587,7 @@ function loadRadiusClients() {
            if (err) { showError(dirErr(err)); return; }
            g_radiusClients = data.clients || [];
            var h = '<h3>RADIUS</h3>';
            h += radiusCertBox(data.cert || {});
            h += '<p class="hint">Switches, WLAN-Access-Points (WPA-Enterprise / 802.1X), VPN-Gateways und Captive Portals melden Benutzer per RADIUS an (UDP 1812, Accounting 1813). Verfahren: PAP, MS-CHAPv2, EAP-PEAP (MSCHAPv2), EAP-TTLS (PAP/MSCHAPv2).</p>';
            h += '<table class="admin-table userlist"><tr><th>Name</th><th>Adresse</th><th>Gruppen</th><th>VLANs</th><th>Gruppen-Attribut</th><th>Actions</th></tr>';
            for (var i = 0; i < g_radiusClients.length; i++) {
@@ -607,6 +608,20 @@ function loadRadiusClients() {
    }, true);
}

/* the certificate 802.1X clients have to trust */
function radiusCertBox(c) {
    if (c.error) return '<p class="hint" style="color:#b00">Kein Server-Zertifikat (' + escapeHtml(c.error) + '): PEAP und TTLS funktionieren so nicht, nur PAP und MS-CHAPv2. /RADIUS/CERT oder /SSL/CERT in der Konfiguration setzen.</p>';
    var h = '<p><b>Server-Zertifikat</b> für WLAN-Clients (PEAP/TTLS): <code>' + escapeHtml(c.subject) + '</code>';
    if (c.names && c.names.length) h += ', Namen: <code>' + escapeHtml(c.names.join(', ')) + '</code>';
    h += ', gültig bis ' + escapeHtml(new Date(c.not_after * 1000).toLocaleDateString()) + '<br>';
    h += 'SHA-1: <code>' + escapeHtml(c.sha1) + '</code><br>SHA-256: <code>' + escapeHtml(c.sha256) + '</code></p>';
    h += '<p><a class="button" href="' + dirApi("radiuscert", "der") + '">Zertifikat herunterladen (.cer, Windows)</a> ';
    h += '<a class="button" href="' + dirApi("radiuscert", "pem") + '">als .pem (Linux, Android, macOS)</a></p>';
    h += '<p class="hint">Auf den Clients als vertrauenswürdige Stammzertifizierungsstelle importieren und im WLAN-Profil auswählen. Den Fingerabdruck beim Import vergleichen.</p>';
    if (!c.server_auth) h += '<p class="hint" style="color:#b00">Dem Zertifikat fehlt die erweiterte Schlüsselverwendung &quot;Serverauthentifizierung&quot;, Windows-Clients können es ablehnen. Ein selbst erzeugtes Zertifikat bekommt sie neu, wenn cert- und key-Datei gelöscht werden und der Dienst neu startet.</p>';
    return h;
}

function editRadiusClient(i) {
    var c = i >= 0 ? g_radiusClients[i] : {name: '', address: '', groups: [], vlans: '', group_attr: 'none', require_ma: true};
    var h = '<h3>' + (i >= 0 ? escapeHtml(c.name) : 'Neuer RADIUS-Client') + '</h3>';
+1 −1
Changes for docs/ROADMAP.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -68,7 +68,7 @@ The directory layer maps them onto AD objects and adds generic objects:
| 8 | SMB2/3 server: IPC$ pipes, NETLOGON/SYSVOL shares, signing | |
| 9 | Packaging for FenrirOS (spec, syslite unit, container), admin UI for AD objects/DNS/LDIF | |
| 10 | OAuth 2.0 / OpenID Connect provider (plugin oidc) | Issuer per domain <base>/<domain>; discovery, JWKS (RS256), authorize (code + PKCE S256, login page, SSO cookie, prompt=none/login), token (authorization_code, refresh_token with rotation, client_credentials, device_code), device verification page, userinfo, introspection, revocation, end-session; clients with group restriction managed in the admin UI; state in OAuthObject records (replicated, not visible over LDAP/SMB); tested end to end with Authlib |
| 10 | RADIUS server (plugin radius) | UDP 1812/1813: PAP, MS-CHAPv2 with MPPE keys, EAP-PEAPv0/EAP-MSCHAPv2 and EAP-TTLS (PAP, MS-CHAPv2) for WPA-Enterprise/802.1X (in-memory TLS 1.2; TLS 1.3 offers are negotiated down, EAP-TLS/TLS 1.3 keying not planned), EAP fragmentation, Status-Server, accounting (logged); Message-Authenticator required by default (BlastRADIUS); RADIUS clients per domain (address/CIDR, shared secret, allowed groups, VLAN per group via Tunnel-*, groups as Filter-Id/Class) managed in the admin UI as RadiusObject records; config /RADIUS/BIND, AUTHPORT, ACCTPORT, CERT, KEY (default: the SSL certificate); tested with radtest, an independent MS-CHAPv2 client and eapol_test |
| 10 | RADIUS server (plugin radius) | UDP 1812/1813: PAP, MS-CHAPv2 with MPPE keys, EAP-PEAPv0/EAP-MSCHAPv2 and EAP-TTLS (PAP, MS-CHAPv2) for WPA-Enterprise/802.1X (in-memory TLS 1.2; TLS 1.3 offers are negotiated down, EAP-TLS/TLS 1.3 keying not planned), EAP fragmentation, Status-Server, accounting (logged); Message-Authenticator required by default (BlastRADIUS); RADIUS clients per domain (address/CIDR, shared secret, allowed groups, VLAN per group via Tunnel-*, groups as Filter-Id/Class) managed in the admin UI as RadiusObject records; config /RADIUS/BIND, AUTHPORT, ACCTPORT, CERT, KEY (default: the SSL certificate); server certificate (public part only, .cer/.pem) downloadable in the admin UI; tested with radtest, an independent MS-CHAPv2 client and eapol_test (also verifying the downloaded certificate) |

## Test bench

+59 −1
Changes for plugin/radius/radiusd.cpp: 59 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -121,6 +121,61 @@ namespace {
        return -1;
    }

    /* What the peer offers, for the log when a handshake fails: netplus
     * answers TLS 1.2 with P-256 and rsa_pkcs1_sha256 whatever was offered. */
    std::string describeHello(const Bytes &rec) {
        if (rec.size() < 9 || rec[0] != 0x16 || rec[5] != 0x01) return "no ClientHello";
        const uint8_t *m = rec.data() + 5;
        size_t n = std::min(rec.size() - 5, size_t(be16(&rec[3])));
        auto list16 = [](const uint8_t *d, size_t len) {
            std::string out;
            for (size_t i = 0; i + 1 < len; i += 2)
                out += (out.empty() ? "" : ",") + crypto::hex(Bytes(d + i, d + i + 2));
            return out;
        };
        size_t p = 4 + 2 + 32;
        if (p >= n) return "short ClientHello";
        std::string out = "sid=" + std::to_string(m[p]);
        p += 1 + m[p];
        if (p + 2 > n) return out;
        size_t cs = be16(m + p);
        if (p + 2 + cs > n) return out;
        out += " suites=" + list16(m + p + 2, cs);
        p += 2 + cs;
        if (p >= n) return out;
        p += 1 + m[p];
        if (p + 2 > n) return out;
        size_t end = std::min(n, p + 2 + be16(m + p));
        for (p += 2; p + 4 <= end;) {
            uint16_t t = be16(m + p), l = be16(m + p + 2);
            if (p + 4 + l > end) break;
            if (t == 0x000a && l >= 2) out += " groups=" + list16(m + p + 6, l - 2);
            else if (t == 0x000d && l >= 2) out += " sigalgs=" + list16(m + p + 6, l - 2);
            else if (t == 0x002b && l >= 1) out += " versions=" + list16(m + p + 5, l - 1);
            p += 4 + l;
        }
        return out;
    }

    /* netplus echoes the client's session id in the ServerHello of a full
     * handshake, which announces a resumption that does not happen. Give
     * the peer a fresh id of the same length instead, on the wire and in
     * the transcript. */
    void freshSessionId(netplus::tls &t, Bytes &out) {
        const size_t SID = 5 + 4 + 2 + 32 + 1;            /* behind the length byte */
        if (out.size() < SID || out[0] != 0x16 || out[5] != 0x02) return;
        size_t n = out[SID - 1];
        const Bytes &cl = t.client_session_id;
        if (!n || n != cl.size() || out.size() < SID + n || !std::equal(cl.begin(), cl.end(), out.begin() + SID)) return;
        auto &tr = t.handshake_transcript;
        if (tr.size() < 4) return;
        size_t at = 4 + ((size_t(tr[1]) << 16) | (size_t(tr[2]) << 8) | tr[3]) + 4 + 2 + 32 + 1;
        if (tr.size() < at + n || !std::equal(cl.begin(), cl.end(), tr.begin() + at)) return;
        Bytes id = crypto::randomBytes(n);
        std::copy(id.begin(), id.end(), out.begin() + SID);
        std::copy(id.begin(), id.end(), tr.begin() + at);
    }

    /* run the server handshake as far as the pushed bytes allow and
     * collect what it queued for the peer */
    void driveTls(netplus::tls &t, Bytes &out) {
@@ -238,6 +293,7 @@ struct RadiusServer::Session {
    std::string outer, inner;
    std::unique_ptr<netplus::tls> tls;
    long hidden = -1;
    std::string hello;
    Bytes in, out;
    size_t outOff = 0;
    size_t mtu = 1000;
@@ -740,19 +796,21 @@ Bytes RadiusServer::eapTls(const Packet &rq, const radius::Nas &nas, Session &s,
    if (s.phase == Session::HANDSHAKE) {
        if (tlsIn.empty()) return eapDone(rq, nas, s, false, "empty TLS message in handshake");
        bool hello = t.handshake_transcript.empty();
        if (hello) s.hello = describeHello(tlsIn);
        long hidden = hello ? hideTls13(tlsIn) : -1;
        Bytes out;
        try {
            t.pushReceivedData(tlsIn.data(), tlsIn.size());
            driveTls(t, out);
        } catch (const std::exception &ex) {
            return eapDone(rq, nas, s, false, std::string("TLS: ") + ex.what());
            return eapDone(rq, nas, s, false, std::string("TLS: ") + ex.what() + " (ClientHello: " + s.hello + ")");
        } catch (...) {
            return eapDone(rq, nas, s, false, "TLS handshake failed");
        }
        if (hidden >= 0 && size_t(hidden) + 1 < t.handshake_transcript.size() &&
            t.handshake_transcript[hidden] == 0xff && t.handshake_transcript[hidden + 1] == 0x2b)
            t.handshake_transcript[hidden] = 0x00;
        if (hello) freshSessionId(t, out);
        if (t.is_tls13) return eapDone(rq, nas, s, false, "TLS 1.3 negotiated");
        if (t.getHandshakeDone()) {
            s.phase = Session::TUNNEL;
+21 −0
Changes for src/admin.cpp: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -504,6 +504,23 @@ namespace fenrirds {
            json_object_put(jobj);
        }

        /* ---- the public part of the RADIUS server certificate, for the
         * trust store of 802.1X clients ---- */
        void apiRadiusCert(libhttppp::HttpRequest &curreq, const char *format) {
            bool pem = strcmp(format, "pem") == 0;
            std::string data, err;
            if (!dirapi::radiusCertFile(pem ? "pem" : "der", data, err)) {
                sendJsonError(curreq, err.c_str(), 404);
                return;
            }
            libhttppp::HttpResponse rep;
            rep.setState(HTTP200);
            rep.setContentType(pem ? "application/x-pem-file" : "application/x-x509-ca-cert");
            rep.setHeaderData("Content-Disposition")->push_back(
                pem ? "attachment; filename=\"fenrirds-radius.pem\"" : "attachment; filename=\"fenrirds-radius.cer\"");
            rep.send(curreq, data, data.size());
        }

        /* ---- JSON API: Remove Computer (enables a clean domain rejoin) ---- */
        void apiRemoveComputer(libhttppp::HttpRequest &curreq, const char *cdid, const char *cid) {
            std::string domain = domainNameFor(cdid);
@@ -2799,6 +2816,10 @@ namespace fenrirds {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiRemoveComputer(curreq, domain, param);
                    } else if (strcmp(apiurl, "radiuscert") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiRadiusCert(curreq, param);
                    } else if (dirapi::isDnsAction(apiurl) || dirapi::isWinGpoAction(apiurl) ||
                               dirapi::isWinScriptAction(apiurl) || dirapi::isOAuthAction(apiurl) ||
                               dirapi::isRadiusAction(apiurl)) {
+5 −1
Changes for src/confhelper.cpp: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <cstdio>
#include <iostream>
#include <fstream>
#include <sstream>
@@ -805,7 +806,8 @@ namespace fenrirds::confhelper {
#endif
    }

    bool generateCertificate(const std::string &cert, const std::string &key, std::string &err) {
    bool generateCertificate(const std::string &cert, const std::string &key, std::string &err,
                             const std::string &hostname) {
#ifndef _WIN32
        struct stat st;
        if (stat(cert.c_str(), &st) == 0 && stat(key.c_str(), &st) == 0)
@@ -827,6 +829,8 @@ namespace fenrirds::confhelper {
            return false;
        char host[256] = "fenrirds";
        gethostname(host, sizeof(host) - 1);
        if (!hostname.empty())
            snprintf(host, sizeof(host), "%s", hostname.c_str());
        selfcert::Result sc;
        try {
            sc = selfcert::create(host);
Loading