blob: 06455f17690a032353e1d1a6f0164b53d6258378 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "kerberos/krb5_interface_impl.h"
#include <algorithm>
#include <utility>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <krb5.h>
#include <profile.h>
#include "kerberos/error_strings.h"
namespace kerberos {
namespace {
// Environment variable for the Kerberos configuration (krb5.conf).
constexpr char kKrb5ConfigEnvVar[] = "KRB5_CONFIG";
// Wrapper classes for safe construction and destruction.
struct ScopedKrb5Context {
ScopedKrb5Context() = default;
~ScopedKrb5Context() {
if (ctx) {
krb5_free_context(ctx);
ctx = nullptr;
}
}
// Converts the krb5 |code| to a human readable error message.
std::string GetErrorMessage(errcode_t code) {
// Fallback if error happens during ctx initialization (e.g. bad config).
if (!ctx)
return base::StringPrintf("Error %ld", code);
const char* emsg = krb5_get_error_message(ctx, code);
std::string msg = base::StringPrintf("%s (%ld)", emsg, code);
krb5_free_error_message(ctx, emsg);
return msg;
}
krb5_context get() const { return ctx; }
krb5_context* get_mutable_ptr() { return &ctx; }
private:
krb5_context ctx = nullptr;
};
struct ScopedKrb5CCache {
// Prefer the constructor taking a context if possible.
ScopedKrb5CCache() {}
explicit ScopedKrb5CCache(krb5_context _ctx) { set_ctx(_ctx); }
~ScopedKrb5CCache() {
if (ccache) {
DCHECK(ctx);
krb5_cc_close(ctx, ccache);
ccache = nullptr;
}
}
// The context must be set if |ccache| is set (though get_mutable_ptr())
// before this object is destroyed.
void set_ctx(krb5_context _ctx) {
ctx = _ctx;
DCHECK(ctx);
}
krb5_ccache get() const { return ccache; }
krb5_ccache* get_mutable_ptr() { return &ccache; }
private:
// Pointer to parent data, not owned.
krb5_context ctx = nullptr;
krb5_ccache ccache = nullptr;
};
// Maps some common krb5 error codes to our internal codes. If something is not
// reported properly, add more cases here.
ErrorType TranslateErrorCode(errcode_t code) {
switch (code) {
case KRB5KDC_ERR_NONE:
return ERROR_NONE;
case KRB5_KDC_UNREACH:
return ERROR_NETWORK_PROBLEM;
case KRB5_CONFIG_BADFORMAT:
case PROF_BAD_BOOLEAN:
case PROF_BAD_INTEGER:
return ERROR_BAD_CONFIG;
case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
return ERROR_BAD_PRINCIPAL;
case KRB5KRB_AP_ERR_BAD_INTEGRITY:
case KRB5KDC_ERR_PREAUTH_FAILED:
return ERROR_BAD_PASSWORD;
case KRB5KDC_ERR_KEY_EXP:
return ERROR_PASSWORD_EXPIRED;
// TODO(https://crbug.com/951741): Verify
case KRB5_KPASSWD_SOFTERROR:
return ERROR_PASSWORD_REJECTED;
// TODO(https://crbug.com/951741): Verify
case KRB5_FCC_NOFILE:
return ERROR_NO_CREDENTIALS_CACHE_FOUND;
// TODO(https://crbug.com/951741): Verify
case KRB5KRB_AP_ERR_TKT_EXPIRED:
return ERROR_KERBEROS_TICKET_EXPIRED;
case KRB5KDC_ERR_ETYPE_NOSUPP:
return ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE;
case KRB5_REALM_UNKNOWN:
return ERROR_CONTACTING_KDC_FAILED;
default:
return ERROR_UNKNOWN_KRB5_ERROR;
}
}
// Returns true if the string contained in |data| matches |str_to_match|.
bool DataMatches(const krb5_data& data, const char* str_to_match) {
// It is not clear whether data.data is null terminated, so a strcmp might
// not work.
return strlen(str_to_match) == data.length &&
memcmp(str_to_match, data.data, data.length) == 0;
}
// Returns true if |creds| has a server that starts with "krbtgt".
bool IsTgt(const krb5_creds& creds) {
return creds.server && creds.server->length > 0 &&
DataMatches(creds.server->data[0], "krbtgt");
}
enum class Action { AcquireTgt, RenewTgt };
struct Options {
std::string principal_name;
std::string password;
std::string krb5cc_path;
std::string config_path;
Action action = Action::AcquireTgt;
};
// Encapsulates krb5 context data required for kinit.
class KinitContext {
public:
explicit KinitContext(Options options) : options_(std::move(options)) {
memset(&k5_, 0, sizeof(k5_));
}
// Runs kinit with the options passed to the constructor. Only call once per
// context. While in principle it should be fine to run multiple times, the
// code path probably hasn't been tested (kinit does not call this multiple
// times).
ErrorType Run() {
DCHECK(!did_run_);
did_run_ = true;
ErrorType error = Initialize();
if (error == ERROR_NONE)
error = RunKinit();
Finalize();
return error;
}
private:
// The following code has been adapted from kinit.c in the mit-krb5 code base.
// It has been formatted to fit this screen.
struct Krb5Data {
krb5_principal me;
char* name;
};
// Wrapper around krb5 data to get rid of the gotos in the original code.
struct KInitData {
// Pointer to parent data, not owned.
const krb5_context ctx = nullptr;
// Pointer to parent data, not owned.
const Krb5Data* k5 = nullptr;
krb5_creds my_creds;
krb5_get_init_creds_opt* options = nullptr;
// The lifetime of the |k5| pointer must exceed the lifetime of this object.
explicit KInitData(const krb5_context ctx, const Krb5Data* k5)
: ctx(ctx), k5(k5) {
memset(&my_creds, 0, sizeof(my_creds));
}
~KInitData() {
if (options)
krb5_get_init_creds_opt_free(ctx, options);
if (my_creds.client == k5->me)
my_creds.client = nullptr;
krb5_free_cred_contents(ctx, &my_creds);
}
};
// Initializes krb5 data.
ErrorType Initialize() {
krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
return TranslateErrorCode(ret);
}
out_cc.set_ctx(ctx.get());
ret = krb5_cc_resolve(ctx.get(), options_.krb5cc_path.c_str(),
out_cc.get_mutable_ptr());
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " resolving ccache";
return TranslateErrorCode(ret);
}
ret = krb5_parse_name_flags(ctx.get(), options_.principal_name.c_str(),
0 /* flags */, &k5_.me);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " when parsing name";
return TranslateErrorCode(ret);
}
ret = krb5_unparse_name(ctx.get(), k5_.me, &k5_.name);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " when unparsing name";
return TranslateErrorCode(ret);
}
options_.principal_name = k5_.name;
return ERROR_NONE;
}
// Finalizes krb5 data.
void Finalize() {
krb5_free_unparsed_name(ctx.get(), k5_.name);
krb5_free_principal(ctx.get(), k5_.me);
memset(&k5_, 0, sizeof(k5_));
}
// Runs the actual kinit code and acquires/renews tickets.
ErrorType RunKinit() {
krb5_error_code ret;
KInitData d(ctx.get(), &k5_);
ret = krb5_get_init_creds_opt_alloc(ctx.get(), &d.options);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
return TranslateErrorCode(ret);
}
ret = krb5_get_init_creds_opt_set_out_ccache(ctx.get(), d.options,
out_cc.get());
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
return TranslateErrorCode(ret);
}
// To get notified of expiry, see
// krb5_get_init_creds_opt_set_expire_callback
switch (options_.action) {
case Action::AcquireTgt:
ret = krb5_get_init_creds_password(
ctx.get(), &d.my_creds, k5_.me, options_.password.c_str(),
nullptr /* prompter */, nullptr /* data */, 0 /* start_time */,
nullptr /* in_tkt_service */, d.options);
break;
case Action::RenewTgt:
ret =
krb5_get_renewed_creds(ctx.get(), &d.my_creds, k5_.me, out_cc.get(),
nullptr /* options_.in_tkt_service */);
break;
}
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret);
return TranslateErrorCode(ret);
}
if (options_.action != Action::AcquireTgt) {
ret = krb5_cc_initialize(ctx.get(), out_cc.get(), k5_.me);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " when initializing cache";
return TranslateErrorCode(ret);
}
ret = krb5_cc_store_cred(ctx.get(), out_cc.get(), &d.my_creds);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while storing credentials";
return TranslateErrorCode(ret);
}
}
return ERROR_NONE;
}
ScopedKrb5Context ctx;
ScopedKrb5CCache out_cc;
Krb5Data k5_;
Options options_;
bool did_run_ = false;
};
// Runs the Kerberos configuration |krb5conf| through the krb5 code to see if it
// can be parsed.
ErrorType ValidateConfigViaKrb5(const std::string& krb5conf) {
// Since krb5 doesn't accept config passed as string, write it to disk.
base::FilePath krb5conf_path;
if (!base::CreateTemporaryFile(&krb5conf_path)) {
LOG(ERROR) << "Failed to create temp file for validating config";
return ERROR_LOCAL_IO;
}
const int size = static_cast<int>(krb5conf.size());
if (base::WriteFile(krb5conf_path, krb5conf.data(), size) != size) {
LOG(ERROR) << "Failed to write config to disk at " << krb5conf_path.value()
<< " for validating config";
return ERROR_LOCAL_IO;
}
// krb5_init_context parses the config file.
setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
ScopedKrb5Context ctx;
krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
unsetenv(kKrb5ConfigEnvVar);
base::DeleteFile(krb5conf_path);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
return TranslateErrorCode(ret);
}
return ERROR_NONE;
}
} // namespace
Krb5InterfaceImpl::Krb5InterfaceImpl() = default;
Krb5InterfaceImpl::~Krb5InterfaceImpl() = default;
ErrorType Krb5InterfaceImpl::AcquireTgt(const std::string& principal_name,
const std::string& password,
const base::FilePath& krb5cc_path,
const base::FilePath& krb5conf_path) {
Options options;
options.action = Action::AcquireTgt;
options.principal_name = principal_name;
options.password = password;
options.krb5cc_path = krb5cc_path.value();
setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
ErrorType error = KinitContext(std::move(options)).Run();
unsetenv(kKrb5ConfigEnvVar);
return error;
}
ErrorType Krb5InterfaceImpl::RenewTgt(const std::string& principal_name,
const base::FilePath& krb5cc_path,
const base::FilePath& krb5conf_path) {
Options options;
options.action = Action::RenewTgt;
options.principal_name = principal_name;
options.krb5cc_path = krb5cc_path.value();
setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
ErrorType error = KinitContext(std::move(options)).Run();
unsetenv(kKrb5ConfigEnvVar);
return error;
}
ErrorType Krb5InterfaceImpl::GetTgtStatus(const base::FilePath& krb5cc_path,
TgtStatus* status) {
DCHECK(status);
ScopedKrb5Context ctx;
krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
return TranslateErrorCode(ret);
}
ScopedKrb5CCache ccache(ctx.get());
std::string prefixed_krb5cc_path = "FILE:" + krb5cc_path.value();
ret = krb5_cc_resolve(ctx.get(), prefixed_krb5cc_path.c_str(),
ccache.get_mutable_ptr());
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while resolving cache";
return TranslateErrorCode(ret);
}
krb5_cc_cursor cur;
ret = krb5_cc_start_seq_get(ctx.get(), ccache.get(), &cur);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret)
<< " while starting to retrieve tickets";
return TranslateErrorCode(ret);
}
krb5_timestamp now = time(nullptr);
krb5_creds creds;
bool found_tgt = false;
while ((ret = krb5_cc_next_cred(ctx.get(), ccache.get(), &cur, &creds)) ==
0) {
if (IsTgt(creds)) {
if (creds.times.endtime)
status->validity_seconds =
std::max<int64_t>(creds.times.endtime - now, 0);
if (creds.times.renew_till) {
status->renewal_seconds =
std::max<int64_t>(creds.times.renew_till - now, 0);
}
if (found_tgt) {
LOG(WARNING) << "More than one TGT found in credential cache '"
<< krb5cc_path.value() << ".";
}
found_tgt = true;
}
krb5_free_cred_contents(ctx.get(), &creds);
}
if (!found_tgt) {
LOG(WARNING) << "No TGT found in credential cache '" << krb5cc_path.value()
<< ".";
}
if (ret != KRB5_CC_END) {
LOG(ERROR) << ctx.GetErrorMessage(ret) << " while retrieving a ticket";
return TranslateErrorCode(ret);
}
ret = krb5_cc_end_seq_get(ctx.get(), ccache.get(), &cur);
if (ret) {
LOG(ERROR) << ctx.GetErrorMessage(ret)
<< " while finishing ticket retrieval";
return TranslateErrorCode(ret);
}
return ERROR_NONE;
}
ErrorType Krb5InterfaceImpl::ValidateConfig(const std::string& krb5conf,
ConfigErrorInfo* error_info) {
*error_info = config_parser_.Validate(krb5conf);
if (error_info->code() != CONFIG_ERROR_NONE)
return ERROR_BAD_CONFIG;
// Also try the mit krb5 code to parse the config.
error_info->Clear();
ErrorType error = ValidateConfigViaKrb5(krb5conf);
if (error == ERROR_BAD_CONFIG) {
error_info->set_code(CONFIG_ERROR_KRB5_FAILED_TO_PARSE);
return error;
}
// Ignore all other errors, they're most likely unrelated. The
// |config_parser_| should already cover pretty much everything, anyway.
error_info->set_code(CONFIG_ERROR_NONE);
if (error != ERROR_NONE) {
LOG(WARNING) << "Ignoring unrelated error " << GetErrorString(error)
<< " while validating config";
}
return error;
}
} // namespace kerberos