Merge pull request #1830 from majestrate/static-auth-codes-2021-12-26

static endpoint auth codes
This commit is contained in:
majestrate 2022-04-20 16:22:22 -04:00 committed by GitHub
commit 64d6ba8a53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 317 additions and 20 deletions

View File

@ -244,6 +244,14 @@ endif()
target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static)
target_link_libraries(liblokinet PRIVATE libunbound)
pkg_check_modules(CRYPT libcrypt IMPORTED_TARGET)
if(CRYPT_FOUND AND NOT CMAKE_CROSSCOMPILING)
add_definitions(-DHAVE_CRYPT)
add_library(libcrypt INTERFACE)
target_link_libraries(libcrypt INTERFACE PkgConfig::CRYPT)
target_link_libraries(liblokinet PRIVATE libcrypt)
message(STATUS "using libcrypt ${CRYPT_VERSION}")
endif()
if(BUILD_LIBLOKINET)

View File

@ -317,7 +317,7 @@ namespace llarp
ClientOnly,
Comment{
"Set the endpoint authentication mechanism.",
"none/whitelist/lmq",
"none/whitelist/lmq/file",
},
[this](std::string arg) {
if (arg.empty())
@ -366,6 +366,42 @@ namespace llarp
m_AuthWhitelist.emplace(std::move(addr));
});
conf.defineOption<fs::path>(
"network",
"auth-file",
ClientOnly,
MultiValue,
Comment{
"Read auth tokens from file to accept endpoint auth",
"Can be provided multiple times",
},
[this](fs::path arg) {
if (not fs::exists(arg))
throw std::invalid_argument{
stringify("cannot load auth file ", arg, " as it does not seem to exist")};
m_AuthFiles.emplace(std::move(arg));
});
conf.defineOption<std::string>(
"network",
"auth-file-type",
ClientOnly,
Comment{
"How to interpret the contents of an auth file.",
"Possible values: hashes, plaintext",
},
[this](std::string arg) { m_AuthFileType = service::ParseAuthFileType(std::move(arg)); });
conf.defineOption<std::string>(
"network",
"auth-static",
ClientOnly,
MultiValue,
Comment{
"Manually add a static auth code to accept for endpoint auth",
"Can be provided multiple times",
},
[this](std::string arg) { m_AuthStaticTokens.emplace(std::move(arg)); });
conf.defineOption<bool>(
"network",
"reachable",

View File

@ -115,9 +115,12 @@ namespace llarp
std::unordered_map<huint128_t, service::Address> m_mapAddrs;
service::AuthType m_AuthType = service::AuthType::eAuthTypeNone;
service::AuthFileType m_AuthFileType = service::AuthFileType::eAuthFileHashes;
std::optional<std::string> m_AuthUrl;
std::optional<std::string> m_AuthMethod;
std::unordered_set<service::Address> m_AuthWhitelist;
std::unordered_set<std::string> m_AuthStaticTokens;
std::set<fs::path> m_AuthFiles;
std::vector<llarp::dns::SRVData> m_SRVRecords;

View File

@ -100,6 +100,10 @@ namespace llarp
virtual bool
check_identity_privkey(const SecretKey&) = 0;
/// check if a password hash string matches the challenge
virtual bool
check_passwd_hash(std::string pwhash, std::string challenge) = 0;
};
inline Crypto::~Crypto() = default;

View File

@ -13,6 +13,11 @@
#include <llarp/util/str.hpp>
#include <cassert>
#include <cstring>
#ifdef HAVE_CRYPT
#include <crypt.h>
#endif
#include <llarp/util/str.hpp>
extern "C"
{
@ -463,6 +468,25 @@ namespace llarp
auto d = keypair.data();
crypto_kem_keypair(d + PQ_SECRETKEYSIZE, d);
}
bool
CryptoLibSodium::check_passwd_hash(std::string pwhash, std::string challenge)
{
(void)pwhash;
(void)challenge;
bool ret = false;
#ifdef HAVE_CRYPT
auto pos = pwhash.find_last_of('$');
auto settings = pwhash.substr(0, pos);
crypt_data data{};
if (char* ptr = crypt_r(challenge.c_str(), settings.c_str(), &data))
{
ret = ptr == pwhash;
}
sodium_memzero(&data, sizeof(data));
#endif
return ret;
}
} // namespace sodium
const byte_t*

View File

@ -104,6 +104,9 @@ namespace llarp
bool
check_identity_privkey(const SecretKey&) override;
bool
check_passwd_hash(std::string pwhash, std::string challenge) override;
};
} // namespace sodium

View File

@ -174,7 +174,11 @@ namespace llarp
LogInfo(Name(), " setting to be not reachable by default");
}
if (conf.m_AuthType != service::AuthType::eAuthTypeNone)
if (conf.m_AuthType == service::AuthType::eAuthTypeFile)
{
m_AuthPolicy = service::MakeFileAuthPolicy(m_router, conf.m_AuthFiles, conf.m_AuthFileType);
}
else if (conf.m_AuthType != service::AuthType::eAuthTypeNone)
{
std::string url, method;
if (conf.m_AuthUrl.has_value() and conf.m_AuthMethod.has_value())
@ -183,7 +187,12 @@ namespace llarp
method = *conf.m_AuthMethod;
}
auto auth = std::make_shared<rpc::EndpointAuthRPC>(
url, method, conf.m_AuthWhitelist, Router()->lmq(), shared_from_this());
url,
method,
conf.m_AuthWhitelist,
conf.m_AuthStaticTokens,
Router()->lmq(),
shared_from_this());
auth->Start();
m_AuthPolicy = std::move(auth);
}

View File

@ -6,14 +6,16 @@ namespace llarp::rpc
EndpointAuthRPC::EndpointAuthRPC(
std::string url,
std::string method,
Whitelist_t whitelist,
Whitelist_t whitelist_addrs,
std::unordered_set<std::string> whitelist_tokens,
LMQ_ptr lmq,
Endpoint_ptr endpoint)
: m_AuthURL(std::move(url))
, m_AuthMethod(std::move(method))
, m_AuthWhitelist(std::move(whitelist))
, m_LMQ(std::move(lmq))
, m_Endpoint(std::move(endpoint))
: m_AuthURL{std::move(url)}
, m_AuthMethod{std::move(method)}
, m_AuthWhitelist{std::move(whitelist_addrs)}
, m_AuthStaticTokens{std::move(whitelist_tokens)}
, m_LMQ{std::move(lmq)}
, m_Endpoint{std::move(endpoint)}
{}
void
@ -57,13 +59,6 @@ namespace llarp::rpc
reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"});
return;
}
if (not m_Conn.has_value())
{
// we don't have a connection to the backend so it's failed
reply(service::AuthResult{
service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"});
return;
}
if (msg->proto != llarp::service::ProtocolType::Auth)
{
@ -72,9 +67,32 @@ namespace llarp::rpc
return;
}
std::string payload{(char*)msg->payload.data(), msg->payload.size()};
if (m_AuthStaticTokens.count(payload))
{
reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"});
return;
}
if (not m_Conn.has_value())
{
if (m_AuthStaticTokens.empty())
{
// we don't have a connection to the backend so it's failed
reply(service::AuthResult{
service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"});
}
else
{
// static auth mode
reply(service::AuthResult{service::AuthResultCode::eAuthRejected, "access not permitted"});
}
return;
}
const auto authinfo = msg->EncodeAuthInfo();
std::string_view metainfo{authinfo.data(), authinfo.size()};
std::string_view payload{(char*)msg->payload.data(), msg->payload.size()};
// call method with 2 parameters: metainfo and userdata
m_LMQ->request(
*m_Conn,

View File

@ -20,7 +20,8 @@ namespace llarp::rpc
explicit EndpointAuthRPC(
std::string url,
std::string method,
Whitelist_t whitelist,
Whitelist_t addr_whitelist,
std::unordered_set<std::string> token_whitelist,
LMQ_ptr lmq,
Endpoint_ptr endpoint);
virtual ~EndpointAuthRPC() = default;
@ -40,6 +41,7 @@ namespace llarp::rpc
const std::string m_AuthURL;
const std::string m_AuthMethod;
const Whitelist_t m_AuthWhitelist;
const std::unordered_set<std::string> m_AuthStaticTokens;
LMQ_ptr m_LMQ;
Endpoint_ptr m_Endpoint;
std::optional<oxenmq::ConnectionID> m_Conn;

View File

@ -1,6 +1,11 @@
#include "auth.hpp"
#include <unordered_map>
#include <llarp/router/abstractrouter.hpp>
#include "protocol.hpp"
#include <llarp/util/str.hpp>
#include <llarp/util/fs.hpp>
namespace llarp::service
{
/// maybe get auth result from string
@ -22,6 +27,7 @@ namespace llarp::service
ParseAuthType(std::string data)
{
std::unordered_map<std::string, AuthType> values = {
{"file", AuthType::eAuthTypeFile},
{"lmq", AuthType::eAuthTypeLMQ},
{"whitelist", AuthType::eAuthTypeWhitelist},
{"none", AuthType::eAuthTypeNone}};
@ -31,6 +37,25 @@ namespace llarp::service
return itr->second;
}
AuthFileType
ParseAuthFileType(std::string data)
{
std::unordered_map<std::string, AuthFileType> values = {
{"plain", AuthFileType::eAuthFilePlain},
{"plaintext", AuthFileType::eAuthFilePlain},
{"hashed", AuthFileType::eAuthFileHashes},
{"hashes", AuthFileType::eAuthFileHashes},
{"hash", AuthFileType::eAuthFileHashes}};
const auto itr = values.find(data);
if (itr == values.end())
throw std::invalid_argument("no such auth file type: " + data);
#ifndef HAVE_CRYPT
if (itr->second == AuthFileType::eAuthFileHashes)
throw std::invalid_argument("unsupported auth file type: " + data);
#endif
return itr->second;
}
/// turn an auth result code into an int
uint64_t
AuthResultCodeAsInt(AuthResultCode code)
@ -58,4 +83,106 @@ namespace llarp::service
}
}
class FileAuthPolicy : public IAuthPolicy, public std::enable_shared_from_this<FileAuthPolicy>
{
const std::set<fs::path> m_Files;
const AuthFileType m_Type;
AbstractRouter* const m_Router;
mutable util::Mutex m_Access;
std::unordered_set<ConvoTag> m_Pending;
/// returns an auth result for a auth info challange, opens every file until it finds a token
/// matching it
/// this is expected to be done in the IO thread
AuthResult
CheckFiles(const AuthInfo& info) const
{
for (const auto& f : m_Files)
{
fs::ifstream i{f};
std::string line{};
while (std::getline(i, line))
{
// split off comments
const auto parts = split_any(line, "#;", true);
if (auto part = parts[0]; not parts.empty() and not parts[0].empty())
{
// split off whitespaces and check password
if (CheckPasswd(std::string{TrimWhitespace(part)}, info.token))
return AuthResult{AuthResultCode::eAuthAccepted, "accepted by whitelist"};
}
}
}
return AuthResult{AuthResultCode::eAuthRejected, "rejected by whitelist"};
}
bool
CheckPasswd(std::string hash, std::string challenge) const
{
switch (m_Type)
{
case AuthFileType::eAuthFilePlain:
return hash == challenge;
case AuthFileType::eAuthFileHashes:
return CryptoManager::instance()->check_passwd_hash(
std::move(hash), std::move(challenge));
default:
return false;
}
}
public:
FileAuthPolicy(AbstractRouter* r, std::set<fs::path> files, AuthFileType filetype)
: m_Files{std::move(files)}, m_Type{filetype}, m_Router{r}
{}
void
AuthenticateAsync(
std::shared_ptr<ProtocolMessage> msg, std::function<void(AuthResult)> hook) override
{
auto reply = m_Router->loop()->make_caller(
[tag = msg->tag, hook, self = shared_from_this()](AuthResult result) {
{
util::Lock _lock{self->m_Access};
self->m_Pending.erase(tag);
}
hook(result);
});
{
util::Lock _lock{m_Access};
m_Pending.emplace(msg->tag);
}
if (msg->proto == ProtocolType::Auth)
{
m_Router->QueueDiskIO(
[self = shared_from_this(),
auth = AuthInfo{std::string{
reinterpret_cast<const char*>(msg->payload.data()), msg->payload.size()}},
reply]() {
try
{
reply(self->CheckFiles(auth));
}
catch (std::exception& ex)
{
reply(AuthResult{AuthResultCode::eAuthFailed, ex.what()});
}
});
}
else
reply(AuthResult{AuthResultCode::eAuthRejected, "protocol error"});
}
bool
AsyncAuthPending(ConvoTag tag) const override
{
util::Lock _lock{m_Access};
return m_Pending.count(tag);
}
};
std::shared_ptr<IAuthPolicy>
MakeFileAuthPolicy(AbstractRouter* r, std::set<fs::path> files, AuthFileType filetype)
{
return std::make_shared<FileAuthPolicy>(r, std::move(files), filetype);
}
} // namespace llarp::service

View File

@ -44,7 +44,7 @@ namespace llarp::service
struct IAuthPolicy
{
~IAuthPolicy() = default;
virtual ~IAuthPolicy() = default;
/// asynchronously determine if we accept new convotag from remote service, call hook with
/// result later
@ -71,7 +71,16 @@ namespace llarp::service
/// manual whitelist
eAuthTypeWhitelist,
/// LMQ server
eAuthTypeLMQ
eAuthTypeLMQ,
/// static file
eAuthTypeFile,
};
/// how to interpret an file for auth
enum class AuthFileType
{
eAuthFilePlain,
eAuthFileHashes,
};
/// get an auth type from a string
@ -79,4 +88,13 @@ namespace llarp::service
AuthType
ParseAuthType(std::string arg);
/// get an auth file type from a string
/// throws std::invalid_argument if arg is invalid
AuthFileType
ParseAuthFileType(std::string arg);
/// make an IAuthPolicy that reads out of a static file
std::shared_ptr<IAuthPolicy>
MakeFileAuthPolicy(AbstractRouter*, std::set<fs::path> files, AuthFileType fileType);
} // namespace llarp::service

View File

@ -47,3 +47,48 @@ TEST_CASE("PQ crypto")
REQUIRE(c->pqe_decrypt(block, otherShared, pq_keypair_to_secret(keys)));
REQUIRE(otherShared == shared);
}
#ifdef HAVE_CRYPT
TEST_CASE("passwd hash valid")
{
llarp::sodium::CryptoLibSodium crypto;
// poggers password hashes
std::set<std::string> valid_hashes;
// UNIX DES
valid_hashes.emplace("CVu85Ms694POo");
// sha256 salted
valid_hashes.emplace(
"$5$cIghotiBGjfPC7Fu$"
"TXXxPhpUcEiF9tMnjhEVJFi9AlNDSkNRQFTrXPQTKS9");
// sha512 salted
valid_hashes.emplace(
"$6$qB77ms3wCIo.xVKP$Hl0RLuDgWNmIW4s."
"5KUbFmnauoTfrWSPJzDCD8ZTSSfwRbMgqgG6F9y3K.YEYVij8g/"
"Js0DRT2RhgXoX0sHGb.");
for (const auto& hash : valid_hashes)
{
// make sure it is poggers ...
REQUIRE(crypto.check_passwd_hash(hash, "poggers"));
// ... and not inscrutible
REQUIRE(not crypto.check_passwd_hash(hash, "inscrutible"));
}
}
TEST_CASE("passwd hash malformed")
{
llarp::sodium::CryptoLibSodium crypto;
std::set<std::string> invalid_hashes = {
"stevejobs",
"$JKEDbzgzym1N6", // crypt() for "stevejobs" with a $ at the begining
"$0$zero$AAAAAAAAAAA",
"$$$AAAAAAAAAAAA",
"$LIGMA$BALLS$LMAOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO."};
for (const auto& hash : invalid_hashes)
REQUIRE(not crypto.check_passwd_hash(hash, "stevejobs"));
}
#endif