Commit 6fbfcc30 authored by jan.koester's avatar jan.koester
Browse files

test

parent 14230ff0
Loading
Loading
Loading
Loading
Loading
+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); 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) |
| 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 and 1.3, keying per RFC 9427), 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

+58 −43
Changes for plugin/radius/radiusd.cpp: 58 added lines, 43 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@
#include <netinet/in.h>

#include <json-c/json.h>
#include <netplus/crypto/sha.h>
#include <netplus/exception.h>

#include "crypto/crypto.h"
@@ -94,35 +95,7 @@ namespace {
        while (b.size() % 4) b.push_back(0);
    }

    /* Hide TLS 1.3 from the handshake: EAP keying for it (RFC 9427) is not
     * implemented, so the supported_versions extension of the ClientHello
     * is renamed to an unknown type. The original byte goes back into the
     * transcript afterwards (see restoreTranscript), so both Finished
     * messages still cover what the client really sent. Returns the offset
     * of the renamed type within the handshake message, or -1. */
    long hideTls13(Bytes &rec) {
        if (rec.size() < 9 || rec[0] != 0x16 || rec[5] != 0x01) return -1;
        const uint8_t *m = rec.data() + 5;
        size_t n = std::min(rec.size() - 5, size_t(be16(&rec[3])));
        size_t p = 4 + 2 + 32;
        if (p >= n) return -1;
        p += 1 + m[p];                                    /* session id */
        if (p + 2 > n) return -1;
        p += 2 + be16(m + p);                             /* cipher suites */
        if (p >= n) return -1;
        p += 1 + m[p];                                    /* compression */
        if (p + 2 > n) return -1;
        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 (t == 0x002b) { rec[5 + p] = 0xff; return long(p); }
            p += 4 + l;
        }
        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. */
    /* What the peer offers, for the log when a handshake fails */
    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;
@@ -176,6 +149,42 @@ namespace {
        std::copy(id.begin(), id.end(), tr.begin() + at);
    }

    /* TLS-Exporter (RFC 8446 7.5). netplus does not keep the exporter
     * secret, all it takes is at hand: the master secret follows from the
     * handshake secret, "exp master" covers ClientHello..server Finished,
     * the transcript without the peer's Finished at its end. */
    Bytes tlsExporter(netplus::tls &t, const std::string &label, const Bytes &context, size_t len) {
        uint16_t suite = t.chosenSuite;
        bool sha384 = suite == 0x1302;
        size_t hl = sha384 ? 48 : 32;
        auto hash = [&](const Bytes &b) { return sha384 ? netplus::sha384_hash(b) : netplus::sha256_hash(b); };
        Bytes none = hash({});
        Bytes derived = netplus::tls::hkdf_expand_label(t.tls13_hs_secret, "derived", none, hl, suite);
        Bytes master = netplus::tls::hkdf_extract(derived, Bytes(hl, 0), suite);
        Bytes tr = t.handshake_transcript;
        if (tr.size() < 4 + hl) return {};
        tr.resize(tr.size() - 4 - hl);
        Bytes exporter = netplus::tls::hkdf_expand_label(master, "exp master", hash(tr), hl, suite);
        Bytes secret = netplus::tls::hkdf_expand_label(exporter, label, none, hl, suite);
        return netplus::tls::hkdf_expand_label(secret, "exporter", hash(context), len, suite);
    }

    /* MSK, RFC 5216 2.3 / RFC 5281 8 with TLS 1.2, RFC 9427 2.1 with 1.3
     * (the label is the same for all methods, the EAP type the context;
     * the length enters the exporter, so ask for all 128 octets) */
    Bytes eapMsk(netplus::tls &t, uint8_t method) {
        if (t.is_tls13)
            return sub(tlsExporter(t, "EXPORTER_EAP_TLS_Key_Material", {method}, 128), 0, 64);
        return t.prf(t.masterSecret, method == EAP_TTLS ? "ttls keying material" : "client EAP encryption",
                     crypto::concat(t.clientRandom, t.serverRandom), 64);
    }

    /* the implicit MS-CHAPv2 challenge of TTLS (RFC 5281 11.2.4, RFC 9427 2.4) */
    Bytes ttlsChallenge(netplus::tls &t) {
        if (t.is_tls13) return tlsExporter(t, "ttls challenge", {}, 17);
        return t.prf(t.masterSecret, "ttls challenge", crypto::concat(t.clientRandom, t.serverRandom), 17);
    }

    /* 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) {
@@ -226,6 +235,17 @@ namespace {
            }
            if (rec.size() < 5) return true;
            uint16_t ver = be16(&rec[1]);
            if (t.is_tls13) {
                if (rec[0] != 0x17) return false;
                Bytes p;
                uint8_t inner = 0;
                try {
                    t.tls13_recv_record(rec[0], ver, rec.data() + 5, rec.size() - 5, p, inner, false);
                } catch (...) { return false; }
                if (inner == 0x15) return false;
                if (inner == 0x17) plain.insert(plain.end(), p.begin(), p.end());
                continue;
            }
            Bytes frag(rec.begin() + 5, rec.end());
            Bytes p;
            try {
@@ -292,7 +312,6 @@ struct RadiusServer::Session {
    Phase phase = START;
    std::string outer, inner;
    std::unique_ptr<netplus::tls> tls;
    long hidden = -1;
    std::string hello;
    Bytes in, out;
    size_t outOff = 0;
@@ -692,9 +711,8 @@ Bytes RadiusServer::eapDone(const Packet &rq, const radius::Nas &nas, Session &s
        return reject(rq, nas, (s.inner.empty() ? s.outer : s.inner) + ": " + reason, ex);
    }
    ex.addSplit(radius::EAP_MESSAGE, eapPacket(EAP_SUCCESS, s.respId, {}));
    /* MSK (RFC 5216 2.3, PEAPv0 and TTLS (RFC 5281 8) only differ in the label) */
    Bytes seed = crypto::concat(s.tls->clientRandom, s.tls->serverRandom);
    Bytes msk = s.tls->prf(s.tls->masterSecret, s.method == EAP_TTLS ? "ttls keying material" : "client EAP encryption", seed, 64);
    Bytes msk = eapMsk(*s.tls, s.method);
    if (msk.size() != 64) return eapDone(rq, nas, s, false, "no keying material");
    ex.addVsa(radius::VENDOR_MICROSOFT, radius::MS_MPPE_RECV_KEY, radius::encodeMppeKey(sub(msk, 0, 32), rq.auth, nas.secret));
    ex.addVsa(radius::VENDOR_MICROSOFT, radius::MS_MPPE_SEND_KEY, radius::encodeMppeKey(sub(msk, 32, 32), rq.auth, nas.secret));
    return accept(rq, nas, s.user, ex);
@@ -797,7 +815,6 @@ Bytes RadiusServer::eapTls(const Packet &rq, const radius::Nas &nas, Session &s,
        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());
@@ -807,17 +824,15 @@ Bytes RadiusServer::eapTls(const Packet &rq, const radius::Nas &nas, Session &s,
        } 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 (hello && !t.is_tls13) freshSessionId(t, out);        /* TLS 1.3 wants the echo */
        if (t.getHandshakeDone()) {
            s.phase = Session::TUNNEL;
            /* TTLS peers may already have sent their AVPs */
            Bytes plain;
            if (!readApp(t, plain)) return eapDone(rq, nas, s, false, "bad TLS record");
            if (out.empty() && !plain.empty()) return tunnel(rq, nas, s, plain);
            /* TLS 1.3 ends with the peer's Finished: nothing to acknowledge,
             * phase 2 starts right away (RFC 9427 3) */
            if (out.empty()) return tunnel(rq, nas, s, plain);
        }
        if (out.empty()) return eapDone(rq, nas, s, false, "TLS handshake stalled in state " + std::to_string(int(t.hs_state)));
        s.out = std::move(out);
@@ -843,7 +858,8 @@ Bytes RadiusServer::tunnel(const Packet &rq, const radius::Nas &nas, Session &s,
/* encrypt `inner` for the peer and send it as the next EAP request */
namespace {
    Bytes seal(netplus::tls &t, const Bytes &inner) {
        t.sendTLS12Record(0x17, inner);
        if (t.is_tls13) t.sendTLS13Record(0x17, inner);
        else t.sendTLS12Record(0x17, inner);
        return drain(t);
    }
}
@@ -967,8 +983,7 @@ Bytes RadiusServer::ttlsInner(const Packet &rq, const radius::Nas &nas, Session
    }
    if (chal.size() == 16 && resp.size() == 50) {
        /* the challenge comes out of the TLS keys (RFC 5281 11.2.4) */
        Bytes seed = crypto::concat(s.tls->clientRandom, s.tls->serverRandom);
        Bytes implicit = s.tls->prf(s.tls->masterSecret, "ttls challenge", seed, 17);
        Bytes implicit = ttlsChallenge(*s.tls);
        if (!crypto::equal(chal, sub(implicit, 0, 16)) || resp[0] != implicit[16])
            return eapDone(rq, nas, s, false, "MS-CHAPv2 challenge does not match the tunnel");
        Bytes peer = sub(resp, 2, 16), nt = sub(resp, 26, 24), hash;