Commit 8baef6ab authored by jan.koester's avatar jan.koester
Browse files

test

parent 7fcea7a5
Loading
Loading
Loading
Loading
+2 −0
Changes for CMakeLists.txt: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -58,6 +58,8 @@ add_library(adconnect_core STATIC
    src/sasl.cpp
    src/discover.cpp
    src/join.cpp
    src/acceptor.cpp
    src/home.cpp
)
target_link_libraries(adconnect_core PUBLIC adconnect_proto confplus::confplus netplus::netplus Threads::Threads)

src/acceptor.cpp

0 → 100644
+175 −0
Changes for src/acceptor.cpp: 175 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#include "acceptor.h"

#include <netplus/random.h>

#include "ber.h"

namespace adconnect {

namespace {

using krb5::Bytes;
namespace der = krb5::der;

const Bytes SpnegoOid = Bytes("\x06\x06\x2b\x06\x01\x05\x05\x02", 8);
const Bytes KrbOid = Bytes("\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02", 11);
/* Microsoft's (an old Windows typo of the above, still sent first) */
const Bytes MsKrbOid = Bytes("\x06\x09\x2a\x86\x48\x82\xf7\x12\x01\x02\x02", 11);

constexpr int Skew = 300;
enum Usage { UsageTicket = 2, UsageApReqAuthenticator = 11, UsageApRepEncPart = 12 };

[[noreturn]] void refuse(const std::string &why) {
    throw std::runtime_error("Kerberos login refused: " + why);
}

/* the OID TLV at the start of s, and its size */
Bytes leadingOid(const Bytes &s) {
    if (s.size() < 2 || s[0] != 0x06) refuse("no mechanism OID");
    size_t n = ber::elementSize(s.data(), s.size());
    if (!n || n > s.size()) refuse("bad mechanism OID");
    return s.substr(0, n);
}

} // namespace

bool Acceptor::replayed(const std::string &key, time_t now) {
    std::lock_guard<std::mutex> l(_lock);
    for (auto it = _seen.begin(); it != _seen.end();)
        it = it->second < now ? _seen.erase(it) : std::next(it);
    if (_seen.count(key)) return true;
    _seen[key] = now + 2 * Skew;
    return false;
}

Accepted Acceptor::accept(const Bytes &token) {
    try {
        /* SPNEGO (NegTokenInit) around a Kerberos token, or the Kerberos token itself */
        bool spnego = false;
        Bytes mechToken = token;
        ber::Element outer = ber::Reader(token).next(0x60);
        Bytes oid = leadingOid(outer.content);
        if (oid == SpnegoOid) {
            spnego = true;
            ber::Element init = ber::Reader(outer.content.substr(oid.size())).next(0xa0);
            auto f = der::fields(ber::Reader(init.content).next(ber::Sequence).content);
            if (!f.count(2)) refuse("SPNEGO without a mechanism token (no Kerberos ticket: the client tried NTLM?)");
            mechToken = der::octets(f[2]);
            outer = ber::Reader(mechToken).next(0x60);
            oid = leadingOid(outer.content);
        }
        if (oid != KrbOid && oid != MsKrbOid) refuse("not a Kerberos token");
        Bytes inner = outer.content.substr(oid.size());
        if (inner.size() < 3 || inner.compare(0, 2, Bytes("\x01\x00", 2)) != 0) refuse("not an AP-REQ");

        /* the AP-REQ: ticket and authenticator */
        auto ap = der::fields(der::application(inner.substr(2), 14));
        if (!ap.count(3) || !ap.count(4)) refuse("incomplete AP-REQ");
        auto tk = der::fields(der::application(ap[3], 1));
        if (!tk.count(1) || !tk.count(3)) refuse("incomplete ticket");
        std::string realm = der::string(tk[1]);
        krb5::EncryptedData tenc = krb5::parseEncryptedData(tk[3]);
        krb5::Key serviceKey;
        if (!_keys(realm, tenc.etype, serviceKey))
            refuse("no " + krb5::etypeName(tenc.etype) + " key for realm " + realm);
        Bytes tplain;
        try {
            tplain = krb5::decrypt(serviceKey, UsageTicket, tenc.cipher);
        } catch (const krb5::CryptoError &) {
            refuse("the ticket doesn't decrypt with this system's key (joined again since?)");
        }
        auto et = der::fields(der::application(tplain, 3));
        if (!et.count(1) || !et.count(2) || !et.count(3) || !et.count(7)) refuse("incomplete ticket contents");
        krb5::Key sessionKey = krb5::parseEncryptionKey(et[1]);
        std::string crealm = der::string(et[2]);
        auto cn = der::fields(ber::Reader(et[3]).next(ber::Sequence).content);
        krb5::Principal client;
        client.realm = crealm;
        if (cn.count(0)) client.type = (int)der::integer(cn[0]);
        if (cn.count(1)) {
            ber::Reader names(ber::Reader(cn[1]).next(ber::Sequence).content);
            while (!names.atEnd()) client.names.push_back(names.next().content);
        }
        time_t now = time(nullptr);
        time_t endtime = krb5::parseKerberosTime(der::string(et[7]));
        if (!endtime || now > endtime + Skew) refuse("the ticket expired");
        if (et.count(6) && krb5::parseKerberosTime(der::string(et[6])) > now + Skew) refuse("the ticket isn't valid yet");

        krb5::EncryptedData aenc = krb5::parseEncryptedData(ap[4]);
        Bytes aplain;
        try {
            aplain = krb5::decrypt(sessionKey, UsageApReqAuthenticator, aenc.cipher);
        } catch (const krb5::CryptoError &) {
            refuse("the authenticator doesn't decrypt");
        }
        auto au = der::fields(der::application(aplain, 2));
        if (!au.count(1) || !au.count(2) || !au.count(4) || !au.count(5)) refuse("incomplete authenticator");
        if (der::string(au[1]) != crealm || au[2] != et[3]) refuse("authenticator and ticket name different clients");
        std::string ctimeText = der::string(au[5]);
        time_t ctime = krb5::parseKerberosTime(ctimeText);
        long long cusec = der::integer(au[4]);
        long diff = (long)(now - ctime);
        if (!ctime || diff > Skew || diff < -Skew)
            refuse("the clocks differ by " + std::to_string(diff < 0 ? -diff : diff) + " s (more than 5 minutes)");
        if (replayed(client.str() + "|" + ctimeText + "|" + std::to_string(cusec), now)) refuse("a replayed authenticator");

        Accepted a;
        a.client = client;
        a.endtime = endtime;
        a.sessionKey = au.count(6) ? krb5::parseEncryptionKey(au[6]).value : sessionKey.value;

        /* the AP-REP: the authenticator's time back, under the ticket's session key */
        uint8_t r[4];
        netplus::fillRandomBytes(r, 4);
        uint32_t seq = ((uint32_t)r[0] << 24 | (uint32_t)r[1] << 16 | (uint32_t)r[2] << 8 | r[3]) & 0x3fffffff;
        Bytes encPart = ber::tlv(ber::applicationConstructed(27),
                                 ber::sequence(der::ctx(0, ber::tlv(0x18, ctimeText)) + der::ctx(1, ber::integer(cusec)) +
                                               der::ctx(3, ber::integer(seq))));
        Bytes apRep = ber::tlv(ber::applicationConstructed(15),
                               ber::sequence(der::ctx(0, ber::integer(5)) + der::ctx(1, ber::integer(15)) +
                                             der::ctx(2, der::encryptedData(sessionKey.etype,
                                                                            krb5::encrypt(sessionKey, UsageApRepEncPart, encPart)))));
        Bytes gss = ber::tlv(0x60, KrbOid + Bytes("\x02\x00", 2) + apRep);
        if (spnego) {
            /* NegTokenResp: accept-completed, the client's Kerberos mechanism, our token */
            a.response = ber::tlv(0xa1, ber::sequence(der::ctx(0, ber::enumerated(0)) + der::ctx(1, oid) +
                                                      der::ctx(2, ber::octets(gss))));
        } else {
            a.response = gss;
        }
        return a;
    } catch (const ber::Error &e) {
        refuse(std::string("malformed token: ") + e.what());
    } catch (const krb5::Error &e) {
        refuse(e.what());
    }
}

} // namespace adconnect

src/acceptor.h

0 → 100644
+74 −0
Changes for src/acceptor.h: 74 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* The accepting side of Kerberos, for SMB logins (ksmbd asks sharedd,
 * sharedd asks adconnectd): the client's token -- SPNEGO with a Kerberos
 * mechanism token, as Windows sends it, or a bare Kerberos GSS token --
 * checked against our own account's keys: the ticket decrypted, the
 * authenticator fresh (5 minutes) and not seen before. The answer is the
 * client, the session key (the authenticator's subkey) and the token to
 * send back: the AP-REP, in SPNEGO if the client used SPNEGO. */

#pragma once

#include <ctime>
#include <functional>
#include <map>
#include <mutex>
#include <stdexcept>
#include <string>

#include "krb5.h"

namespace adconnect {

struct Accepted {
    krb5::Principal client;
    krb5::Bytes sessionKey;
    krb5::Bytes response;
    time_t endtime = 0;  /* the ticket's */
};

class Acceptor {
public:
    /* our key of etype for a ticket of realm; false if we have none */
    using KeySource = std::function<bool(const std::string &realm, int etype, krb5::Key &key)>;

    explicit Acceptor(KeySource keys) : _keys(std::move(keys)) {}

    /* throws std::runtime_error saying why the token isn't accepted */
    Accepted accept(const krb5::Bytes &token);

private:
    bool replayed(const std::string &key, time_t now);

    KeySource _keys;
    std::mutex _lock;
    std::map<std::string, time_t> _seen;  /* authenticator -> until when it's remembered */
};

} // namespace adconnect
+1 −0
Changes for src/config.cpp: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -257,6 +257,7 @@ void readDomain(const Scope &s, Config &cfg, bool multi, bool &idBaseSet) {
    readString(s, "home", cfg.home);
    readString(s, "shell", cfg.shell);
    readBool(s, "lowercase_names", cfg.lowercase);
    readBool(s, "create_home", cfg.createHome);
    cfg.qualified = multi;
    readBool(s, "qualified_names", cfg.qualified);
    readBool(s, "enumerate", cfg.enumerate);
+1 −0
Changes for src/config.h: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ struct Config {
    std::string home = "/home/%d/%n";   /* %d domain, %n the account name, %u the name (with @domain if qualified) */
    std::string shell = "/bin/sh";
    bool lowercase = true;
    bool createHome = true;             /* make a missing home directory at a user's first lookup */
    bool qualified = false;             /* names user@domain (default with several domains) */
    bool enumerate = false;
    std::string userFilter;             /* ANDed to every user search */
Loading