blob: 6dcc26ab266bfe8e15b0953d1025b1d379feac68 [file] [log] [blame]
// 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/rpc/client_negotiation.h"
#include <gssapi/gssapi.h>
#include <gssapi/gssapi_krb5.h>
#include <sasl/sasl.h>
#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <ostream>
#include <set>
#include <string>
#include <gflags/gflags_declare.h>
#include <glog/logging.h>
#include "kudu/gutil/basictypes.h"
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/stl_util.h"
#include "kudu/gutil/strings/join.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/rpc/blocking_ops.h"
#include "kudu/rpc/constants.h"
#include "kudu/rpc/rpc_header.pb.h"
#include "kudu/rpc/sasl_common.h"
#include "kudu/rpc/sasl_helper.h"
#include "kudu/rpc/serialization.h"
#include "kudu/security/cert.h"
#include "kudu/security/gssapi.h"
#include "kudu/security/tls_context.h"
#include "kudu/security/tls_handshake.h"
#include "kudu/security/token.pb.h"
#include "kudu/util/debug/leakcheck_disabler.h"
#include "kudu/util/faststring.h"
#include "kudu/util/net/sockaddr.h"
#include "kudu/util/net/socket.h"
#include "kudu/util/slice.h"
#include "kudu/util/trace.h"
#if defined(__APPLE__)
// Almost all functions in the SASL API are marked as deprecated
// since macOS 10.11.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif // #if defined(__APPLE__)
DECLARE_bool(rpc_encrypt_loopback_connections);
using kudu::security::RpcEncryption;
using std::set;
using std::string;
using std::unique_ptr;
using strings::Substitute;
namespace kudu {
namespace rpc {
static int ClientNegotiationGetoptCb(ClientNegotiation* client_negotiation,
const char* plugin_name,
const char* option,
const char** result,
unsigned* len) {
return client_negotiation->GetOptionCb(plugin_name, option, result, len);
}
static int ClientNegotiationSimpleCb(ClientNegotiation* client_negotiation,
int id,
const char** result,
unsigned* len) {
return client_negotiation->SimpleCb(id, result, len);
}
static int ClientNegotiationSecretCb(sasl_conn_t* conn,
ClientNegotiation* client_negotiation,
int id,
sasl_secret_t** psecret) {
return client_negotiation->SecretCb(conn, id, psecret);
}
// Return an appropriately-typed Status object based on an ErrorStatusPB returned
// from an Error RPC.
// In case there is no relevant Status type, return a RuntimeError.
static Status StatusFromRpcError(const ErrorStatusPB& error) {
DCHECK(error.IsInitialized()) << "Error status PB must be initialized";
if (PREDICT_FALSE(!error.has_code())) {
return Status::RuntimeError(error.message());
}
const string code_name = ErrorStatusPB::RpcErrorCodePB_Name(error.code());
switch (error.code()) {
case ErrorStatusPB_RpcErrorCodePB_FATAL_UNAUTHORIZED: // fall-through
case ErrorStatusPB_RpcErrorCodePB_FATAL_INVALID_AUTHENTICATION_TOKEN:
return Status::NotAuthorized(code_name, error.message());
case ErrorStatusPB_RpcErrorCodePB_ERROR_UNAVAILABLE:
return Status::ServiceUnavailable(code_name, error.message());
default:
return Status::RuntimeError(code_name, error.message());
}
}
ClientNegotiation::ClientNegotiation(unique_ptr<Socket> socket,
const security::TlsContext* tls_context,
std::optional<security::SignedTokenPB> authn_token,
RpcEncryption encryption,
bool encrypt_loopback,
std::string sasl_proto_name)
: socket_(std::move(socket)),
helper_(SaslHelper::CLIENT),
tls_context_(tls_context),
tls_handshake_(security::TlsHandshakeType::CLIENT),
encryption_(encryption),
tls_negotiated_(false),
encrypt_loopback_(encrypt_loopback),
authn_token_(std::move(authn_token)),
psecret_(nullptr, std::free),
negotiated_authn_(AuthenticationType::INVALID),
negotiated_mech_(SaslMechanism::INVALID),
sasl_proto_name_(std::move(sasl_proto_name)),
deadline_(MonoTime::Max()) {
callbacks_.push_back(SaslBuildCallback(SASL_CB_GETOPT,
reinterpret_cast<int (*)()>(&ClientNegotiationGetoptCb), this));
callbacks_.push_back(SaslBuildCallback(SASL_CB_AUTHNAME,
reinterpret_cast<int (*)()>(&ClientNegotiationSimpleCb), this));
callbacks_.push_back(SaslBuildCallback(SASL_CB_PASS,
reinterpret_cast<int (*)()>(&ClientNegotiationSecretCb), this));
callbacks_.push_back(SaslBuildCallback(SASL_CB_LIST_END, nullptr, nullptr));
DCHECK(socket_);
DCHECK(tls_context_);
}
Status ClientNegotiation::EnablePlain(const string& user, const string& pass) {
RETURN_NOT_OK(helper_.EnablePlain());
plain_auth_user_ = user;
plain_pass_ = pass;
return Status::OK();
}
Status ClientNegotiation::EnableGSSAPI() {
return helper_.EnableGSSAPI();
}
SaslMechanism::Type ClientNegotiation::negotiated_mechanism() const {
return negotiated_mech_;
}
void ClientNegotiation::set_server_fqdn(const string& domain_name) {
helper_.set_server_fqdn(domain_name);
}
void ClientNegotiation::set_deadline(const MonoTime& deadline) {
deadline_ = deadline;
}
Status ClientNegotiation::Negotiate(unique_ptr<ErrorStatusPB>* rpc_error) {
TRACE("Beginning negotiation");
// Ensure we can use blocking calls on the socket during negotiation.
RETURN_NOT_OK(CheckInBlockingMode(socket_.get()));
// Step 1: send the connection header.
RETURN_NOT_OK(SendConnectionHeader());
faststring recv_buf;
{ // Step 2: send and receive the NEGOTIATE step messages.
RETURN_NOT_OK(SendNegotiate());
NegotiatePB response;
RETURN_NOT_OK(RecvNegotiatePB(&response, &recv_buf, rpc_error));
RETURN_NOT_OK(HandleNegotiate(response));
TRACE("Negotiated authn=$0", AuthenticationTypeToString(negotiated_authn_));
}
// Step 3: if both ends support TLS, do a TLS handshake.
// TODO(KUDU-1921): allow the client to require TLS.
if (encryption_ != RpcEncryption::DISABLED &&
ContainsKey(server_features_, TLS)) {
RETURN_NOT_OK(tls_context_->InitiateHandshake(&tls_handshake_));
if (negotiated_authn_ == AuthenticationType::SASL) {
// When using SASL authentication, verifying the server's certificate is
// not necessary. This allows the client to still use TLS encryption for
// connections to servers which only have a self-signed certificate.
tls_handshake_.set_verification_mode(security::TlsVerificationMode::VERIFY_NONE);
}
// To initiate the TLS handshake, we pretend as if the server sent us an
// empty TLS_HANDSHAKE token.
NegotiatePB initial;
initial.set_step(NegotiatePB::TLS_HANDSHAKE);
initial.set_tls_handshake("");
Status s = HandleTlsHandshake(initial);
while (s.IsIncomplete()) {
NegotiatePB response;
RETURN_NOT_OK(RecvNegotiatePB(&response, &recv_buf, rpc_error));
s = HandleTlsHandshake(response);
}
RETURN_NOT_OK(s);
tls_negotiated_ = true;
}
// Step 4: Authentication
switch (negotiated_authn_) {
case AuthenticationType::SASL:
RETURN_NOT_OK(AuthenticateBySasl(&recv_buf, rpc_error));
break;
case AuthenticationType::TOKEN:
RETURN_NOT_OK(AuthenticateByToken(&recv_buf, rpc_error));
break;
case AuthenticationType::CERTIFICATE:
// The TLS handshake has already authenticated the server.
break;
case AuthenticationType::INVALID: LOG(FATAL) << "unreachable";
}
// Step 5: Send connection context.
RETURN_NOT_OK(SendConnectionContext());
TRACE("Negotiation successful");
return Status::OK();
}
Status ClientNegotiation::SendNegotiatePB(const NegotiatePB& msg) {
RequestHeader header;
header.set_call_id(kNegotiateCallId);
DCHECK(socket_);
DCHECK(msg.IsInitialized()) << "message must be initialized";
DCHECK(msg.has_step()) << "message must have a step";
TRACE("Sending $0 NegotiatePB request", NegotiatePB::NegotiateStep_Name(msg.step()));
return SendFramedMessageBlocking(socket_.get(), header, msg, deadline_);
}
Status ClientNegotiation::RecvNegotiatePB(NegotiatePB* msg,
faststring* buffer,
unique_ptr<ErrorStatusPB>* rpc_error) {
ResponseHeader header;
Slice param_buf;
RETURN_NOT_OK(ReceiveFramedMessageBlocking(
socket_.get(), buffer, &header, &param_buf, deadline_));
RETURN_NOT_OK(helper_.CheckNegotiateCallId(header.call_id()));
if (header.is_error()) {
return ParseError(param_buf, rpc_error);
}
RETURN_NOT_OK(helper_.ParseNegotiatePB(param_buf, msg));
TRACE("Received $0 NegotiatePB response",
NegotiatePB::NegotiateStep_Name(msg->step()));
return Status::OK();
}
Status ClientNegotiation::ParseError(const Slice& err_data,
unique_ptr<ErrorStatusPB>* rpc_error) {
unique_ptr<ErrorStatusPB> error(new ErrorStatusPB);
if (!error->ParseFromArray(err_data.data(), err_data.size())) {
return Status::IOError("invalid error response, missing fields",
error->InitializationErrorString());
}
Status s = StatusFromRpcError(*error);
TRACE("Received error response from server: $0", s.ToString());
if (rpc_error) {
rpc_error->swap(error);
}
return s;
}
Status ClientNegotiation::SendConnectionHeader() {
const uint8_t buflen = kMagicNumberLength + kHeaderFlagsLength;
uint8_t buf[buflen];
serialization::SerializeConnHeader(buf);
size_t nsent;
return socket_->BlockingWrite(buf, buflen, &nsent, deadline_);
}
Status ClientNegotiation::InitSaslClient() {
// TODO(KUDU-1922): consider setting SASL_SUCCESS_DATA
unsigned flags = 0;
const auto desc = Substitute("creating new SASL $0 client", sasl_proto_name_);
sasl_conn_t* sasl_conn = nullptr;
RETURN_NOT_OK_PREPEND(WrapSaslCall(
nullptr /* no conn */,
[&]() {
return sasl_client_new(
sasl_proto_name_.c_str(), // Registered name of the service using SASL. Required.
helper_.server_fqdn(), // The fully qualified domain name of the remote server.
nullptr, // Local and remote IP address strings. (we don't use
nullptr, // any mechanisms which require this info.)
&callbacks_[0], // Connection-specific callbacks.
flags,
&sasl_conn);
},
desc.c_str()), desc + " failed");
sasl_conn_.reset(sasl_conn);
return Status::OK();
}
Status ClientNegotiation::SendNegotiate() {
NegotiatePB msg;
msg.set_step(NegotiatePB::NEGOTIATE);
// Advertise our supported features.
client_features_ = kSupportedClientRpcFeatureFlags;
if (encryption_ != RpcEncryption::DISABLED) {
client_features_.insert(TLS);
// If the remote peer is local, then we allow using TLS for authentication
// without encryption or integrity.
if (socket_->IsLoopbackConnection() && !encrypt_loopback_) {
client_features_.insert(TLS_AUTHENTICATION_ONLY);
}
}
for (RpcFeatureFlag feature : client_features_) {
msg.add_supported_features(feature);
}
if (!helper_.EnabledMechs().empty()) {
msg.add_authn_types()->mutable_sasl();
}
if (tls_context_->has_signed_cert() && !tls_context_->is_external_cert()) {
// We only provide authenticated TLS if the certificates are generated
// by the internal CA.
msg.add_authn_types()->mutable_certificate();
}
if (authn_token_ && tls_context_->has_trusted_cert()) {
// TODO(KUDU-1924): check that the authn token is not expired. Can this be done
// reliably on clients?
msg.add_authn_types()->mutable_token();
}
if (PREDICT_FALSE(msg.authn_types().empty())) {
return Status::NotAuthorized("client is not configured with an authentication type");
}
return SendNegotiatePB(msg);
}
Status ClientNegotiation::HandleNegotiate(const NegotiatePB& response) {
if (PREDICT_FALSE(response.step() != NegotiatePB::NEGOTIATE)) {
return Status::NotAuthorized("expected NEGOTIATE step",
NegotiatePB::NegotiateStep_Name(response.step()));
}
TRACE("Received NEGOTIATE response from server");
// Fill in the set of features supported by the server.
for (int flag : response.supported_features()) {
// We only add the features that our local build knows about.
RpcFeatureFlag feature_flag = RpcFeatureFlag_IsValid(flag) ?
static_cast<RpcFeatureFlag>(flag) : UNKNOWN;
if (feature_flag != UNKNOWN) {
server_features_.insert(feature_flag);
}
}
if (encryption_ == RpcEncryption::REQUIRED &&
!ContainsKey(server_features_, RpcFeatureFlag::TLS)) {
return Status::NotAuthorized("server does not support required TLS encryption");
}
// Get the authentication type which the server would like to use.
DCHECK_LE(response.authn_types().size(), 1);
if (response.authn_types().empty()) {
// If the server doesn't send back an authentication type, default to SASL
// in order to maintain backwards compatibility.
negotiated_authn_ = AuthenticationType::SASL;
} else {
const auto& authn_type = response.authn_types(0);
switch (authn_type.type_case()) {
case AuthenticationTypePB::kSasl:
negotiated_authn_ = AuthenticationType::SASL;
break;
case AuthenticationTypePB::kToken:
// TODO(todd): we should also be checking tls_context_->has_trusted_cert()
// here to match the original logic we used to advertise TOKEN support,
// or perhaps just check explicitly whether we advertised TOKEN.
if (!authn_token_) {
return Status::RuntimeError(
"server chose token authentication, but client has no token");
}
negotiated_authn_ = AuthenticationType::TOKEN;
return Status::OK();
case AuthenticationTypePB::kCertificate:
if (!tls_context_->has_signed_cert()) {
return Status::RuntimeError(
"server chose certificate authentication, but client has no certificate");
}
negotiated_authn_ = AuthenticationType::CERTIFICATE;
return Status::OK();
case AuthenticationTypePB::TYPE_NOT_SET:
return Status::RuntimeError("server chose an unknown authentication type");
}
}
DCHECK_EQ(negotiated_authn_, AuthenticationType::SASL);
// Build a map of the SASL mechanisms offered by the server.
set<SaslMechanism::Type> client_mechs(helper_.EnabledMechs());
set<SaslMechanism::Type> server_mechs;
for (const NegotiatePB::SaslMechanism& sasl_mech : response.sasl_mechanisms()) {
auto mech = SaslMechanism::value_of(sasl_mech.mechanism());
if (mech == SaslMechanism::INVALID) {
continue;
}
server_mechs.insert(mech);
}
// Determine which SASL mechanism to use for authenticating the connection.
// We pick the most preferred mechanism which is supported by both parties.
// The preference list in order of most to least preferred:
// * GSSAPI
// * PLAIN
//
// TODO(KUDU-1921): allow the client to require authentication.
if (ContainsKey(client_mechs, SaslMechanism::GSSAPI) &&
ContainsKey(server_mechs, SaslMechanism::GSSAPI)) {
// Check that the client has local Kerberos credentials, and if not fall
// back to an alternate mechanism.
Status s = CheckGSSAPI();
if (s.ok()) {
negotiated_mech_ = SaslMechanism::GSSAPI;
return Status::OK();
}
TRACE("Kerberos authentication credentials are not available: $0", s.ToString());
client_mechs.erase(SaslMechanism::GSSAPI);
}
if (ContainsKey(client_mechs, SaslMechanism::PLAIN) &&
ContainsKey(server_mechs, SaslMechanism::PLAIN)) {
negotiated_mech_ = SaslMechanism::PLAIN;
return Status::OK();
}
// There are no mechanisms in common.
if (ContainsKey(server_mechs, SaslMechanism::GSSAPI) &&
!ContainsKey(client_mechs, SaslMechanism::GSSAPI)) {
return Status::NotAuthorized("server requires authentication, "
"but client does not have Kerberos credentials available");
}
if (!ContainsKey(server_mechs, SaslMechanism::GSSAPI) &&
ContainsKey(client_mechs, SaslMechanism::GSSAPI)) {
return Status::NotAuthorized("client requires authentication, "
"but server does not have Kerberos enabled");
}
string msg = Substitute("client/server supported SASL mechanism mismatch; "
"client mechanisms: [$0], server mechanisms: [$1]",
JoinMapped(client_mechs, SaslMechanism::name_of, ", "),
JoinMapped(server_mechs, SaslMechanism::name_of, ", "));
// For now, there should never be a SASL mechanism mismatch that isn't due
// to one of the sides requiring Kerberos and the other not having it, so
// lets sanity check that.
DCHECK(STLSetIntersection(client_mechs, server_mechs).empty()) << msg;
return Status::NotAuthorized(msg);
}
Status ClientNegotiation::SendTlsHandshake(string tls_token) {
TRACE("Sending TLS_HANDSHAKE message to server");
NegotiatePB msg;
msg.set_step(NegotiatePB::TLS_HANDSHAKE);
msg.mutable_tls_handshake()->swap(tls_token);
return SendNegotiatePB(msg);
}
Status ClientNegotiation::HandleTlsHandshake(const NegotiatePB& response) {
if (PREDICT_FALSE(response.step() != NegotiatePB::TLS_HANDSHAKE)) {
return Status::NotAuthorized("expected TLS_HANDSHAKE step",
NegotiatePB::NegotiateStep_Name(response.step()));
}
if (!response.tls_handshake().empty()) {
TRACE("Received TLS_HANDSHAKE response from server");
}
if (PREDICT_FALSE(!response.has_tls_handshake())) {
return Status::NotAuthorized("No TLS handshake token in TLS_HANDSHAKE response from server");
}
string token;
Status s = tls_handshake_.Continue(response.tls_handshake(), &token);
if (tls_handshake_.NeedsExtraStep(s, token)) {
RETURN_NOT_OK(SendTlsHandshake(std::move(token)));
}
// Check that the handshake step didn't produce an error. Will also propagate
// an Incomplete status.
RETURN_NOT_OK(s);
// TLS handshake is finished.
if (ContainsKey(server_features_, TLS_AUTHENTICATION_ONLY) &&
ContainsKey(client_features_, TLS_AUTHENTICATION_ONLY)) {
TRACE("Negotiated auth-only $0 with cipher $1",
tls_handshake_.GetProtocol(), tls_handshake_.GetCipherDescription());
return tls_handshake_.FinishNoWrap(*socket_);
}
TRACE("Negotiated $0 with cipher $1",
tls_handshake_.GetProtocol(), tls_handshake_.GetCipherDescription());
return tls_handshake_.Finish(&socket_);
}
Status ClientNegotiation::AuthenticateBySasl(faststring* recv_buf,
unique_ptr<ErrorStatusPB>* rpc_error) {
RETURN_NOT_OK(InitSaslClient());
Status s = SendSaslInitiate();
// HandleSasl[Initiate, Challenge] return incomplete if an additional
// challenge step is required, or OK if a SASL_SUCCESS message is expected.
while (s.IsIncomplete()) {
NegotiatePB challenge;
RETURN_NOT_OK(RecvNegotiatePB(&challenge, recv_buf, rpc_error));
s = HandleSaslChallenge(challenge);
}
// Propagate failure from SendSaslInitiate or HandleSaslChallenge.
RETURN_NOT_OK(s);
// Server challenges are over; we now expect the success message.
NegotiatePB success;
RETURN_NOT_OK(RecvNegotiatePB(&success, recv_buf, rpc_error));
return HandleSaslSuccess(success);
}
Status ClientNegotiation::AuthenticateByToken(faststring* recv_buf,
unique_ptr<ErrorStatusPB>* rpc_error) {
// Sanity check that TLS has been negotiated. Sending the token on an
// unencrypted channel is a big no-no.
CHECK(tls_negotiated_);
// Send the token to the server.
NegotiatePB pb;
pb.set_step(NegotiatePB::TOKEN_EXCHANGE);
*pb.mutable_authn_token() = std::move(*authn_token_);
RETURN_NOT_OK(SendNegotiatePB(pb));
pb.Clear();
// Check that the server responds with a non-error TOKEN_EXCHANGE message.
RETURN_NOT_OK(RecvNegotiatePB(&pb, recv_buf, rpc_error));
if (pb.step() != NegotiatePB::TOKEN_EXCHANGE) {
return Status::NotAuthorized("expected TOKEN_EXCHANGE step",
NegotiatePB::NegotiateStep_Name(pb.step()));
}
return Status::OK();
}
Status ClientNegotiation::SendSaslInitiate() {
TRACE("Initiating SASL $0 handshake", SaslMechanism::name_of(negotiated_mech_));
// At this point we've already chosen the SASL mechanism to use
// (negotiated_mech_), but we need to let the SASL library know. SASL likes to
// choose the mechanism from among a list of possible options, so we simply
// provide it one option, and then check that it picks that option.
const char* init_msg = nullptr;
unsigned init_msg_len = 0;
const char* negotiated_mech = nullptr;
// If the negotiated mechanism is GSSAPI (Kerberos), configure SASL to use
// integrity protection so that the channel bindings and nonce can be
// verified.
if (negotiated_mech_ == SaslMechanism::GSSAPI) {
RETURN_NOT_OK(EnableProtection(sasl_conn_.get(), SaslProtection::kIntegrity));
}
/* select a mechanism for a connection
* mechlist -- mechanisms server has available (punctuation ignored)
* output:
* prompt_need -- on SASL_INTERACT, list of prompts needed to continue
* clientout -- the initial client response to send to the server
* mech -- set to mechanism name
*
* Returns:
* SASL_OK -- success
* SASL_CONTINUE -- negotiation required
* SASL_NOMEM -- not enough memory
* SASL_NOMECH -- no mechanism meets requested properties
* SASL_INTERACT -- user interaction needed to fill in prompt_need list
*/
static constexpr const char* const kDesc = "calling sasl_client_start()";
TRACE(kDesc);
const Status s = WrapSaslCall(sasl_conn_.get(), [&]() {
return sasl_client_start(
sasl_conn_.get(), // The SASL connection context created by init()
SaslMechanism::name_of(negotiated_mech_), // The list of mechanisms to negotiate.
nullptr, // Disables INTERACT return if NULL.
&init_msg, // Filled in on success.
&init_msg_len, // Filled in on success.
&negotiated_mech); // Filled in on success.
}, kDesc);
if (PREDICT_FALSE(!s.ok() && !s.IsIncomplete())) {
return s;
}
// Check that the SASL library is using the mechanism that we picked.
DCHECK_EQ(SaslMechanism::value_of(negotiated_mech), negotiated_mech_);
NegotiatePB msg;
msg.set_step(NegotiatePB::SASL_INITIATE);
msg.mutable_token()->assign(init_msg, init_msg_len);
msg.add_sasl_mechanisms()->set_mechanism(negotiated_mech);
RETURN_NOT_OK(SendNegotiatePB(msg));
return s;
}
Status ClientNegotiation::SendSaslResponse(const char* resp_msg, unsigned resp_msg_len) {
NegotiatePB reply;
reply.set_step(NegotiatePB::SASL_RESPONSE);
reply.mutable_token()->assign(resp_msg, resp_msg_len);
return SendNegotiatePB(reply);
}
Status ClientNegotiation::HandleSaslChallenge(const NegotiatePB& response) {
if (PREDICT_FALSE(response.step() != NegotiatePB::SASL_CHALLENGE)) {
return Status::NotAuthorized("expected SASL_CHALLENGE step",
NegotiatePB::NegotiateStep_Name(response.step()));
}
TRACE("Received SASL_CHALLENGE response from server");
if (PREDICT_FALSE(!response.has_token())) {
return Status::NotAuthorized("no token in SASL_CHALLENGE response from server");
}
const char* out = nullptr;
unsigned out_len = 0;
const Status s = DoSaslStep(response.token(), &out, &out_len);
if (PREDICT_FALSE(!s.IsIncomplete() && !s.ok())) {
return s;
}
RETURN_NOT_OK(SendSaslResponse(out, out_len));
return s;
}
Status ClientNegotiation::HandleSaslSuccess(const NegotiatePB& response) {
if (PREDICT_FALSE(response.step() != NegotiatePB::SASL_SUCCESS)) {
return Status::NotAuthorized("expected SASL_SUCCESS step",
NegotiatePB::NegotiateStep_Name(response.step()));
}
TRACE("Received SASL_SUCCESS response from server");
if (negotiated_mech_ == SaslMechanism::GSSAPI) {
if (response.has_nonce()) {
// Grab the nonce from the server, if it has sent one. We'll send it back
// later with SASL integrity protection as part of the connection context.
nonce_ = response.nonce();
}
if (tls_negotiated_) {
// Check the channel bindings provided by the server against the expected channel bindings.
if (!response.has_channel_bindings()) {
return Status::NotAuthorized("no channel bindings provided by server");
}
security::Cert cert;
RETURN_NOT_OK(tls_handshake_.GetRemoteCert(&cert));
string expected_channel_bindings;
RETURN_NOT_OK_PREPEND(cert.GetServerEndPointChannelBindings(&expected_channel_bindings),
"failed to generate channel bindings");
Slice received_channel_bindings;
RETURN_NOT_OK_PREPEND(SaslDecode(sasl_conn_.get(),
response.channel_bindings(),
&received_channel_bindings),
"failed to decode channel bindings");
if (expected_channel_bindings != received_channel_bindings) {
Sockaddr addr;
ignore_result(socket_->GetPeerAddress(&addr));
LOG(WARNING) << "Received invalid channel bindings from server "
<< addr.ToString()
<< ", this could indicate an active network man-in-the-middle";
return Status::NotAuthorized("channel bindings do not match");
}
}
}
return Status::OK();
}
Status ClientNegotiation::DoSaslStep(const string& in, const char** out, unsigned* out_len) {
static constexpr const char* const kDesc = "calling sasl_client_step()";
TRACE(kDesc);
return WrapSaslCall(sasl_conn_.get(), [&]() {
return sasl_client_step(
sasl_conn_.get(), in.c_str(), in.length(), nullptr, out, out_len);
}, kDesc);
}
Status ClientNegotiation::SendConnectionContext() {
TRACE("Sending connection context");
RequestHeader header;
header.set_call_id(kConnectionContextCallId);
ConnectionContextPB conn_context;
// This field is deprecated but used by servers <Kudu 1.1. Newer server versions ignore
// this and use the SASL-provided username instead.
conn_context.mutable_deprecated_user_info()->set_real_user(
plain_auth_user_.empty() ? "cpp-client" : plain_auth_user_);
if (nonce_) {
// Reply with the SASL-protected nonce. We only set the nonce when using SASL GSSAPI.
Slice ciphertext;
RETURN_NOT_OK(SaslEncode(sasl_conn_.get(), *nonce_, &ciphertext));
*conn_context.mutable_encoded_nonce() = ciphertext.ToString();
}
return SendFramedMessageBlocking(socket_.get(), header, conn_context, deadline_);
}
int ClientNegotiation::GetOptionCb(const char* plugin_name, const char* option,
const char** result, unsigned* len) {
return helper_.GetOptionCb(plugin_name, option, result, len);
}
// Used for PLAIN.
// SASL callback for SASL_CB_USER, SASL_CB_AUTHNAME, SASL_CB_LANGUAGE
int ClientNegotiation::SimpleCb(int id, const char** result, unsigned* len) {
if (PREDICT_FALSE(!helper_.IsPlainEnabled())) {
LOG(DFATAL) << "Simple callback called, but PLAIN auth is not enabled";
return SASL_FAIL;
}
if (PREDICT_FALSE(result == nullptr)) {
LOG(DFATAL) << "result outparam is NULL";
return SASL_BADPARAM;
}
switch (id) {
// TODO(unknown): Support impersonation?
// For impersonation, USER is the impersonated user, AUTHNAME is the "sudoer".
case SASL_CB_USER:
TRACE("callback for SASL_CB_USER");
*result = plain_auth_user_.c_str();
if (len != nullptr) *len = plain_auth_user_.length();
break;
case SASL_CB_AUTHNAME:
TRACE("callback for SASL_CB_AUTHNAME");
*result = plain_auth_user_.c_str();
if (len != nullptr) *len = plain_auth_user_.length();
break;
case SASL_CB_LANGUAGE:
LOG(DFATAL) << "Unable to handle SASL callback type SASL_CB_LANGUAGE"
<< "(" << id << ")";
return SASL_BADPARAM;
default:
LOG(DFATAL) << "Unexpected SASL callback type: " << id;
return SASL_BADPARAM;
}
return SASL_OK;
}
// Used for PLAIN.
// SASL callback for SASL_CB_PASS: User password.
int ClientNegotiation::SecretCb(sasl_conn_t* conn, int id, sasl_secret_t** psecret) {
if (PREDICT_FALSE(!helper_.IsPlainEnabled())) {
LOG(DFATAL) << "Plain secret callback called, but PLAIN auth is not enabled";
return SASL_FAIL;
}
switch (id) {
case SASL_CB_PASS: {
if (!conn || !psecret) return SASL_BADPARAM;
size_t len = plain_pass_.length();
*psecret = reinterpret_cast<sasl_secret_t*>(malloc(sizeof(sasl_secret_t) + len));
if (!*psecret) {
return SASL_NOMEM;
}
psecret_.reset(*psecret); // Ensure that we free() this structure later.
(*psecret)->len = len;
memcpy((*psecret)->data, plain_pass_.c_str(), len + 1);
break;
}
default:
LOG(DFATAL) << "Unexpected SASL callback type: " << id;
return SASL_BADPARAM;
}
return SASL_OK;
}
Status ClientNegotiation::CheckGSSAPI() {
// Disable leak checking in this function to work around memory leak in libgssapi_krb5
// when opening a corrupt credential cache fails:
// https://krbdev.mit.edu/rt/Ticket/Display.html?id=8437.
// Fixed in MIT Kerberos 1.13.7, 1.14.4 and 1.15.
debug::ScopedLeakCheckDisabler disable_leak_checks;
OM_uint32 major, minor;
gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
// Acquire the Kerberos credential. This will fail if the client does not have
// a Kerberos tgt ticket. In theory it should be sufficient to call
// gss_inquire_cred_by_mech, but that causes a memory leak on RHEL 7.
major = gss_acquire_cred(&minor,
GSS_C_NO_NAME,
GSS_C_INDEFINITE,
const_cast<gss_OID_set>(gss_mech_set_krb5),
GSS_C_INITIATE,
&cred,
nullptr,
nullptr);
Status s = gssapi::MajorMinorToStatus(major, minor);
// Inspect the Kerberos credential to determine if it is expired. The lifetime
// returned from gss_acquire_cred in the RHEL 6 version of krb5 is always 0,
// so it has to be done with a separate call to gss_inquire_cred. The lifetime
// holds the remaining validity of the tgt in seconds.
OM_uint32 lifetime;
if (s.ok()) {
major = gss_inquire_cred(&minor, cred, nullptr, &lifetime, nullptr, nullptr);
s = gssapi::MajorMinorToStatus(major, minor);
}
// Release the credential even if gss_inquire_cred fails.
gss_release_cred(&minor, &cred);
RETURN_NOT_OK(s);
if (lifetime == 0) {
return Status::NotAuthorized("Kerberos ticket expired");
}
return Status::OK();
}
} // namespace rpc
} // namespace kudu
#if defined(__APPLE__)
#pragma GCC diagnostic pop
#endif // #if defined(__APPLE__)