Loading src/acceptor.cpp +13 −4 Changes for src/acceptor.cpp: 13 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -73,14 +73,22 @@ Accepted Acceptor::accept(const Bytes &token) { try { /* SPNEGO (NegTokenInit) around a Kerberos token, or the Kerberos token itself */ bool spnego = false; Bytes mechToken = token; Bytes mechToken = token, offered; /* offered: the client's first mechanism, which the token is for */ 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?)"); if (f.count(0)) { ber::Reader mechs(ber::Reader(f[0]).next(ber::Sequence).content); if (!mechs.atEnd()) { ber::Element first = mechs.next(0x06); offered = ber::tlv(0x06, first.content); } } if (!f.count(2) || (offered != KrbOid && offered != MsKrbOid)) refuse("SPNEGO without a Kerberos token first (the client tries NTLM? -- by address instead of name, or not a domain member)"); mechToken = der::octets(f[2]); outer = ber::Reader(mechToken).next(0x60); oid = leadingOid(outer.content); Loading Loading @@ -158,8 +166,9 @@ Accepted Acceptor::accept(const Bytes &token) { 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) + /* NegTokenResp: accept-completed, the mechanism the client put first * (Windows: Microsoft's OID, with a token of the standard one), our token */ a.response = ber::tlv(0xa1, ber::sequence(der::ctx(0, ber::enumerated(0)) + der::ctx(1, offered) + der::ctx(2, ber::octets(gss)))); } else { a.response = gss; Loading src/directory.cpp +1 −0 Changes for src/directory.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -510,6 +510,7 @@ std::string Directory::handle(Cmd cmd, const std::string &arg) { case Cmd::GrGid: return groupById(arg); case Cmd::GrEnt: return allGroups(); case Cmd::Groups: return groupsOf(arg); case Cmd::Krb5: break; /* the daemon's (acceptor.h) */ } return End; } Loading src/home.cpp +8 −5 Changes for src/home.cpp: 8 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -40,8 +40,11 @@ namespace adconnect { namespace { bool chownTo(int dirfd, const char *name, const uid_r_t &uid, const gid_r_t &gid) { return fchownat_r(dirfd, name, &uid, &gid, AT_SYMLINK_NOFOLLOW) == 0; /* the *_r call; on a kernel without it, the classic one for classic ids */ bool chownTo(int dirfd, const char *name, const uid_r_t &uid, const gid_r_t &gid, int flags = AT_SYMLINK_NOFOLLOW) { if (fchownat_r(dirfd, name, &uid, &gid, flags) == 0) return true; if (errno != ENOSYS || !uid_r_is_legacy(&uid) || !uid_r_is_legacy(&gid)) return false; return fchownat(dirfd, name, (uid_t)uid.legacy, (gid_t)gid.legacy, flags) == 0; } /* src (a directory) into dst (a new, empty one), owned by uid/gid */ Loading Loading @@ -92,8 +95,8 @@ bool copyTree(int src, int dst, const uid_r_t &uid, const gid_r_t &gid) { bool ensureHome(const std::string &dir, const std::string &uidText, const std::string &gidText, std::string &error, const std::string &skel) { if (dir.size() < 2 || dir[0] != '/' || dir.find("/../") != std::string::npos || dir.find("/./") != std::string::npos || dir.compare(dir.size() - 3 < dir.size() ? dir.size() - 3 : 0, 3, "/..") == 0) { bool dotdot = dir.size() >= 3 && dir.compare(dir.size() - 3, 3, "/..") == 0; if (dir.size() < 2 || dir[0] != '/' || dir.find("/../") != std::string::npos || dir.find("/./") != std::string::npos || dotdot) { error = "not a home directory: " + dir; return false; } Loading Loading @@ -129,7 +132,7 @@ bool ensureHome(const std::string &dir, const std::string &uidText, const std::s ok = copyTree(s, home, uid, gid); close(s); } ok = fchownat_r(home, "", &uid, &gid, AT_EMPTY_PATH) == 0 && ok; ok = chownTo(home, "", uid, gid, AT_EMPTY_PATH) && ok; close(home); if (!ok) error = dir + " was only partly set up"; return ok; Loading test/test.cpp +36 −3 Changes for test/test.cpp: 36 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "../src/acceptor.h" #include "../src/directory.h" #include "../src/discover.h" #include "../src/home.h" #include "../src/join.h" #include "../src/gssapi.h" #include "../src/krb5.h" Loading Loading @@ -806,7 +807,8 @@ void testAcceptor() { CHECK(a.sessionKey.size() == 32 && a.sessionKey != sessionKey.value, "acceptor: the session key is the authenticator's subkey"); /* NegTokenResp: accept-completed, the client's mechanism (MS OID first), the AP-REP */ auto f = der::fields(ber::Reader(ber::Reader(a.response).next(0xa1).content).next(ber::Sequence).content); CHECK(ber::toInt(ber::Reader(f[0]).next(ber::Enumerated)) == 0 && f[1] == msKrb, "acceptor: NegTokenResp accept-completed, MS OID"); CHECK(ber::toInt(ber::Reader(f[0]).next(ber::Enumerated)) == 0 && f[1] == msKrb, "acceptor: NegTokenResp accept-completed, the client's first mechanism (MS OID, token of the standard one)"); client.processReply(der::octets(f[2])); CHECK(client.established(), "acceptor: our initiator takes the AP-REP (mutual authentication)"); } catch (const std::exception &e) { Loading @@ -829,14 +831,44 @@ void testAcceptor() { expired.ticket = ticketFor(time(nullptr) - 3600, serviceKey); gss::Context x(expired); CHECK(throws([&] { acc.accept(spnego(x.initialToken())); }), "acceptor: an expired ticket refused"); std::string ntlmOnly = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence(der::ctx(0, ber::sequence(krb))))); CHECK(throws([&] { acc.accept(ntlmOnly); }), "acceptor: SPNEGO without a Kerberos token refused"); std::string noToken = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence(der::ctx(0, ber::sequence(krb))))); CHECK(throws([&] { acc.accept(noToken); }), "acceptor: SPNEGO without a Kerberos token refused"); const std::string ntlmOid("\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a", 12); gss::Context n(cred); std::string ntlmFirst = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence( der::ctx(0, ber::sequence(ntlmOid + krb)) + der::ctx(2, ber::octets(n.initialToken()))))); CHECK(throws([&] { acc.accept(ntlmFirst); }), "acceptor: NTLM first (its token isn't Kerberos) refused"); std::string b; CHECK(base64Encode("hello!?") == "aGVsbG8hPw==" && base64Decode("aGVsbG8hPw==", b) && b == "hello!?" && !base64Decode("a$==", b), "base64"); } void testHome(const std::string &dir) { std::string skel = dir + "/skel", home = dir + "/home/example.com/jan", err; mkdir(skel.c_str(), 0755); mkdir((skel + "/.config").c_str(), 0755); std::ofstream(skel + "/.profile") << "export X=1\n"; std::ofstream(skel + "/.config/app.conf") << "a=b\n"; symlink(".profile", (skel + "/.bashrc").c_str()); std::string me = std::to_string(getuid()), myGroup = std::to_string(getgid()); CHECK(ensureHome(home, me, myGroup, err, skel), "home: made (" + err + ")"); struct stat st; CHECK(stat(home.c_str(), &st) == 0 && S_ISDIR(st.st_mode) && (st.st_mode & 0777) == 0700 && st.st_uid == getuid(), "home: 0700, the user's"); CHECK(stat((dir + "/home/example.com").c_str(), &st) == 0 && (st.st_mode & 0777) == 0755, "home: /home/<domain> made, 0755"); std::ifstream in(home + "/.config/app.conf"); std::string line; std::getline(in, line); char target[64] = ""; ssize_t n = readlink((home + "/.bashrc").c_str(), target, sizeof target - 1); CHECK(line == "a=b" && n > 0 && std::string(target, (size_t)n) == ".profile", "home: /etc/skel copied, symlinks as links"); std::ofstream(home + "/mine") << "keep\n"; CHECK(ensureHome(home, me, myGroup, err, skel) && stat((home + "/mine").c_str(), &st) == 0, "home: there already, left alone"); CHECK(!ensureHome("/home/../etc", me, myGroup, err, skel) && !ensureHome("relative", me, myGroup, err, skel), "home: no .. and only absolute paths"); } time_t fakeNow = 1000; void testCache(const std::string &dir) { Loading Loading @@ -1081,6 +1113,7 @@ int main() { testConfig(dir); testCache(dir); testAcceptor(); testHome(dir); testDiscover(); testJoin(dir); testKrb5Crypto(); Loading Loading
src/acceptor.cpp +13 −4 Changes for src/acceptor.cpp: 13 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -73,14 +73,22 @@ Accepted Acceptor::accept(const Bytes &token) { try { /* SPNEGO (NegTokenInit) around a Kerberos token, or the Kerberos token itself */ bool spnego = false; Bytes mechToken = token; Bytes mechToken = token, offered; /* offered: the client's first mechanism, which the token is for */ 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?)"); if (f.count(0)) { ber::Reader mechs(ber::Reader(f[0]).next(ber::Sequence).content); if (!mechs.atEnd()) { ber::Element first = mechs.next(0x06); offered = ber::tlv(0x06, first.content); } } if (!f.count(2) || (offered != KrbOid && offered != MsKrbOid)) refuse("SPNEGO without a Kerberos token first (the client tries NTLM? -- by address instead of name, or not a domain member)"); mechToken = der::octets(f[2]); outer = ber::Reader(mechToken).next(0x60); oid = leadingOid(outer.content); Loading Loading @@ -158,8 +166,9 @@ Accepted Acceptor::accept(const Bytes &token) { 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) + /* NegTokenResp: accept-completed, the mechanism the client put first * (Windows: Microsoft's OID, with a token of the standard one), our token */ a.response = ber::tlv(0xa1, ber::sequence(der::ctx(0, ber::enumerated(0)) + der::ctx(1, offered) + der::ctx(2, ber::octets(gss)))); } else { a.response = gss; Loading
src/directory.cpp +1 −0 Changes for src/directory.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -510,6 +510,7 @@ std::string Directory::handle(Cmd cmd, const std::string &arg) { case Cmd::GrGid: return groupById(arg); case Cmd::GrEnt: return allGroups(); case Cmd::Groups: return groupsOf(arg); case Cmd::Krb5: break; /* the daemon's (acceptor.h) */ } return End; } Loading
src/home.cpp +8 −5 Changes for src/home.cpp: 8 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -40,8 +40,11 @@ namespace adconnect { namespace { bool chownTo(int dirfd, const char *name, const uid_r_t &uid, const gid_r_t &gid) { return fchownat_r(dirfd, name, &uid, &gid, AT_SYMLINK_NOFOLLOW) == 0; /* the *_r call; on a kernel without it, the classic one for classic ids */ bool chownTo(int dirfd, const char *name, const uid_r_t &uid, const gid_r_t &gid, int flags = AT_SYMLINK_NOFOLLOW) { if (fchownat_r(dirfd, name, &uid, &gid, flags) == 0) return true; if (errno != ENOSYS || !uid_r_is_legacy(&uid) || !uid_r_is_legacy(&gid)) return false; return fchownat(dirfd, name, (uid_t)uid.legacy, (gid_t)gid.legacy, flags) == 0; } /* src (a directory) into dst (a new, empty one), owned by uid/gid */ Loading Loading @@ -92,8 +95,8 @@ bool copyTree(int src, int dst, const uid_r_t &uid, const gid_r_t &gid) { bool ensureHome(const std::string &dir, const std::string &uidText, const std::string &gidText, std::string &error, const std::string &skel) { if (dir.size() < 2 || dir[0] != '/' || dir.find("/../") != std::string::npos || dir.find("/./") != std::string::npos || dir.compare(dir.size() - 3 < dir.size() ? dir.size() - 3 : 0, 3, "/..") == 0) { bool dotdot = dir.size() >= 3 && dir.compare(dir.size() - 3, 3, "/..") == 0; if (dir.size() < 2 || dir[0] != '/' || dir.find("/../") != std::string::npos || dir.find("/./") != std::string::npos || dotdot) { error = "not a home directory: " + dir; return false; } Loading Loading @@ -129,7 +132,7 @@ bool ensureHome(const std::string &dir, const std::string &uidText, const std::s ok = copyTree(s, home, uid, gid); close(s); } ok = fchownat_r(home, "", &uid, &gid, AT_EMPTY_PATH) == 0 && ok; ok = chownTo(home, "", uid, gid, AT_EMPTY_PATH) && ok; close(home); if (!ok) error = dir + " was only partly set up"; return ok; Loading
test/test.cpp +36 −3 Changes for test/test.cpp: 36 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "../src/acceptor.h" #include "../src/directory.h" #include "../src/discover.h" #include "../src/home.h" #include "../src/join.h" #include "../src/gssapi.h" #include "../src/krb5.h" Loading Loading @@ -806,7 +807,8 @@ void testAcceptor() { CHECK(a.sessionKey.size() == 32 && a.sessionKey != sessionKey.value, "acceptor: the session key is the authenticator's subkey"); /* NegTokenResp: accept-completed, the client's mechanism (MS OID first), the AP-REP */ auto f = der::fields(ber::Reader(ber::Reader(a.response).next(0xa1).content).next(ber::Sequence).content); CHECK(ber::toInt(ber::Reader(f[0]).next(ber::Enumerated)) == 0 && f[1] == msKrb, "acceptor: NegTokenResp accept-completed, MS OID"); CHECK(ber::toInt(ber::Reader(f[0]).next(ber::Enumerated)) == 0 && f[1] == msKrb, "acceptor: NegTokenResp accept-completed, the client's first mechanism (MS OID, token of the standard one)"); client.processReply(der::octets(f[2])); CHECK(client.established(), "acceptor: our initiator takes the AP-REP (mutual authentication)"); } catch (const std::exception &e) { Loading @@ -829,14 +831,44 @@ void testAcceptor() { expired.ticket = ticketFor(time(nullptr) - 3600, serviceKey); gss::Context x(expired); CHECK(throws([&] { acc.accept(spnego(x.initialToken())); }), "acceptor: an expired ticket refused"); std::string ntlmOnly = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence(der::ctx(0, ber::sequence(krb))))); CHECK(throws([&] { acc.accept(ntlmOnly); }), "acceptor: SPNEGO without a Kerberos token refused"); std::string noToken = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence(der::ctx(0, ber::sequence(krb))))); CHECK(throws([&] { acc.accept(noToken); }), "acceptor: SPNEGO without a Kerberos token refused"); const std::string ntlmOid("\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a", 12); gss::Context n(cred); std::string ntlmFirst = ber::tlv(0x60, spnegoOid + ber::tlv(0xa0, ber::sequence( der::ctx(0, ber::sequence(ntlmOid + krb)) + der::ctx(2, ber::octets(n.initialToken()))))); CHECK(throws([&] { acc.accept(ntlmFirst); }), "acceptor: NTLM first (its token isn't Kerberos) refused"); std::string b; CHECK(base64Encode("hello!?") == "aGVsbG8hPw==" && base64Decode("aGVsbG8hPw==", b) && b == "hello!?" && !base64Decode("a$==", b), "base64"); } void testHome(const std::string &dir) { std::string skel = dir + "/skel", home = dir + "/home/example.com/jan", err; mkdir(skel.c_str(), 0755); mkdir((skel + "/.config").c_str(), 0755); std::ofstream(skel + "/.profile") << "export X=1\n"; std::ofstream(skel + "/.config/app.conf") << "a=b\n"; symlink(".profile", (skel + "/.bashrc").c_str()); std::string me = std::to_string(getuid()), myGroup = std::to_string(getgid()); CHECK(ensureHome(home, me, myGroup, err, skel), "home: made (" + err + ")"); struct stat st; CHECK(stat(home.c_str(), &st) == 0 && S_ISDIR(st.st_mode) && (st.st_mode & 0777) == 0700 && st.st_uid == getuid(), "home: 0700, the user's"); CHECK(stat((dir + "/home/example.com").c_str(), &st) == 0 && (st.st_mode & 0777) == 0755, "home: /home/<domain> made, 0755"); std::ifstream in(home + "/.config/app.conf"); std::string line; std::getline(in, line); char target[64] = ""; ssize_t n = readlink((home + "/.bashrc").c_str(), target, sizeof target - 1); CHECK(line == "a=b" && n > 0 && std::string(target, (size_t)n) == ".profile", "home: /etc/skel copied, symlinks as links"); std::ofstream(home + "/mine") << "keep\n"; CHECK(ensureHome(home, me, myGroup, err, skel) && stat((home + "/mine").c_str(), &st) == 0, "home: there already, left alone"); CHECK(!ensureHome("/home/../etc", me, myGroup, err, skel) && !ensureHome("relative", me, myGroup, err, skel), "home: no .. and only absolute paths"); } time_t fakeNow = 1000; void testCache(const std::string &dir) { Loading Loading @@ -1081,6 +1113,7 @@ int main() { testConfig(dir); testCache(dir); testAcceptor(); testHome(dir); testDiscover(); testJoin(dir); testKrb5Crypto(); Loading