| // 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, |
| std::optional<security::JwtRawPB> jwt, |
| 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)), |
| jwt_(std::move(jwt)), |
| 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 || |
| negotiated_authn_ == AuthenticationType::JWT) { |
| // When using SASL or JWT 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::JWT: |
| RETURN_NOT_OK(AuthenticateByJwt(&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, ¶m_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 (jwt_) { |
| // TODO(zchovan): make sure that we are using a trusted certificate |
| msg.add_authn_types()->mutable_jwt(); |
| } |
| |
| 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::kJwt: |
| if (!jwt_) { |
| return Status::RuntimeError( |
| "server chose JWT authentication, but client has no JWT"); |
| } |
| negotiated_authn_ = AuthenticationType::JWT; |
| 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::AuthenticateByJwt(faststring* recv_buf, |
| unique_ptr<ErrorStatusPB>* rpc_error) { |
| NegotiatePB pb; |
| pb.set_step(NegotiatePB::JWT_EXCHANGE); |
| *pb.mutable_jwt_raw() = std::move(*jwt_); |
| RETURN_NOT_OK(SendNegotiatePB(pb)); |
| pb.Clear(); |
| RETURN_NOT_OK(RecvNegotiatePB(&pb, recv_buf, rpc_error)); |
| if (pb.step() != NegotiatePB::JWT_EXCHANGE) { |
| return Status::NotAuthorized("expected JWT_EXCHANGE step", |
| NegotiatePB::NegotiateStep_Name(pb.step())); |
| } |
| return Status::OK(); |
| } |
| |
| 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. |
| if (PREDICT_FALSE(!tls_negotiated_)) { |
| constexpr const char* const kErrMsg = |
| "received authn token over an unencrypted channel"; |
| LOG(DFATAL) << kErrMsg; |
| return Status::IllegalState(kErrMsg); |
| } |
| |
| // 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__) |