common/protobuf/kudu/rpc/client_negotiation.cc (616 lines of code) (raw):

// 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, boost::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__)