http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/crypto.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/crypto.h b/be/src/kudu/security/crypto.h new file mode 100644 index 0000000..619be4d --- /dev/null +++ b/be/src/kudu/security/crypto.h @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include <string> + +#include "kudu/security/openssl_util.h" + +// Forward declarations for the OpenSSL typedefs. +typedef struct rsa_st RSA; +typedef struct bio_st BIO; + +namespace kudu { + +class Status; + +namespace security { + +extern const size_t kNonceSize; + +// Supported message digests for data signing and signature verification. +enum class DigestType { + SHA256, + SHA512, +}; + +// A class with generic public key interface, but actually it represents +// an RSA key. +class PublicKey : public RawDataWrapper<EVP_PKEY> { + public: + ~PublicKey() {} + + Status FromString(const std::string& data, DataFormat format) WARN_UNUSED_RESULT; + Status ToString(std::string* data, DataFormat format) const WARN_UNUSED_RESULT; + Status FromFile(const std::string& fpath, DataFormat format) WARN_UNUSED_RESULT; + + Status FromBIO(BIO* bio, DataFormat format) WARN_UNUSED_RESULT; + + // Using the key, verify data signature using the specified message + // digest algorithm for signature verification. + // The input signature should be in in raw format (i.e. no base64 encoding). + Status VerifySignature(DigestType digest, + const std::string& data, + const std::string& signature) const WARN_UNUSED_RESULT; + + // Sets 'equals' to true if the other public key equals this. + Status Equals(const PublicKey& other, bool* equals) const WARN_UNUSED_RESULT; +}; + +// A class with generic private key interface, but actually it represents +// an RSA private key. It's important to have PrivateKey and PublicKey +// be different types to avoid accidental leakage of private keys. +class PrivateKey : public RawDataWrapper<EVP_PKEY> { + public: + ~PrivateKey() {} + + Status FromString(const std::string& data, DataFormat format) WARN_UNUSED_RESULT; + Status ToString(std::string* data, DataFormat format) const WARN_UNUSED_RESULT; + Status FromFile(const std::string& fpath, DataFormat format) WARN_UNUSED_RESULT; + + // Output the public part of the keypair into the specified placeholder. + Status GetPublicKey(PublicKey* public_key) const WARN_UNUSED_RESULT; + + // Using the key, generate data signature using the specified + // message digest algorithm. The result signature is in raw format + // (i.e. no base64 encoding). + Status MakeSignature(DigestType digest, + const std::string& data, + std::string* signature) const WARN_UNUSED_RESULT; +}; + +// Utility method to generate private keys. +Status GeneratePrivateKey(int num_bits, PrivateKey* ret) WARN_UNUSED_RESULT; + +// Generates a nonce of size kNonceSize, and writes it to the provided string. +Status GenerateNonce(std::string* s) WARN_UNUSED_RESULT; + +} // namespace security +} // namespace kudu
http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/init.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/init.cc b/be/src/kudu/security/init.cc new file mode 100644 index 0000000..dfdb5cd --- /dev/null +++ b/be/src/kudu/security/init.cc @@ -0,0 +1,473 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/init.h" + +#include <krb5/krb5.h> + +#include <algorithm> +#include <functional> +#include <memory> +#include <mutex> +#include <random> +#include <string> +#include <vector> + +#include <boost/optional.hpp> + +#include "kudu/gutil/strings/util.h" +#include "kudu/util/flags.h" +#include "kudu/util/flag_tags.h" +#include "kudu/util/net/net_util.h" +#include "kudu/util/rw_mutex.h" +#include "kudu/util/scoped_cleanup.h" +#include "kudu/util/thread.h" + +DEFINE_string(keytab_file, "", + "Path to the Kerberos Keytab file for this server. Specifying a " + "keytab file will cause the server to kinit, and enable Kerberos " + "to be used to authenticate RPC connections."); +TAG_FLAG(keytab_file, stable); + +DEFINE_string(principal, "kudu/_HOST", + "Kerberos principal that this daemon will log in as. The special token " + "_HOST will be replaced with the FQDN of the local host."); +TAG_FLAG(principal, experimental); +// This is currently tagged as unsafe because there is no way for users to configure +// clients to expect a non-default principal. As such, configuring a server to login +// as a different one would end up with a cluster that can't be connected to. +// See KUDU-1884. +TAG_FLAG(principal, unsafe); + +using std::mt19937; +using std::random_device; +using std::string; +using std::uniform_int_distribution; +using std::uniform_real_distribution; +using std::vector; + +namespace kudu { +namespace security { + +namespace { + +class KinitContext; + +// Global context for usage of the Krb5 library. +krb5_context g_krb5_ctx; + +// Global instance of the context used by the kinit/renewal thread. +KinitContext* g_kinit_ctx; + +// This lock is used to avoid a race while renewing the kerberos ticket. +// The race can occur between the time we reinitialize the cache and the +// time when we actually store the renewed credential back in the cache. +RWMutex* g_kerberos_reinit_lock; + +class KinitContext { + public: + KinitContext(); + + // Equivalent implementation of 'kinit -kt <keytab path> <principal>'. + // + // This logs in from the given keytab as the given principal, returning + // RuntimeError if any part of this process fails. + // + // If the log-in is successful, then the default ticket cache is overwritten + // with the credentials of the newly logged-in principal. + Status Kinit(const string& keytab_path, const string& principal); + + // Acquires a new Ticket Granting Ticket (TGT). + // + // Renews the existing ticket if possible, or acquires a new Ticket Granting + // Ticket (TGT). + Status DoRenewal(); + + // Calculates the next sleep interval based on the 'ticket_end_timestamp_' and + // adds some jitter so that all the nodes do not hit the KDC at the same time. + // + // If 'num_retries' > 0, it calls GetBackedOffRenewInterval() to return a backed + // off interval. + int32_t GetNextRenewInterval(uint32_t num_retries); + + // Returns a value based on 'time_remaining' that increases exponentially with + // 'num_retries', with a random jitter of +/- 0%-50% of that value. + int32_t GetBackedOffRenewInterval(int32_t time_remaining, uint32_t num_retries); + + const string& principal_str() const { return principal_str_; } + const string& username_str() const { return username_str_; } + + private: + krb5_principal principal_; + krb5_keytab keytab_; + krb5_ccache ccache_; + krb5_get_init_creds_opt* opts_; + + // The stringified principal and username that we are logged in as. + string principal_str_, username_str_; + + // This is the time that the current TGT in use expires. + int32_t ticket_end_timestamp_; +}; + +Status Krb5CallToStatus(krb5_context ctx, krb5_error_code code) { + if (code == 0) return Status::OK(); + + std::unique_ptr<const char, std::function<void(const char*)>> err_msg( + krb5_get_error_message(ctx, code), + std::bind(krb5_free_error_message, ctx, std::placeholders::_1)); + return Status::RuntimeError(err_msg.get()); +} +#define KRB5_RETURN_NOT_OK_PREPEND(call, prepend) \ + RETURN_NOT_OK_PREPEND(Krb5CallToStatus(g_krb5_ctx, (call)), (prepend)) + + +void InitKrb5Ctx() { + static std::once_flag once; + std::call_once(once, [&]() { + CHECK_EQ(krb5_init_context(&g_krb5_ctx), 0); + }); +} + +KinitContext::KinitContext() {} + +// Port of the data_eq() implementation from krb5/k5-int.h +inline int data_eq(krb5_data d1, krb5_data d2) { + return (d1.length == d2.length && !memcmp(d1.data, d2.data, d1.length)); +} + +// Port of the data_eq_string() implementation from krb5/k5-int.h +inline int data_eq_string(krb5_data d, const char *s) { + return (d.length == strlen(s) && !memcmp(d.data, s, d.length)); +} + +Status Krb5UnparseName(krb5_principal princ, string* name) { + char* c_name; + KRB5_RETURN_NOT_OK_PREPEND(krb5_unparse_name(g_krb5_ctx, princ, &c_name), + "krb5_unparse_name"); + auto cleanup_name = MakeScopedCleanup([&]() { + krb5_free_unparsed_name(g_krb5_ctx, c_name); + }); + *name = c_name; + return Status::OK(); +} + +// Periodically calls DoRenewal(). +void RenewThread() { + uint32_t failure_retries = 0; + while (true) { + // This thread is run immediately after the first Kinit, so sleep first. + SleepFor(MonoDelta::FromSeconds(g_kinit_ctx->GetNextRenewInterval(failure_retries))); + + Status s = g_kinit_ctx->DoRenewal(); + WARN_NOT_OK(s, "Kerberos reacquire error: "); + if (!s.ok()) { + ++failure_retries; + } else { + failure_retries = 0; + } + } +} + +int32_t KinitContext::GetNextRenewInterval(uint32_t num_retries) { + int32_t time_remaining = ticket_end_timestamp_ - time(nullptr); + + // If the last ticket renewal was a failure, we back off our retry attempts exponentially. + if (num_retries > 0) return GetBackedOffRenewInterval(time_remaining, num_retries); + + // If the time remaining between now and ticket expiry is: + // * > 10 minutes: We attempt to renew the ticket between 5 seconds and 5 minutes before the + // ticket expires. + // * 5 - 10 minutes: We attempt to renew the ticket betwen 5 seconds and 1 minute before the + // ticket expires. + // * < 5 minutes: Attempt to renew the ticket every 'time_remaining'. + // The jitter is added to make sure that every server doesn't flood the KDC at the same time. + random_device rd; + mt19937 generator(rd()); + if (time_remaining > 600) { + uniform_int_distribution<> dist(5, 300); + return time_remaining - dist(generator); + } else if (time_remaining > 300) { + uniform_int_distribution<> dist(5, 60); + return time_remaining - dist(generator); + } + return time_remaining; +} + +int32_t KinitContext::GetBackedOffRenewInterval(int32_t time_remaining, uint32_t num_retries) { + // The minimum sleep interval after a failure will be 60 seconds. + int32_t next_interval = std::max(time_remaining, 60); + int32_t base_time = std::min(next_interval * (1 << num_retries), INT32_MAX); + random_device rd; + mt19937 generator(rd()); + uniform_real_distribution<> dist(0.5, 1.5); + return static_cast<int32_t>(base_time * dist(generator)); +} + +Status KinitContext::DoRenewal() { + + krb5_cc_cursor cursor; + // Setup a cursor to iterate through the credential cache. + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_start_seq_get(g_krb5_ctx, ccache_, &cursor), + "Failed to peek into ccache"); + auto cleanup_cursor = MakeScopedCleanup([&]() { + krb5_cc_end_seq_get(g_krb5_ctx, ccache_, &cursor); }); + + krb5_creds creds; + memset(&creds, 0, sizeof(krb5_creds)); + + krb5_error_code rc; + // Iterate through the credential cache. + while (!(rc = krb5_cc_next_cred(g_krb5_ctx, ccache_, &cursor, &creds))) { + auto cleanup_creds = MakeScopedCleanup([&]() { + krb5_free_cred_contents(g_krb5_ctx, &creds); }); + if (krb5_is_config_principal(g_krb5_ctx, creds.server)) continue; + + // We only want to renew the TGT (Ticket Granting Ticket). Ignore all other tickets. + // This follows the same format as is_local_tgt() from krb5:src/clients/klist/klist.c + if (creds.server->length != 2 || + data_eq(creds.server->data[1], principal_->realm) == 0 || + data_eq_string(creds.server->data[0], KRB5_TGS_NAME) == 0 || + data_eq(creds.server->realm, principal_->realm) == 0) { + continue; + } + + time_t now = time(nullptr); + time_t ticket_expiry = creds.times.endtime; + time_t renew_till = creds.times.renew_till; + time_t renew_deadline = renew_till - 30; + + krb5_creds new_creds; + memset(&new_creds, 0, sizeof(krb5_creds)); + auto cleanup_new_creds = MakeScopedCleanup([&]() { + krb5_free_cred_contents(g_krb5_ctx, &new_creds); }); + // If the ticket has already expired or if there's only a short period before which the + // renew window closes, we acquire a new ticket. + if (ticket_expiry < now || renew_deadline < now) { + // Acquire a new ticket using the keytab. This ticket will automatically be put into the + // credential cache. + { + std::lock_guard<RWMutex> l(*g_kerberos_reinit_lock); + KRB5_RETURN_NOT_OK_PREPEND(krb5_get_init_creds_keytab(g_krb5_ctx, &new_creds, principal_, + keytab_, 0 /* valid from now */, + nullptr /* TKT service name */, + opts_), + "Reacquire error: unable to login from keytab"); +#ifdef __APPLE__ + // Heimdal krb5 doesn't have the 'krb5_get_init_creds_opt_set_out_ccache' option, + // so use this alternate route. + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_initialize(g_krb5_ctx, ccache_, principal_), + "Reacquire error: could not init ccache"); + + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_store_cred(g_krb5_ctx, ccache_, &creds), + "Reacquire error: could not store creds in cache"); +#endif + } + LOG(INFO) << "Successfully reacquired a new kerberos TGT"; + } else { + // Renew existing ticket. + KRB5_RETURN_NOT_OK_PREPEND(krb5_get_renewed_creds(g_krb5_ctx, &new_creds, principal_, + ccache_, nullptr), + "Failed to renew ticket"); + + { + // Take the write lock here so that any connections undergoing negotiation have to wait + // until the new credentials are placed in the cache. + std::lock_guard<RWMutex> l(*g_kerberos_reinit_lock); + // Clear existing credentials in cache. + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_initialize(g_krb5_ctx, ccache_, principal_), + "Failed to re-initialize ccache"); + + // Store the new credentials in the cache. + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_store_cred(g_krb5_ctx, ccache_, &new_creds), + "Failed to store credentials in ccache"); + } + LOG(INFO) << "Successfully renewed kerberos TGT"; + } + ticket_end_timestamp_ = new_creds.times.endtime; + break; + } + return Status::OK(); +} + +Status KinitContext::Kinit(const string& keytab_path, const string& principal) { + InitKrb5Ctx(); + + // Parse the principal + KRB5_RETURN_NOT_OK_PREPEND(krb5_parse_name(g_krb5_ctx, principal.c_str(), &principal_), + "could not parse principal"); + + KRB5_RETURN_NOT_OK_PREPEND(krb5_kt_resolve(g_krb5_ctx, keytab_path.c_str(), &keytab_), + "unable to resolve keytab"); + + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_default(g_krb5_ctx, &ccache_), + "unable to get default credentials cache"); + + KRB5_RETURN_NOT_OK_PREPEND(krb5_get_init_creds_opt_alloc(g_krb5_ctx, &opts_), + "unable to allocate get_init_creds_opt struct"); + +#ifndef __APPLE__ + KRB5_RETURN_NOT_OK_PREPEND(krb5_get_init_creds_opt_set_out_ccache(g_krb5_ctx, opts_, ccache_), + "unable to set init_creds options"); +#endif + + krb5_creds creds; + KRB5_RETURN_NOT_OK_PREPEND(krb5_get_init_creds_keytab(g_krb5_ctx, &creds, principal_, keytab_, + 0 /* valid from now */, + nullptr /* TKT service name */, opts_), + "unable to login from keytab"); + auto cleanup_creds = MakeScopedCleanup([&]() { + krb5_free_cred_contents(g_krb5_ctx, &creds); }); + + ticket_end_timestamp_ = creds.times.endtime; + +#ifdef __APPLE__ + // Heimdal krb5 doesn't have the 'krb5_get_init_creds_opt_set_out_ccache' option, + // so use this alternate route. + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_initialize(g_krb5_ctx, ccache_, principal_), + "could not init ccache"); + + KRB5_RETURN_NOT_OK_PREPEND(krb5_cc_store_cred(g_krb5_ctx, ccache_, &creds), + "could not store creds in cache"); +#endif + + // Convert the logged-in principal back to a string. This may be different than + // 'principal', since the default realm will be filled in based on the Kerberos + // configuration if not originally specified. + RETURN_NOT_OK_PREPEND(Krb5UnparseName(principal_, &principal_str_), + "could not stringify the logged-in principal"); + RETURN_NOT_OK_PREPEND(MapPrincipalToLocalName(principal_str_, &username_str_), + "could not map own logged-in principal to a short username"); + + LOG(INFO) << "Logged in from keytab as " << principal_str_ + << " (short username " << username_str_ << ")"; + + return Status::OK(); +} + +Status GetConfiguredPrincipal(string* principal) { + string p = FLAGS_principal; + string hostname; + // Try to fill in either the FQDN or hostname. + if (!GetFQDN(&hostname).ok()) { + RETURN_NOT_OK(GetHostname(&hostname)); + } + GlobalReplaceSubstring("_HOST", hostname, &p); + *principal = p; + return Status::OK(); +} +} // anonymous namespace + + +RWMutex* KerberosReinitLock() { + return g_kerberos_reinit_lock; +} + +Status CanonicalizeKrb5Principal(std::string* principal) { + InitKrb5Ctx(); + krb5_principal princ; + KRB5_RETURN_NOT_OK_PREPEND(krb5_parse_name(g_krb5_ctx, principal->c_str(), &princ), + "could not parse principal"); + auto cleanup = MakeScopedCleanup([&]() { + krb5_free_principal(g_krb5_ctx, princ); + }); + RETURN_NOT_OK_PREPEND(Krb5UnparseName(princ, principal), + "failed to convert principal back to string"); + return Status::OK(); +} + +Status MapPrincipalToLocalName(const std::string& principal, std::string* local_name) { + InitKrb5Ctx(); + krb5_principal princ; + KRB5_RETURN_NOT_OK_PREPEND(krb5_parse_name(g_krb5_ctx, principal.c_str(), &princ), + "could not parse principal"); + auto cleanup = MakeScopedCleanup([&]() { + krb5_free_principal(g_krb5_ctx, princ); + }); + char buf[1024]; + krb5_error_code rc; +#ifndef __APPLE__ + rc = krb5_aname_to_localname(g_krb5_ctx, princ, arraysize(buf), buf); +#else + // macOS's Heimdal library has a no-op implementation of + // krb5_aname_to_localname, so instead we fall down to below and grab the + // first component of the principal. + rc = KRB5_LNAME_NOTRANS; +#endif + if (rc == KRB5_LNAME_NOTRANS) { + // No name mapping specified. We fall back to simply taking the first component + // of the principal, for compatibility with the default behavior of Hadoop. + // TODO(todd): we should support custom configured auth-to-local mapping, since + // most Hadoop ecosystem components do not load them from krb5.conf. + if (princ->length > 0) { + local_name->assign(princ->data[0].data, princ->data[0].length); + return Status::OK(); + } + return Status::NotFound("unable to find first component of principal"); + } + if (rc == KRB5_CONFIG_NOTENUFSPACE) { + return Status::InvalidArgument("mapped username too large"); + } + KRB5_RETURN_NOT_OK_PREPEND(rc, "krb5_aname_to_localname"); + if (strlen(buf) == 0) { + return Status::InvalidArgument("principal mapped to empty username"); + } + local_name->assign(buf); + return Status::OK(); +} + +boost::optional<string> GetLoggedInPrincipalFromKeytab() { + if (!g_kinit_ctx) return boost::none; + return g_kinit_ctx->principal_str(); +} + +boost::optional<string> GetLoggedInUsernameFromKeytab() { + if (!g_kinit_ctx) return boost::none; + return g_kinit_ctx->username_str(); +} + +Status InitKerberosForServer() { + if (FLAGS_keytab_file.empty()) return Status::OK(); + + // Have the daemons use an in-memory ticket cache, so they don't accidentally + // pick up credentials from test cases or any other daemon. + // TODO(todd): extract these krb5 env vars into some constants since they're + // typo-prone. + setenv("KRB5CCNAME", "MEMORY:kudu", 1); + setenv("KRB5_KTNAME", FLAGS_keytab_file.c_str(), 1); + + // KUDU-1897: disable the Kerberos replay cache. The KRPC protocol includes a + // per-connection server-generated nonce to protect against replay attacks + // when authenticating via Kerberos. The replay cache has many performance and + // implementation issues. + setenv("KRB5RCACHETYPE", "none", 1); + + g_kinit_ctx = new KinitContext(); + string principal; + RETURN_NOT_OK(GetConfiguredPrincipal(&principal)); + RETURN_NOT_OK_PREPEND(g_kinit_ctx->Kinit(FLAGS_keytab_file, principal), "unable to kinit"); + + g_kerberos_reinit_lock = new RWMutex(RWMutex::Priority::PREFER_WRITING); + scoped_refptr<Thread> renew_thread; + // Start the renewal thread. + RETURN_NOT_OK(Thread::Create("kerberos", "renewal thread", &RenewThread, &renew_thread)); + + return Status::OK(); +} + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/init.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/init.h b/be/src/kudu/security/init.h new file mode 100644 index 0000000..61c9577 --- /dev/null +++ b/be/src/kudu/security/init.h @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +#pragma once + +#include <string> + +#include <boost/optional/optional_fwd.hpp> + +namespace kudu { + +class RWMutex; +class Status; + +namespace security { + +// Initializes Kerberos for a server. In particular, this processes +// the '--keytab_file' command line flag. +Status InitKerberosForServer(); + +// Returns the process lock 'kerberos_reinit_lock' +// This lock is acquired in write mode while the ticket is being renewed, and +// acquired in read mode before using the SASL library which might require a ticket. +RWMutex* KerberosReinitLock(); + +// Return the full principal (user/host@REALM) that the server has used to +// log in from the keytab. +// +// If the server has not logged in from a keytab, returns boost::none. +boost::optional<std::string> GetLoggedInPrincipalFromKeytab(); + +// Same, but returns the mapped short username. +boost::optional<std::string> GetLoggedInUsernameFromKeytab(); + +// Canonicalize the given principal name by adding '@DEFAULT_REALM' in the case that +// the principal has no realm. +// +// TODO(todd): move to kerberos_util.h in the later patch in this series (the file doesn't +// exist yet, and trying to avoid rebase pain). +Status CanonicalizeKrb5Principal(std::string* principal); + +// Map the given Kerberos principal 'principal' to a short username (i.e. with no realm or +// host component). +// +// This respects the "auth-to-local" mappings from the system krb5.conf. However, if no such +// mapping can be found, we fall back to simply taking the first component of the principal. +// +// TODO(todd): move to kerberos_util.h in the later patch in this series (the file doesn't +// exist yet, and trying to avoid rebase pain). +Status MapPrincipalToLocalName(const std::string& principal, std::string* local_name); + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/kerberos_util.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/kerberos_util.cc b/be/src/kudu/security/kerberos_util.cc new file mode 100644 index 0000000..09f1b82 --- /dev/null +++ b/be/src/kudu/security/kerberos_util.cc @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/kerberos_util.h" +#include "kudu/gutil/strings/split.h" + +#include <array> +#include <utility> + +namespace kudu { +namespace security { + +std::array<StringPiece, 3> SplitKerberosPrincipal(StringPiece principal) { + + std::pair<StringPiece, StringPiece> user_realm = strings::Split(principal, "@"); + std::pair<StringPiece, StringPiece> princ_host = strings::Split(user_realm.first, "/"); + return {{princ_host.first, princ_host.second, user_realm.second}}; +} + + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/kerberos_util.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/kerberos_util.h b/be/src/kudu/security/kerberos_util.h new file mode 100644 index 0000000..4c27a86 --- /dev/null +++ b/be/src/kudu/security/kerberos_util.h @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +#pragma once + +#include <array> + +class StringPiece; + +namespace kudu { +namespace security { + +std::array<StringPiece, 3> SplitKerberosPrincipal(StringPiece principal); + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/krb5_realm_override.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/krb5_realm_override.cc b/be/src/kudu/security/krb5_realm_override.cc new file mode 100644 index 0000000..3f875f1 --- /dev/null +++ b/be/src/kudu/security/krb5_realm_override.cc @@ -0,0 +1,104 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// This file provides a workaround for tests running with Kerberos 1.11 or earlier. +// These versions of Kerberos are missing a fix which allows service principals +// to use IP addresses in their host component: +// +// http://krbdev.mit.edu/rt/Ticket/Display.html?id=7603 +// +// We use such principals in external minicluster tests, where servers have IP addresses +// like 127.x.y.z that have no corresponding reverse DNS. +// +// The file contains an implementation of krb5_get_host_realm which wraps the one +// in the Kerberos library. It detects the return code that indicates the +// above problem and falls back to the default realm/ +// +// The wrapper is injected via linking it into tests as well as the +// "security" library. The linkage invocation uses the '-Wl,--undefined' +// linker flag to force linking even though no symbol here is explicitly +// referenced. + +#include <dlfcn.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <krb5/krb5.h> +#include <glog/logging.h> + +extern "C" { + +// This symbol is exported from the static library so that other static-linked binaries +// can reference it and force this compilation unit to be linked. Otherwise the linker +// thinks it's unused and doesn't link it. +int krb5_realm_override_loaded = 1; + +// Save the original function from the Kerberos library itself. +// We use dlsym() to load all of them, since this file gets linked into +// some test binaries that themselves may not link against libkrb5.so at all. +static void* g_orig_krb5_get_host_realm; +static void* g_orig_krb5_get_default_realm; +static void* g_orig_krb5_free_default_realm; + +// We only enable our workaround if this environment variable is set. +constexpr static const char* kEnvVar = "KUDU_ENABLE_KRB5_REALM_FIX"; + +#define CALL_ORIG(func_name, ...) \ + ((decltype(&func_name))g_orig_ ## func_name)(__VA_ARGS__) + +__attribute__((constructor)) +static void init_orig_func() { + g_orig_krb5_get_host_realm = dlsym(RTLD_NEXT, "krb5_get_host_realm"); + g_orig_krb5_get_default_realm = dlsym(RTLD_NEXT, "krb5_get_default_realm"); + g_orig_krb5_free_default_realm = dlsym(RTLD_NEXT, "krb5_free_default_realm"); +} + +krb5_error_code krb5_get_host_realm(krb5_context context, const char* host, char*** realmsp) { + CHECK(g_orig_krb5_get_host_realm); + CHECK(g_orig_krb5_get_default_realm); + CHECK(g_orig_krb5_free_default_realm); + + krb5_error_code rc = CALL_ORIG(krb5_get_host_realm, context, host, realmsp); + if (rc != KRB5_ERR_NUMERIC_REALM || getenv(kEnvVar) == nullptr) { + return rc; + } + // If we get KRB5_ERR_NUMERIC_REALM, this is indicative of a Kerberos version + // which has not provided support for numeric addresses as service host names + // So, we fill in the default realm instead. + char* default_realm; + rc = CALL_ORIG(krb5_get_default_realm, context, &default_realm); + if (rc != 0) { + return rc; + } + + char** ret_realms; + ret_realms = static_cast<char**>(malloc(2 * sizeof(*ret_realms))); + if (ret_realms == nullptr) return ENOMEM; + ret_realms[0] = strdup(default_realm); + if (ret_realms[0] == nullptr) { + free(ret_realms); + return ENOMEM; + } + ret_realms[1] = 0; + *realmsp = ret_realms; + + CALL_ORIG(krb5_free_default_realm, context, default_realm); + return 0; +} + +} // extern "C" http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/openssl_util.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/openssl_util.cc b/be/src/kudu/security/openssl_util.cc new file mode 100644 index 0000000..f5ce822 --- /dev/null +++ b/be/src/kudu/security/openssl_util.cc @@ -0,0 +1,211 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/openssl_util.h" + +#include <cstdio> +#include <cstdlib> +#include <mutex> +#include <sstream> +#include <string> + +#include <glog/logging.h> +#include <openssl/err.h> +#include <openssl/rand.h> +#include <openssl/ssl.h> + +#include "kudu/util/debug/leakcheck_disabler.h" +#include "kudu/util/errno.h" +#include "kudu/util/mutex.h" +#include "kudu/util/status.h" +#include "kudu/util/thread.h" + +using std::ostringstream; +using std::string; + +namespace kudu { +namespace security { + +namespace { + +// Determine whether initialization was ever called. +// +// Thread safety: +// - written by DoInitializeOpenSSL (single-threaded, due to std::call_once) +// - read by DisableOpenSSLInitialization (must not be concurrent with above) +bool g_ssl_is_initialized = false; + +// If true, then we expect someone else has initialized SSL. +// +// Thread safety: +// - read by DoInitializeOpenSSL (single-threaded, due to std::call_once) +// - written by DisableOpenSSLInitialization (must not be concurrent with above) +bool g_disable_ssl_init = false; + +// Array of locks used by OpenSSL. +// We use an intentionally-leaked C-style array here to avoid non-POD static data. +Mutex* kCryptoLocks = nullptr; + +// Lock/Unlock the nth lock. Only to be used by OpenSSL. +void LockingCB(int mode, int type, const char* /*file*/, int /*line*/) { + DCHECK(kCryptoLocks); + Mutex* m = &kCryptoLocks[type]; + if (mode & CRYPTO_LOCK) { + m->lock(); + } else { + m->unlock(); + } +} + +Status CheckOpenSSLInitialized() { + if (!CRYPTO_get_locking_callback()) { + return Status::RuntimeError("Locking callback not initialized"); + } + auto ctx = ssl_make_unique(SSL_CTX_new(SSLv23_method())); + if (!ctx) { + ERR_clear_error(); + return Status::RuntimeError("SSL library appears uninitialized (cannot create SSL_CTX)"); + } + return Status::OK(); +} + +void DoInitializeOpenSSL() { + // In case the user's thread has left some error around, clear it. + ERR_clear_error(); + SCOPED_OPENSSL_NO_PENDING_ERRORS; + if (g_disable_ssl_init) { + VLOG(2) << "Not initializing OpenSSL (disabled by application)"; + return; + } + + // Check that OpenSSL isn't already initialized. If it is, it's likely + // we are embedded in (or embedding) another application/library which + // initializes OpenSSL, and we risk installing conflicting callbacks + // or crashing due to concurrent initialization attempts. In that case, + // log a warning. + auto ctx = ssl_make_unique(SSL_CTX_new(SSLv23_method())); + if (ctx) { + LOG(WARNING) << "It appears that OpenSSL has been previously initialized by " + << "code outside of Kudu. Please use kudu::client::DisableOpenSSLInitialization() " + << "to avoid potential crashes due to conflicting initialization."; + // Continue anyway; all of the below is idempotent, except for the locking callback, + // which we check before overriding. They aren't thread-safe, however -- that's why + // we try to get embedding applications to do the right thing here rather than risk a + // potential initialization race. + } else { + // As expected, SSL is not initialized, so SSL_CTX_new() failed. Make sure + // it didn't leave anything in our error queue. + ERR_clear_error(); + } + + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + RAND_poll(); + + if (!CRYPTO_get_locking_callback()) { + // Initialize the OpenSSL mutexes. We intentionally leak these, so ignore + // LSAN warnings. + debug::ScopedLeakCheckDisabler d; + int num_locks = CRYPTO_num_locks(); + CHECK(!kCryptoLocks); + kCryptoLocks = new Mutex[num_locks]; + + // Callbacks used by OpenSSL required in a multi-threaded setting. + CRYPTO_set_locking_callback(LockingCB); + } + + g_ssl_is_initialized = true; +} + +} // anonymous namespace + +Status DisableOpenSSLInitialization() { + if (g_disable_ssl_init) return Status::OK(); + if (g_ssl_is_initialized) { + return Status::IllegalState("SSL already initialized. Initialization can only be disabled " + "before first usage."); + } + RETURN_NOT_OK(CheckOpenSSLInitialized()); + g_disable_ssl_init = true; + return Status::OK(); +} + +void InitializeOpenSSL() { + static std::once_flag ssl_once; + std::call_once(ssl_once, DoInitializeOpenSSL); +} + +string GetOpenSSLErrors() { + ostringstream serr; + uint32_t l; + int line, flags; + const char *file, *data; + bool is_first = true; + while ((l = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) { + if (is_first) { + is_first = false; + } else { + serr << " "; + } + + char buf[256]; + ERR_error_string_n(l, buf, sizeof(buf)); + serr << buf << ":" << file << ":" << line; + if (flags & ERR_TXT_STRING) { + serr << ":" << data; + } + } + return serr.str(); +} + +string GetSSLErrorDescription(int error_code) { + switch (error_code) { + case SSL_ERROR_NONE: return ""; + case SSL_ERROR_ZERO_RETURN: return "SSL_ERROR_ZERO_RETURN"; + case SSL_ERROR_WANT_READ: return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_WANT_CONNECT: return "SSL_ERROR_WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: return "SSL_ERROR_WANT_ACCEPT"; + case SSL_ERROR_WANT_X509_LOOKUP: return "SSL_ERROR_WANT_X509_LOOKUP"; + case SSL_ERROR_SYSCALL: { + string queued_error = GetOpenSSLErrors(); + if (!queued_error.empty()) { + return queued_error; + } + return kudu::ErrnoToString(errno); + }; + default: return GetOpenSSLErrors(); + } +} + +const string& DataFormatToString(DataFormat fmt) { + static const string kStrFormatUnknown = "UNKNOWN"; + static const string kStrFormatDer = "DER"; + static const string kStrFormatPem = "PEM"; + switch (fmt) { + case DataFormat::DER: + return kStrFormatDer; + case DataFormat::PEM: + return kStrFormatPem; + default: + return kStrFormatUnknown; + } +} + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/openssl_util.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/openssl_util.h b/be/src/kudu/security/openssl_util.h new file mode 100644 index 0000000..db85a85 --- /dev/null +++ b/be/src/kudu/security/openssl_util.h @@ -0,0 +1,190 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include <functional> +#include <memory> +#include <string> + +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> +#include <openssl/x509.h> + +#include "kudu/gutil/strings/substitute.h" +#include "kudu/util/status.h" + +// Forward declarations for the OpenSSL typedefs. +typedef struct X509_req_st X509_REQ; +typedef struct bio_st BIO; +typedef struct evp_pkey_st EVP_PKEY; +typedef struct ssl_ctx_st SSL_CTX; +typedef struct ssl_st SSL; +typedef struct x509_st X509; + +#define OPENSSL_CHECK_OK(call) \ + CHECK_GT((call), 0) + +#define OPENSSL_RET_NOT_OK(call, msg) \ + if ((call) <= 0) { \ + return Status::RuntimeError((msg), GetOpenSSLErrors()); \ + } + +#define OPENSSL_RET_IF_NULL(call, msg) \ + if ((call) == nullptr) { \ + return Status::RuntimeError((msg), GetOpenSSLErrors()); \ + } + +// Scoped helper which DCHECKs that on both scope entry and exit, there are no +// pending OpenSSL errors for the current thread. +// +// This allows us to avoid calling ERR_clear_error() defensively before every +// OpenSSL call, but rather call it only when we get an error code indicating +// there may be some pending error. +// +// Example usage: +// +// void MyFunc() { +// SCOPED_OPENSSL_NO_PENDING_ERRORS; +// ... use OpenSSL APIs ... +// } +#define SCOPED_OPENSSL_NO_PENDING_ERRORS \ + kudu::security::internal::ScopedCheckNoPendingSSLErrors _no_ssl_errors(__PRETTY_FUNCTION__) + +namespace kudu { +namespace security { + +// Disable initialization of OpenSSL. Must be called before +// any call to InitializeOpenSSL(). +Status DisableOpenSSLInitialization() WARN_UNUSED_RESULT; + +// Initializes static state required by the OpenSSL library. +// This is a no-op if DisableOpenSSLInitialization() has been called. +// +// Safe to call multiple times. +void InitializeOpenSSL(); + +// Fetches errors from the OpenSSL error error queue, and stringifies them. +// +// The error queue will be empty after this method returns. +// +// See man(3) ERR_get_err for more discussion. +std::string GetOpenSSLErrors(); + +// Returns a string representation of the provided error code, which must be +// from a prior call to the SSL_get_error function. +// +// If necessary, the OpenSSL error queue may be inspected and emptied as part of +// this call, and/or 'errno' may be inspected. As a result, this method should +// only be used directly after the error occurs, and from the same thread. +// +// See man(3) SSL_get_error for more discussion. +std::string GetSSLErrorDescription(int error_code); + +// A generic wrapper for OpenSSL structures. +template <typename T> +using c_unique_ptr = std::unique_ptr<T, std::function<void(T*)>>; + +// For each SSL type, the Traits class provides the important OpenSSL +// API functions. +template<typename SSL_TYPE> +struct SslTypeTraits {}; + +template<> struct SslTypeTraits<X509> { + static constexpr auto free = &X509_free; + static constexpr auto read_pem = &PEM_read_bio_X509; + static constexpr auto read_der = &d2i_X509_bio; + static constexpr auto write_pem = &PEM_write_bio_X509; + static constexpr auto write_der = &i2d_X509_bio; +}; +template<> struct SslTypeTraits<X509_EXTENSION> { + static constexpr auto free = &X509_EXTENSION_free; +}; +template<> struct SslTypeTraits<X509_REQ> { + static constexpr auto free = &X509_REQ_free; + static constexpr auto read_pem = &PEM_read_bio_X509_REQ; + static constexpr auto read_der = &d2i_X509_REQ_bio; + static constexpr auto write_pem = &PEM_write_bio_X509_REQ; + static constexpr auto write_der = &i2d_X509_REQ_bio; +}; +template<> struct SslTypeTraits<EVP_PKEY> { + static constexpr auto free = &EVP_PKEY_free; +}; +template<> struct SslTypeTraits<SSL_CTX> { + static constexpr auto free = &SSL_CTX_free; +}; + +template<typename SSL_TYPE, typename Traits = SslTypeTraits<SSL_TYPE>> +c_unique_ptr<SSL_TYPE> ssl_make_unique(SSL_TYPE* d) { + return {d, Traits::free}; +} + +// Acceptable formats for keys, X509 certificates and X509 CSRs. +enum class DataFormat { + DER = 0, // DER/ASN1 format (binary): for representing object on the wire + PEM = 1, // PEM format (ASCII): for storing on filesystem, printing, etc. +}; + +// Data format representation as a string. +const std::string& DataFormatToString(DataFormat fmt); + +// Template wrapper for dynamically allocated entities with custom deleter. +// Mostly, using it for xxx_st types from the OpenSSL crypto library. +template<typename Type> +class RawDataWrapper { + public: + typedef Type RawDataType; + + RawDataType* GetRawData() const { + return data_.get(); + } + + void AdoptRawData(RawDataType* d) { + data_ = ssl_make_unique(d); + } + + protected: + c_unique_ptr<RawDataType> data_; +}; + + +namespace internal { + +// Implementation of SCOPED_OPENSSL_NO_PENDING_ERRORS. Use the macro form +// instead of directly instantiating the implementation class. +struct ScopedCheckNoPendingSSLErrors { + public: + explicit ScopedCheckNoPendingSSLErrors(const char* func) + : func_(func) { + DCHECK_EQ(ERR_peek_error(), 0) + << "Expected no pending OpenSSL errors on " << func_ + << " entry, but had: " << GetOpenSSLErrors(); + } + ~ScopedCheckNoPendingSSLErrors() { + DCHECK_EQ(ERR_peek_error(), 0) + << "Expected no pending OpenSSL errors on " << func_ + << " exit, but had: " << GetOpenSSLErrors(); + } + + private: + const char* const func_; +}; + +} // namespace internal +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/openssl_util_bio.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/openssl_util_bio.h b/be/src/kudu/security/openssl_util_bio.h new file mode 100644 index 0000000..6ab6a55 --- /dev/null +++ b/be/src/kudu/security/openssl_util_bio.h @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include "kudu/security/openssl_util.h" + +#include <string> + +#include <glog/logging.h> +#include <openssl/bio.h> +#include <openssl/buffer.h> + +#include "kudu/gutil/strings/substitute.h" +#include "kudu/util/status.h" + +namespace kudu { +namespace security { + +template<> struct SslTypeTraits<BIO> { + static constexpr auto free = &BIO_free; +}; + +template<typename TYPE, typename Traits = SslTypeTraits<TYPE>> +Status ToBIO(BIO* bio, DataFormat format, TYPE* obj) { + CHECK(bio); + CHECK(obj); + switch (format) { + case DataFormat::DER: + OPENSSL_RET_NOT_OK(Traits::write_der(bio, obj), + "error exporting data in DER format"); + break; + case DataFormat::PEM: + OPENSSL_RET_NOT_OK(Traits::write_pem(bio, obj), + "error exporting data in PEM format"); + break; + } + OPENSSL_RET_NOT_OK(BIO_flush(bio), "error flushing BIO"); + return Status::OK(); +} + +template<typename TYPE, typename Traits = SslTypeTraits<TYPE>> +Status FromBIO(BIO* bio, DataFormat format, c_unique_ptr<TYPE>* ret) { + CHECK(bio); + switch (format) { + case DataFormat::DER: + *ret = ssl_make_unique(Traits::read_der(bio, nullptr)); + break; + case DataFormat::PEM: + *ret = ssl_make_unique(Traits::read_pem(bio, nullptr, nullptr, nullptr)); + break; + } + if (PREDICT_FALSE(!*ret)) { + return Status::RuntimeError(GetOpenSSLErrors()); + } + return Status::OK(); +} + +template<typename Type, typename Traits = SslTypeTraits<Type>> +Status FromString(const string& data, DataFormat format, + c_unique_ptr<Type>* ret) { + const void* mdata = reinterpret_cast<const void*>(data.data()); + auto bio = ssl_make_unique(BIO_new_mem_buf( +#if OPENSSL_VERSION_NUMBER < 0x10002000L + const_cast<void*>(mdata), +#else + mdata, +#endif + data.size())); + RETURN_NOT_OK_PREPEND((FromBIO<Type, Traits>(bio.get(), format, ret)), + "unable to load data from memory"); + return Status::OK(); +} + +template<typename Type, typename Traits = SslTypeTraits<Type>> +Status ToString(std::string* data, DataFormat format, Type* obj) { + CHECK(data); + auto bio = ssl_make_unique(BIO_new(BIO_s_mem())); + RETURN_NOT_OK_PREPEND((ToBIO<Type, Traits>(bio.get(), format, obj)), + "error serializing data"); + BUF_MEM* membuf; + OPENSSL_CHECK_OK(BIO_get_mem_ptr(bio.get(), &membuf)); + data->assign(membuf->data, membuf->length); + return Status::OK(); +} + +template<typename Type, typename Traits = SslTypeTraits<Type>> +Status FromFile(const string& fpath, DataFormat format, + c_unique_ptr<Type>* ret) { + auto bio = ssl_make_unique(BIO_new(BIO_s_file())); + OPENSSL_RET_NOT_OK(BIO_read_filename(bio.get(), fpath.c_str()), + strings::Substitute("could not read data from file '$0'", fpath)); + RETURN_NOT_OK_PREPEND((FromBIO<Type, Traits>(bio.get(), format, ret)), + strings::Substitute("unable to load data from file '$0'", fpath)); + return Status::OK(); +} + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/security-test-util.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/security-test-util.cc b/be/src/kudu/security/security-test-util.cc new file mode 100644 index 0000000..b2c8020 --- /dev/null +++ b/be/src/kudu/security/security-test-util.cc @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/security-test-util.h" + +#include <glog/logging.h> + +#include "kudu/security/ca/cert_management.h" +#include "kudu/security/cert.h" +#include "kudu/security/crypto.h" +#include "kudu/security/tls_context.h" +#include "kudu/util/path_util.h" +#include "kudu/util/test_util.h" + +namespace kudu { +namespace security { + +using ca::CaCertRequestGenerator; +using ca::CertSigner; + +Status GenerateSelfSignedCAForTests(PrivateKey* ca_key, Cert* ca_cert) { + static const int64_t kRootCaCertExpirationSeconds = 24 * 60 * 60; + // Create a key for the self-signed CA. + RETURN_NOT_OK(GeneratePrivateKey(512, ca_key)); + + CaCertRequestGenerator::Config config = { "test-ca-cn" }; + RETURN_NOT_OK(CertSigner::SelfSignCA(*ca_key, + config, + kRootCaCertExpirationSeconds, + ca_cert)); + return Status::OK(); +} + +std::ostream& operator<<(std::ostream& o, PkiConfig c) { + switch (c) { + case PkiConfig::NONE: o << "NONE"; break; + case PkiConfig::SELF_SIGNED: o << "SELF_SIGNED"; break; + case PkiConfig::TRUSTED: o << "TRUSTED"; break; + case PkiConfig::SIGNED: o << "SIGNED"; break; + case PkiConfig::EXTERNALLY_SIGNED: o << "EXTERNALLY_SIGNED"; break; + } + return o; +} + +Status ConfigureTlsContext(PkiConfig config, + const Cert& ca_cert, + const PrivateKey& ca_key, + TlsContext* tls_context) { + switch (config) { + case PkiConfig::NONE: break; + case PkiConfig::SELF_SIGNED: + RETURN_NOT_OK(tls_context->GenerateSelfSignedCertAndKey()); + break; + case PkiConfig::TRUSTED: + RETURN_NOT_OK(tls_context->AddTrustedCertificate(ca_cert)); + break; + case PkiConfig::SIGNED: { + RETURN_NOT_OK(tls_context->AddTrustedCertificate(ca_cert)); + RETURN_NOT_OK(tls_context->GenerateSelfSignedCertAndKey()); + Cert cert; + RETURN_NOT_OK(CertSigner(&ca_cert, &ca_key).Sign(*tls_context->GetCsrIfNecessary(), &cert)); + RETURN_NOT_OK(tls_context->AdoptSignedCert(cert)); + break; + }; + case PkiConfig::EXTERNALLY_SIGNED: { + // Write certificate to file. + std::string cert_path = JoinPathSegments(GetTestDataDirectory(), "kudu-test-cert.pem"); + RETURN_NOT_OK(CreateSSLServerCert(cert_path)); + // Write private key to file. + std::string key_path = JoinPathSegments(GetTestDataDirectory(), "kudu-test-key.pem"); + RETURN_NOT_OK(CreateSSLPrivateKey(key_path)); + RETURN_NOT_OK(tls_context->LoadCertificateAndKey(cert_path, key_path)); + RETURN_NOT_OK(tls_context->LoadCertificateAuthority(cert_path)); + }; + } + return Status::OK(); +} + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/security-test-util.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/security-test-util.h b/be/src/kudu/security/security-test-util.h new file mode 100644 index 0000000..f0b8977 --- /dev/null +++ b/be/src/kudu/security/security-test-util.h @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include <ostream> +#include <string> + +#include "kudu/util/env.h" +#include "kudu/util/status.h" + +namespace kudu { +namespace security { + +class Cert; +class PrivateKey; +class PublicKey; +class TlsContext; + +// TODO(todd): consolidate these certs with those in +// security/test/test_certs.h once we support configuring a password +// for the RPC cert. +inline static Status CreateSSLServerCert(const std::string& file_path) { + static const char* test_server_cert = R"( +-----BEGIN CERTIFICATE----- +MIIEejCCA2KgAwIBAgIJAKMdvDR5PL82MA0GCSqGSIb3DQEBBQUAMIGEMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5j +aXNjbzERMA8GA1UEChMIQ2xvdWRlcmExEjAQBgNVBAMTCWxvY2FsaG9zdDEhMB8G +CSqGSIb3DQEJARYSaGVucnlAY2xvdWRlcmEuY29tMB4XDTEzMDkyMjAwMjUxOFoX +DTQxMDIwNzAwMjUxOFowgYQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y +bmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMREwDwYDVQQKEwhDbG91ZGVyYTES +MBAGA1UEAxMJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJoZW5yeUBjbG91ZGVy +YS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoUj3pMQ2ELkrz +zq+koixljVFBAEEqwUWSjA+GEKwfFb/UPRjeO/wrKndp2r83jc6KRt66rvAIl8cr +b54yTOsJ/ZcARrjTwG3IG8Tely/54ZQyH0ImdJyEbCSoI04zX3ovjlppz3g5xanj +WmpAh6pzPgBOTfisCLMPD70xQ8F//QWZdNatoly54STkTWoJv/Oll/UpXcBY8JOR ++ytX82eGgG4F8YoQqmbjrrN5JAmqLRiBAkr3WUena6ekqJBalJRzex/Wh8a9XEV7 +9HFVVngBhezsOJgf81hzBzzhULKfxuXl8uaUj3Z9cZg39CDsyz+ULYbsPm8VoMUI +VCf7MUVTAgMBAAGjgewwgekwHQYDVR0OBBYEFK94kea7jIKQawAIb+0DqsA1rf6n +MIG5BgNVHSMEgbEwga6AFK94kea7jIKQawAIb+0DqsA1rf6noYGKpIGHMIGEMQsw +CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy +YW5jaXNjbzERMA8GA1UEChMIQ2xvdWRlcmExEjAQBgNVBAMTCWxvY2FsaG9zdDEh +MB8GCSqGSIb3DQEJARYSaGVucnlAY2xvdWRlcmEuY29tggkAox28NHk8vzYwDAYD +VR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAEtkPPncCnN2IFVJvz04K+VsX +b6w3qwPynQKc67+++JkNb3TYKrh/0UVM1NrEOu3TGplqOrKgAlITuaWNqNOSBu1R +WJtrz85YkonED5awjjuALVEY82+c7pOXkuv5G5421RINfRn2hNzgw8VFb5CEvxHH +jER80Vx6UGKr/S649qTQ8AzVzTwWS86VsGI2azAD7D67G/IDGf+0P7FsXonKY+vl +vKzkfaO1+qEOLtDHV9mwlsxl3Re/MNym4ExWHi9txynCNiRZHqWoZUS+KyYqIR2q +seCrQwgi1Fer9Ekd5XNjWjigC3VC3SjMqWaxeKbZ2/AuABJMz5xSiRkgwphXEQ== +-----END CERTIFICATE----- + )"; + RETURN_NOT_OK(WriteStringToFile(Env::Default(), test_server_cert, file_path)); + return Status::OK(); +} + +// Writes the test SSL private key into a temporary file. +inline static Status CreateSSLPrivateKey(const std::string& file_path) { + static const char* test_private_key = R"( +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqFI96TENhC5K886vpKIsZY1RQQBBKsFFkowPhhCsHxW/1D0Y +3jv8Kyp3adq/N43Oikbeuq7wCJfHK2+eMkzrCf2XAEa408BtyBvE3pcv+eGUMh9C +JnSchGwkqCNOM196L45aac94OcWp41pqQIeqcz4ATk34rAizDw+9MUPBf/0FmXTW +raJcueEk5E1qCb/zpZf1KV3AWPCTkfsrV/NnhoBuBfGKEKpm466zeSQJqi0YgQJK +91lHp2unpKiQWpSUc3sf1ofGvVxFe/RxVVZ4AYXs7DiYH/NYcwc84VCyn8bl5fLm +lI92fXGYN/Qg7Ms/lC2G7D5vFaDFCFQn+zFFUwIDAQABAoIBABNTpiIxbLDhs998 +uvQ3XsumR08kXVcwa/GgvWOSZIEJOUaAYWubDaBTNvTjlhMl6DI+YvKihZMnAkp9 +fXefF1nFUWJJvI0ryi8w6RD54RtbCG4c4raRqysVU7wumZsSenAdc0o09UQE6zXc +uth/+1VSKCzVjRkLwquXDg0rD3vHfQHWQvQlzwUh3OACA3LfLezVqzrEB02YVRxm +xwg5veeMg6Aod8vsvsajry9eE0hKeFGonRANerL9lwZxzD2ZjU2fSEJYY3xxKVgi +o+QVTKaAt9pivOs10YVZDcIDH0xmDpxAkaLb5ZAbnjwhf7WGYgEm8VruAHkJxyXX +yPf3rpkCgYEA0dp/Xv5KBIkD6JJao8hnhtP5x9U7o/pTzRxaO3WUflvTI6DtC0nk +cTOwFVs4WljT76T937q2x4stuX0woHzvIaZ6wwZ2vv81ikDY6pE8eLWsH/xFAmkx +HBfkSijFgJV6EpTqUnFD7QKU89tzWrh/kxaMO1WgFaBhxPPs3K1LDTUCgYEAzVW5 +3yjfVHNgjWTeAbnbToGvUihOidvIvS5cVo5q0Dhfabz0tiXFxAoQUGErUGPC8Nu2 +n/HxTI3b0PbCCwjBsDNqX2kzxTSe5aTGIrBUWbped1bxme8jggXuWYbg8vvLpsYf +wAJPxnGIxW/v/aftHUhbTIuVfZX2+UnilrwiwWcCgYEAg8paz4eXaH277KVtMwq6 +qZwac/tgNz0Qv/GcYVcYaLq2QNvhwoMnakhxvxfIrkS25PuTTJxwCaVIlAMhNMkB +TPrGghBfJtgUAb1z/Ow1NAG0FWpS1I7HfsMqZcBxOK2nOmA3QItNg11pujQJn+Ha +jL9OVj0SCkLs48nk6ToTtjkCgYEAh8YCtNwq6IWuN3CWGCAUMpIwIqxCWof48Zch +OZ7MZEiSVrG6QmMxpRJefTfzUyHUOj2eQZ7SxqMa0c8IuhEdOeyVjudaczD7TLAq +z68252oDovfbo8Tr/sL7OzmjryfuHqXtQqKEq5xRKvR8hYavlGhO7otx2uv5thcz +/CYE+UsCgYAsgewfzbcBqJwsAUs98DK99EU8VqKuaYlU5wNvAVb27O6sVeysSokM +G1TGIXJPphA3dSAe4Pf/j4ff/eGaS20FAFhs4BPpw0fAeCHpmD0BjIba0lxBS/gY +dc+JVPKL8Fe4a8fmsI6ndcZQ9qpOdZM5WOD0ldKRc+SsrYKkTmOOJQ== +-----END RSA PRIVATE KEY----- + )"; + RETURN_NOT_OK(WriteStringToFile(Env::Default(), test_private_key, file_path)); + return Status::OK(); +} + +Status GenerateSelfSignedCAForTests(PrivateKey* ca_key, Cert* ca_cert); + +// Describes the options for configuring a TlsContext. +enum class PkiConfig { + // The TLS context has no TLS cert and no trusted certs. + NONE, + // The TLS context has a self-signed TLS cert and no trusted certs. + SELF_SIGNED, + // The TLS context has no TLS cert and a trusted cert. + TRUSTED, + // The TLS context has a signed TLS cert and trusts the corresponding signing cert. + SIGNED, + // The TLS context has a externally signed TLS cert and trusts the corresponding signing cert. + EXTERNALLY_SIGNED, +}; + +// PkiConfig pretty-printer. +std::ostream& operator<<(std::ostream& o, PkiConfig c); + +Status ConfigureTlsContext(PkiConfig config, + const Cert& ca_cert, + const PrivateKey& ca_key, + TlsContext* tls_context); + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/simple_acl.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/simple_acl.cc b/be/src/kudu/security/simple_acl.cc new file mode 100644 index 0000000..75b06a2 --- /dev/null +++ b/be/src/kudu/security/simple_acl.cc @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/simple_acl.h" + +#include <ctype.h> + +#include <vector> + +#include "kudu/gutil/map-util.h" +#include "kudu/gutil/strings/split.h" +#include "kudu/gutil/strings/stringpiece.h" +#include "kudu/util/status.h" + +using std::string; +using std::vector; + +namespace kudu { +namespace security { + +SimpleAcl::SimpleAcl() { +} + +SimpleAcl::~SimpleAcl() { +} + +Status SimpleAcl::ParseFlag(const string& flag) { + vector<StringPiece> fields = strings::Split(flag, ",", strings::SkipWhitespace()); + set<string> users; + for (const auto& field : fields) { + if (field.empty()) continue; + // if any field is a wildcard, no need to include the rest. + if (flag == "*") { + Reset({"*"}); + return Status::OK(); + } + + + // Leave open the use of various special characters at the start of each + // username. We reserve some special characters that might be useful in + // ACLs: + // '!': might be interpreted as "not" + // '@': often used to read data from a file + // '#': comments + // '$': maybe variable expansion? + // '%': used by sudoers for groups + // '*': only allowed for special wildcard ACL above + // '-', '+', '=': useful for allow/deny style ACLs + // <quote characters>: in case we want to add quoted strings + // whitespace: down right confusing + static const char* kReservedStartingCharacters = "!@#$%*-=+'\""; + if (strchr(kReservedStartingCharacters, field[0]) || + isspace(field[0])) { + return Status::NotSupported("invalid username", field.ToString()); + } + + users.insert(field.ToString()); + } + + Reset(std::move(users)); + return Status::OK(); +} + +void SimpleAcl::Reset(set<string> users) { + users_ = std::move(users); +} + +bool SimpleAcl::UserAllowed(const string& username) { + return ContainsKey(users_, "*") || ContainsKey(users_, username); +} + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/simple_acl.h ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/simple_acl.h b/be/src/kudu/security/simple_acl.h new file mode 100644 index 0000000..96ca598 --- /dev/null +++ b/be/src/kudu/security/simple_acl.h @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +#pragma once + +#include "kudu/gutil/macros.h" + +#include <set> +#include <string> + +namespace kudu { +class Status; + +namespace security { + +// Represent a very simple access control list which contains a set of users. +// +// This is basically just a wrapper around a set<string> with a bit of parsing logic and +// support for the '*' wildcard. +class SimpleAcl { + public: + SimpleAcl(); + ~SimpleAcl(); + + // Parse a flag value, which should be of the form 'user1,user2,user3' to indicate a + // list of users, or '*' to indicate a wildcard. This syntax may be expanded later to + // include groups, "allow/deny" style access, etc. + // + // Thread-unsafe: must be called before the ACL may be consulted, and may not be + // called a second time concurrent with reads of the ACL. + Status ParseFlag(const std::string& flag); + + // Return true if the given user is allowed by the ACL. + // + // Thread-safe after initialization. + bool UserAllowed(const std::string& username); + + // Reset the ACL to the specific set of usernames. + void Reset(std::set<std::string> users); + + private: + // The set of users, or a set with the single value '*' for the wildcard. + std::set<std::string> users_; +}; + +} // namespace security +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/test/mini_kdc-test.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/test/mini_kdc-test.cc b/be/src/kudu/security/test/mini_kdc-test.cc new file mode 100644 index 0000000..01ed84d --- /dev/null +++ b/be/src/kudu/security/test/mini_kdc-test.cc @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include <string> + +#include <boost/optional.hpp> +#include <gflags/gflags.h> +#include <gtest/gtest.h> + +#include "kudu/security/init.h" +#include "kudu/security/test/mini_kdc.h" +#include "kudu/util/env.h" +#include "kudu/util/test_util.h" + +using std::string; + +DECLARE_string(keytab_file); +DECLARE_string(principal); + +namespace kudu { + +class MiniKdcTest : public KuduTest {}; + +TEST_F(MiniKdcTest, TestBasicOperation) { + MiniKdcOptions options; + MiniKdc kdc(options); + ASSERT_OK(kdc.Start()); + ASSERT_GT(kdc.port(), 0); + ASSERT_OK(kdc.CreateUserPrincipal("alice")); + ASSERT_OK(kdc.Kinit("alice")); + + ASSERT_OK(kdc.Stop()); + ASSERT_OK(kdc.Start()); + + // Check that alice is kinit'd. + string klist; + ASSERT_OK(kdc.Klist(&klist)); + ASSERT_STR_CONTAINS(klist, "[email protected]"); + + ASSERT_OK(kdc.CreateUserPrincipal("bob")); + ASSERT_OK(kdc.Kinit("bob")); + + // Check that bob has replaced alice as the kinit'd principal. + ASSERT_OK(kdc.Klist(&klist)); + ASSERT_STR_NOT_CONTAINS(klist, "[email protected]"); + ASSERT_STR_CONTAINS(klist, "[email protected]"); + ASSERT_STR_CONTAINS(klist, "krbtgt/[email protected]"); + + // Drop 'bob' credentials. We'll get a RuntimeError because klist + // exits with a non-zero exit code if there are no cached credentials. + ASSERT_OK(kdc.Kdestroy()); + ASSERT_TRUE(kdc.Klist(&klist).IsRuntimeError()); + + // Test keytab creation. + const string kSPN = "kudu/foo.example.com"; + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab(kSPN, &kt_path)); + SCOPED_TRACE(kt_path); + ASSERT_OK(kdc.KlistKeytab(kt_path, &klist)); + ASSERT_STR_CONTAINS(klist, "kudu/[email protected]"); + + // Test programmatic keytab login. + kdc.SetKrb5Environment(); + FLAGS_keytab_file = kt_path; + FLAGS_principal = kSPN; + ASSERT_OK(security::InitKerberosForServer()); + ASSERT_EQ("kudu/[email protected]", *security::GetLoggedInPrincipalFromKeytab()); + + // Test principal canonicalization. + string princ = "foo"; + ASSERT_OK(security::CanonicalizeKrb5Principal(&princ)); + ASSERT_EQ("[email protected]", princ); + + // Test auth-to-local mapping for a user from the local realm as well as a remote realm. + { + string local_user; + ASSERT_OK(security::MapPrincipalToLocalName("[email protected]", &local_user)); + ASSERT_EQ("foo", local_user); + + ASSERT_OK(security::MapPrincipalToLocalName("foo/[email protected]", &local_user)); + ASSERT_EQ("foo", local_user); + + // The Heimdal implementation in macOS does not correctly implement auth to + // local mapping (see init.cc). +#ifndef __APPLE__ + ASSERT_OK(security::MapPrincipalToLocalName("[email protected]", &local_user)); + ASSERT_EQ("other-foo", local_user); +#endif + } +} + +// Regression test to ensure that dropping a stopped MiniKdc doesn't panic. +TEST_F(MiniKdcTest, TestStopDrop) { + MiniKdcOptions options; + MiniKdc kdc(options); +} + +TEST_F(MiniKdcTest, TestOperationsWhenKdcNotRunning) { + MiniKdcOptions options; + MiniKdc kdc(options); + ASSERT_OK(kdc.Start()); + ASSERT_OK(kdc.Stop()); + + // MiniKdc::CreateUserPrincipal() works directly with the local files, + // so it should work fine even if KDC is shut down. + ASSERT_OK(kdc.CreateUserPrincipal("alice")); + + { + // Without running KDC it should not be possible to obtain and cache an + // initial ticket-granting ticket for principal. + const Status s = kdc.Kinit("alice"); + ASSERT_TRUE(s.IsRuntimeError()) << s.ToString(); + ASSERT_STR_CONTAINS(s.ToString(), "process exited with non-zero status"); + } + { + // Without running KDC klist should fail. + string klist; + const Status s = kdc.Klist(&klist); + ASSERT_TRUE(s.IsRuntimeError()) << s.ToString(); + ASSERT_STR_CONTAINS(s.ToString(), "process exited with non-zero status"); + } + + ASSERT_OK(kdc.Start()); + + // Once KDC has started, 'kinit' and 'klist' should work with no issues. + ASSERT_OK(kdc.Kinit("alice")); + { + // Check that alice is kinit'd. + string klist; + ASSERT_OK(kdc.Klist(&klist)); + ASSERT_STR_CONTAINS(klist, "[email protected]"); + } +} + +} // namespace kudu http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/84b8155c/be/src/kudu/security/test/mini_kdc.cc ---------------------------------------------------------------------- diff --git a/be/src/kudu/security/test/mini_kdc.cc b/be/src/kudu/security/test/mini_kdc.cc new file mode 100644 index 0000000..4b84bf2 --- /dev/null +++ b/be/src/kudu/security/test/mini_kdc.cc @@ -0,0 +1,390 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "kudu/security/test/mini_kdc.h" + +#include <csignal> +#include <stdlib.h> + +#include <limits> +#include <map> +#include <memory> +#include <string> + +#include <glog/logging.h> + +#include "kudu/gutil/gscoped_ptr.h" +#include "kudu/gutil/strings/numbers.h" +#include "kudu/gutil/strings/split.h" +#include "kudu/gutil/strings/strip.h" +#include "kudu/gutil/strings/substitute.h" +#include "kudu/util/env.h" +#include "kudu/util/monotime.h" +#include "kudu/util/path_util.h" +#include "kudu/util/stopwatch.h" +#include "kudu/util/subprocess.h" +#include "kudu/util/test_util.h" + +using std::map; +using std::string; +using std::unique_ptr; +using strings::Substitute; + +namespace kudu { + +string MiniKdcOptions::ToString() const { + return strings::Substitute("{ realm: $0, port: $1, data_root: $2 }", realm, port, data_root); +} + +MiniKdc::MiniKdc() + : MiniKdc(MiniKdcOptions()) { +} + +MiniKdc::MiniKdc(const MiniKdcOptions& options) + : options_(options) { + if (options_.realm.empty()) { + options_.realm = "KRBTEST.COM"; + } + if (options_.data_root.empty()) { + options_.data_root = JoinPathSegments(GetTestDataDirectory(), "krb5kdc"); + } + if (options_.renew_lifetime.empty()) { + options_.renew_lifetime = "7d"; + } + if (options_.ticket_lifetime.empty()) { + options_.ticket_lifetime = "24h"; + } +} + +MiniKdc::~MiniKdc() { + if (kdc_process_) { + WARN_NOT_OK(Stop(), "Unable to stop MiniKdc"); + } +} + +map<string, string> MiniKdc::GetEnvVars() const { + return { + {"KRB5_CONFIG", JoinPathSegments(options_.data_root, "krb5.conf")}, + {"KRB5_KDC_PROFILE", JoinPathSegments(options_.data_root, "kdc.conf")}, + {"KRB5CCNAME", JoinPathSegments(options_.data_root, "krb5cc")} + }; +} + +vector<string> MiniKdc::MakeArgv(const vector<string>& in_argv) { + vector<string> real_argv = { "env" }; + for (const auto& p : GetEnvVars()) { + real_argv.push_back(Substitute("$0=$1", p.first, p.second)); + } + for (const string& a : in_argv) { + real_argv.push_back(a); + } + return real_argv; +} + +namespace { +// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. +Status GetBinaryPath(const string& binary, + const vector<string>& search, + string* path) { + string p; + + // First, check specified locations which are sometimes not on the PATH. + // This is necessary to check first so that the system Heimdal kerberos + // binaries won't be found first on OS X. + for (const auto& location : search) { + p = JoinPathSegments(location, binary); + if (Env::Default()->FileExists(p)) { + *path = p; + return Status::OK(); + } + } + + // Next check if the binary is on the PATH. + Status s = Subprocess::Call({ "which", binary }, "", &p); + if (s.ok()) { + StripTrailingNewline(&p); + *path = p; + return Status::OK(); + } + + return Status::NotFound("Unable to find binary", binary); +} + +// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. +Status GetBinaryPath(const string& binary, string* path) { + static const vector<string> kCommonLocations = { + "/usr/local/opt/krb5/sbin", // Homebrew + "/usr/local/opt/krb5/bin", // Homebrew + "/opt/local/sbin", // Macports + "/opt/local/bin", // Macports + "/usr/lib/mit/sbin", // SLES + "/usr/sbin", // Linux + }; + return GetBinaryPath(binary, kCommonLocations, path); +} +} // namespace + + +Status MiniKdc::Start() { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, "starting KDC"); + CHECK(!kdc_process_); + VLOG(1) << "Starting Kerberos KDC: " << options_.ToString(); + + if (!Env::Default()->FileExists(options_.data_root)) { + VLOG(1) << "Creating KDC database and configuration files"; + RETURN_NOT_OK(Env::Default()->CreateDir(options_.data_root)); + + RETURN_NOT_OK(CreateKdcConf()); + RETURN_NOT_OK(CreateKrb5Conf()); + + // Create the KDC database using the kdb5_util tool. + string kdb5_util_bin; + RETURN_NOT_OK(GetBinaryPath("kdb5_util", &kdb5_util_bin)); + + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kdb5_util_bin, "create", + "-s", // Stash the master password. + "-P", "masterpw", // Set a password. + "-W", // Use weak entropy (since we don't need real security). + }))); + } + + // Start the Kerberos KDC. + string krb5kdc_bin; + RETURN_NOT_OK(GetBinaryPath("krb5kdc", &krb5kdc_bin)); + + kdc_process_.reset(new Subprocess( + MakeArgv({ + krb5kdc_bin, + "-n", // Do not daemonize. + }))); + + RETURN_NOT_OK(kdc_process_->Start()); + + const bool need_config_update = (options_.port == 0); + // Wait for KDC to start listening on its ports and commencing operation. + RETURN_NOT_OK(WaitForKdcPorts()); + + if (need_config_update) { + // If we asked for an ephemeral port, grab the actual ports and + // rewrite the configuration so that clients can connect. + RETURN_NOT_OK(CreateKrb5Conf()); + RETURN_NOT_OK(CreateKdcConf()); + } + + return Status::OK(); +} + +Status MiniKdc::Stop() { + CHECK(kdc_process_); + VLOG(1) << "Stopping KDC"; + unique_ptr<Subprocess> proc(kdc_process_.release()); + RETURN_NOT_OK(proc->Kill(SIGKILL)); + RETURN_NOT_OK(proc->Wait()); + + return Status::OK(); +} + +// Creates a kdc.conf file according to the provided options. +Status MiniKdc::CreateKdcConf() const { + static const string kFileTemplate = R"( +[kdcdefaults] +kdc_ports = $2 +kdc_tcp_ports = "" + +[realms] +$1 = { + acl_file = $0/kadm5.acl + admin_keytab = $0/kadm5.keytab + database_name = $0/principal + key_stash_file = $0/.k5.$1 + max_renewable_life = 7d 0h 0m 0s +} + )"; + string file_contents = strings::Substitute(kFileTemplate, options_.data_root, + options_.realm, options_.port); + return WriteStringToFile(Env::Default(), file_contents, + JoinPathSegments(options_.data_root, "kdc.conf")); +} + +// Creates a krb5.conf file according to the provided options. +Status MiniKdc::CreateKrb5Conf() const { + static const string kFileTemplate = R"( +[logging] + kdc = FILE:/dev/stderr + +[libdefaults] + default_realm = $1 + dns_lookup_kdc = false + dns_lookup_realm = false + forwardable = true + renew_lifetime = $2 + ticket_lifetime = $3 + + # In miniclusters, we start daemons on local loopback IPs that + # have no reverse DNS entries. So, disable reverse DNS. + rdns = false + + # The server side will start its GSSAPI server using the local FQDN. + # However, in tests, we connect to it via a non-matching loopback IP. + # This enables us to connect despite that mismatch. + ignore_acceptor_hostname = true + +[realms] + $1 = { + kdc = 127.0.0.1:$0 + # This super-arcane syntax can be found documented in various Hadoop + # vendors' security guides and very briefly in the MIT krb5 docs. + # Basically, this one says to map anyone coming in as [email protected] + # and map them to a local user 'other-foo' + auth_to_local = RULE:[1:other-$$1@$$0](.*@OTHERREALM.COM$$)s/@.*// + } + )"; + string file_contents = strings::Substitute(kFileTemplate, options_.port, options_.realm, + options_.renew_lifetime, options_.ticket_lifetime); + return WriteStringToFile(Env::Default(), file_contents, + JoinPathSegments(options_.data_root, "krb5.conf")); +} + +Status MiniKdc::WaitForKdcPorts() { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("waiting for KDC ports")); + // We have to use 'lsof' to figure out which ports the KDC bound to if we + // requested ephemeral ones. The KDC doesn't log the bound port or expose it + // in any other fashion, and re-implementing lsof involves parsing a lot of + // files in /proc/. So, requiring lsof for tests and parsing its output seems + // more straight-forward. We call lsof in a loop in case the kdc is slow to + // bind to the ports. + + string lsof; + RETURN_NOT_OK(GetBinaryPath("lsof", {"/sbin", "/usr/sbin"}, &lsof)); + + const vector<string> cmd = { + lsof, "-wbnP", "-Ffn", + "-p", std::to_string(kdc_process_->pid()), + "-a", "-i", "4UDP"}; + + string lsof_out; + for (int i = 1; ; i++) { + lsof_out.clear(); + Status s = Subprocess::Call(cmd, "", &lsof_out); + + if (s.ok()) { + StripTrailingNewline(&lsof_out); + break; + } else if (i > 10) { + return s; + } + + SleepFor(MonoDelta::FromMilliseconds(i * i)); + } + + // The '-Ffn' flag gets lsof to output something like: + // p19730 + // f123 + // n*:41254 + // The first line is the pid. We ignore it. + // The second line is the file descriptor number. We ignore it. + // The third line has the bind address and port. + vector<string> lines = strings::Split(lsof_out, "\n"); + int32_t port = -1; + if (lines.size() != 3 || + lines[2].substr(0, 3) != "n*:" || + !safe_strto32(lines[2].substr(3), &port) || + port <= 0) { + return Status::RuntimeError("unexpected lsof output", lsof_out); + } + CHECK(port > 0 && port < std::numeric_limits<uint16_t>::max()) + << "parsed invalid port: " << port; + VLOG(1) << "Determined bound KDC port: " << port; + if (options_.port == 0) { + options_.port = port; + } else { + // Sanity check: if KDC's port is already established, it's supposed to be + // written into the configuration files, so the process must bind to the + // already established port. + CHECK(options_.port == port); + } + return Status::OK(); +} + +Status MiniKdc::CreateUserPrincipal(const string& username) { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("creating user principal $0", username)); + string kadmin; + RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("add_principal -pw $0 $0", username)}))); + return Status::OK(); +} + +Status MiniKdc::CreateServiceKeytab(const string& spn, + string* path) { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("creating service keytab for $0", spn)); + string kt_path = spn; + StripString(&kt_path, "/", '_'); + kt_path = JoinPathSegments(options_.data_root, kt_path) + ".keytab"; + + string kadmin; + RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("add_principal -randkey $0", spn)}))); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("ktadd -k $0 $1", kt_path, spn)}))); + *path = kt_path; + return Status::OK(); +} + +Status MiniKdc::Kinit(const string& username) { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("kinit for $0", username)); + string kinit; + RETURN_NOT_OK(GetBinaryPath("kinit", &kinit)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ kinit, username }), username)); + return Status::OK(); +} + +Status MiniKdc::Kdestroy() { + SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, "kdestroy"); + string kdestroy; + RETURN_NOT_OK(GetBinaryPath("kdestroy", &kdestroy)); + return Subprocess::Call(MakeArgv({ kdestroy, "-A" })); +} + +Status MiniKdc::Klist(string* output) { + string klist; + RETURN_NOT_OK(GetBinaryPath("klist", &klist)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-A" }), "", output)); + return Status::OK(); +} + +Status MiniKdc::KlistKeytab(const string& keytab_path, string* output) { + string klist; + RETURN_NOT_OK(GetBinaryPath("klist", &klist)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-k", keytab_path }), "", output)); + return Status::OK(); +} + +Status MiniKdc::SetKrb5Environment() const { + if (!kdc_process_) { + return Status::IllegalState("KDC not started"); + } + for (const auto& p : GetEnvVars()) { + CHECK_ERR(setenv(p.first.c_str(), p.second.c_str(), 1 /*overwrite*/)); + } + + return Status::OK(); +} + +} // namespace kudu
