diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2019-12-02 14:37:52 +0000 |
---|---|---|
committer | evergreen <evergreen@mongodb.com> | 2019-12-02 14:37:52 +0000 |
commit | 7861a2618676421aa4ad79f9a831c048b72b0043 (patch) | |
tree | 9512d78bb14c8e19c74e755a0b2e67de5f223315 /src/mongo/client | |
parent | 9f59f0c4ed671b62329837f31821476963022d39 (diff) | |
download | mongo-7861a2618676421aa4ad79f9a831c048b72b0043.tar.gz |
SERVER-44263 Implement common iam sasl protocol code for client and server with unit tests
Diffstat (limited to 'src/mongo/client')
-rw-r--r-- | src/mongo/client/SConscript | 35 | ||||
-rw-r--r-- | src/mongo/client/sasl_iam_client_protocol.cpp | 233 | ||||
-rw-r--r-- | src/mongo/client/sasl_iam_client_protocol.h | 118 | ||||
-rw-r--r-- | src/mongo/client/sasl_iam_client_protocol.idl | 60 | ||||
-rw-r--r-- | src/mongo/client/sasl_iam_protocol_common.h | 131 | ||||
-rw-r--r-- | src/mongo/client/sasl_iam_protocol_common.idl | 82 |
6 files changed, 659 insertions, 0 deletions
diff --git a/src/mongo/client/SConscript b/src/mongo/client/SConscript index a50eba71e70..cd456d7bba1 100644 --- a/src/mongo/client/SConscript +++ b/src/mongo/client/SConscript @@ -38,6 +38,41 @@ env.Library( ], ) +env.Library( + target=[ + 'sasl_iam_common', + ], + source=[ + env.Idlc('sasl_iam_protocol_common.idl')[0], + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/idl/idl_parser', + '$BUILD_DIR/mongo/db/server_options_core', # For object_check.h + ], +) + +kmsEnv = env.Clone() + +kmsEnv.InjectThirdParty(libraries=['kms-message']) + +kmsEnv.Library( + target=[ + 'sasl_iam_client', + ], + source=[ + 'sasl_iam_client_protocol.cpp', + env.Idlc('sasl_iam_client_protocol.idl')[0], + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/third_party/shim_kms_message', + '$BUILD_DIR/mongo/idl/idl_parser', + '$BUILD_DIR/mongo/db/server_options_core', # For object_check.h + 'sasl_iam_common', + ], +) + saslClientEnv = env.Clone() saslLibs = [] saslClientSource = [ diff --git a/src/mongo/client/sasl_iam_client_protocol.cpp b/src/mongo/client/sasl_iam_client_protocol.cpp new file mode 100644 index 00000000000..466acc4f795 --- /dev/null +++ b/src/mongo/client/sasl_iam_client_protocol.cpp @@ -0,0 +1,233 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/client/sasl_iam_client_protocol.h" + +#include <iostream> + +#include "mongo/base/data_range_cursor.h" +#include "mongo/base/data_type_validated.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/json.h" +#include "mongo/client/sasl_iam_client_protocol_gen.h" +#include "mongo/platform/mutex.h" +#include "mongo/platform/random.h" +#include "mongo/util/base64.h" +#include "mongo/util/kms_message_support.h" + +namespace mongo { +namespace iam { +namespace { +// Secure Random for SASL IAM Nonce generation +Mutex saslIAMClientMutex = MONGO_MAKE_LATCH("IAMClientMutex"); +SecureRandom saslIAMClientGen; + +std::vector<char> generateClientNonce() { + + std::vector<char> ret; + ret.resize(iam::kClientFirstNonceLength); + + { + stdx::lock_guard<Latch> lk(saslIAMClientMutex); + saslIAMClientGen.fill(ret.data(), ret.size()); + } + + return ret; +} + +/** + * Returns false if a dns name contains an empty part. + * Good: a.b or a.b.c or a + * Bad: a..b or a.b..c + */ +bool validateHostNameParts(StringData str) { + size_t pos = str.find('.'); + if (pos != std::string::npos) { + while (true) { + size_t last_pos = pos; + pos = str.find('.', last_pos + 1); + if (pos == std::string::npos) { + break; + } + if (last_pos + 1 == pos) { + return false; + } + } + } + + return true; +} + +void uassertKmsRequestInternal(kms_request_t* request, const char* file, int line, bool ok) { + if (!ok) { + const char* msg = kms_request_get_error(request); + uasserted(51299, + str::stream() << "Internal AWS IAM Error: " << msg << " at " << file << ":" + << line); + } +} + +template <typename T> +AWSCredentials parseCredentials(StringData data) { + BSONObj obj = fromjson(data.toString()); + + auto creds = T::parse(IDLParserErrorContext("security-credentials"), obj); + + return AWSCredentials(creds.getAccessKeyId().toString(), + creds.getSecretAccessKey().toString(), + creds.getToken().toString()); +} +} // namespace + + +std::string iam::generateClientFirst(std::vector<char>* clientNonce) { + *clientNonce = generateClientNonce(); + + IamClientFirst first; + first.setNonce(*clientNonce); + first.setGs2_cb_flag(static_cast<int>('n')); + + return iam::convertToByteString(first); +} + +#define uassertKmsRequest(X) uassertKmsRequestInternal(request.get(), __FILE__, __LINE__, (X)); + +std::string generateClientSecond(StringData serverFirstBase64, + const std::vector<char>& clientNonce, + const AWSCredentials& credentials) { + dassert(clientNonce.size() == kClientFirstNonceLength); + auto serverFirst = convertFromByteString<IamServerFirst>(serverFirstBase64); + + uassert(51298, + "Nonce must be 64 bytes", + serverFirst.getServerNonce().length() == iam::kServerFirstNonceLength); + + uassert(51297, + "First part of nonce must match client", + std::equal(serverFirst.getServerNonce().data(), + serverFirst.getServerNonce().data() + iam::kClientFirstNonceLength, + clientNonce.begin(), + clientNonce.end()) == true); + + uassert(51296, + "Host name length is incorrect", + !serverFirst.getStsHost().empty() && + serverFirst.getStsHost().size() < iam::kMaxStsHostNameLength); + + uassert(51295, + "Host name is not allowed to have a empty DNS name part.", + validateHostNameParts(serverFirst.getStsHost())); + + + auto request = UniqueKmsRequest(kms_caller_identity_request_new(NULL)); + + // Use current time + uassertKmsRequest(kms_request_set_date(request.get(), nullptr)); + + // Region is derived from host + uassertKmsRequest( + kms_request_set_region(request.get(), getRegionFromHost(serverFirst.getStsHost()).c_str())); + + // sts is always the name of the service + uassertKmsRequest(kms_request_set_service(request.get(), iam::kAwsServiceName.rawData())); + + uassertKmsRequest(kms_request_add_header_field( + request.get(), "Host", serverFirst.getStsHost().toString().c_str())); + + auto serverNonce = serverFirst.getServerNonce(); + uassertKmsRequest(kms_request_add_header_field( + request.get(), + iam::kMongoServerNonceHeader.rawData(), + base64::encode(serverNonce.data(), serverNonce.length()).c_str())); + + uassertKmsRequest(kms_request_add_header_field( + request.get(), iam::kMongoGS2CBHeader.rawData(), iam::kMongoDefaultGS2CBFlag.rawData())); + + uassertKmsRequest( + kms_request_set_access_key_id(request.get(), credentials.accessKeyId.c_str())); + uassertKmsRequest( + kms_request_set_secret_key(request.get(), credentials.secretAccessKey.c_str())); + + + IamClientSecond second; + + if (credentials.sessionToken) { + // TODO: move this into kms-message + uassertKmsRequest(kms_request_add_header_field( + request.get(), "X-Amz-Security-Token", credentials.sessionToken.get().c_str())); + + second.setXAmzSecurityToken(boost::optional<StringData>(credentials.sessionToken.get())); + } + + UniqueKmsCharBuffer kmsSignature(kms_request_get_signature(request.get())); + second.setAuthHeader(kmsSignature.get()); + + second.setXAmzDate(kms_request_get_canonical_header(request.get(), kXAmzDateHeader.rawData())); + + return iam::convertToByteString(second); +} + +std::string iam::getRegionFromHost(StringData host) { + if (host == iam::kAwsDefaultStsHost) { + return iam::kAwsDefaultRegion.toString(); + } + + size_t firstPeriod = host.find('.'); + if (firstPeriod == std::string::npos) { + return iam::kAwsDefaultRegion.toString(); + } + + size_t secondPeriod = host.find('.', firstPeriod + 1); + if (secondPeriod == std::string::npos) { + return host.substr(firstPeriod + 1).toString(); + } + + return host.substr(firstPeriod + 1, secondPeriod - firstPeriod - 1).toString(); +} + +std::string iam::parseRoleFromEC2IamSecurityCredentials(StringData data) { + size_t pos = data.find('\n'); + + uassert( + 51294, "Failed to parse role name from EC2 instance metadata", pos != std::string::npos); + return data.substr(0, pos).toString(); +} + +AWSCredentials iam::parseCredentialsFromEC2IamSecurityCredentials(StringData data) { + return parseCredentials<Ec2SecurityCredentials>(data); +} + +AWSCredentials iam::parseCredentialsFromECSTaskIamCredentials(StringData data) { + return parseCredentials<EcsTaskSecurityCredentials>(data); +} + +} // namespace iam +} // namespace mongo diff --git a/src/mongo/client/sasl_iam_client_protocol.h b/src/mongo/client/sasl_iam_client_protocol.h new file mode 100644 index 00000000000..cf5d4d79bec --- /dev/null +++ b/src/mongo/client/sasl_iam_client_protocol.h @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <string> +#include <vector> + +#include "mongo/base/string_data.h" +#include "mongo/client/sasl_iam_protocol_common.h" + +namespace mongo { +namespace iam { + +/** + * Generate client first message for IAM Auth. + * + * Returns nonce as out parameter for client to store + */ +std::string generateClientFirst(std::vector<char>* clientNonce); + +/** + * Parse IAM Auth server first message and generate client second. + */ +std::string generateClientSecond(StringData serverFirst, + const std::vector<char>& clientNonce, + const AWSCredentials& credentials); + +/** + * Get the AWS Role Name from a request to + * http://169.254.169.254/latest/meta-data/iam/security-credentials/ + * + * The input is expected to be a simple line that ends in a newline (\n). + */ +std::string parseRoleFromEC2IamSecurityCredentials(StringData data); + +/** + * Get the AWS region from a DNS Name + * + * Region by default is "us-east-1" since this is the implicit region for "sts.amazonaws.com". + * + * Host Region + * sts.amazonaws.com us-east-1 + * first.second.third second + * first.second second + * first us-east-1 + */ +std::string getRegionFromHost(StringData host); + +/** + * Get a set of AWS Credentials from a request to + * http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME> + * + * where ROLE_NAME comes from parseRoleFromEC2IamSecurityCredentials. + * + * Per + * https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials, + * we expect the following JSON: + * + * { + * "Code" : "Success", + * "LastUpdated" : "DATE", + * "Type" : "AWS-HMAC", + * "AccessKeyId" : "ACCESS_KEY_ID", + * "SecretAccessKey" : "SECRET_ACCESS_KEY", + * "Token" : "SECURITY_TOKEN_STRING", + * "Expiration" : "EXPIRATION_DATE" + * } + */ +AWSCredentials parseCredentialsFromEC2IamSecurityCredentials(StringData data); + +/** + * Get a set of AWS Credentials from a request to + * http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI + * + * where AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is an environment variable. + * + * Per https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html, + * we expect the following JSON: + * + * { + * "AccessKeyId": "ACCESS_KEY_ID", + * "Expiration": "EXPIRATION_DATE", + * "RoleArn": "TASK_ROLE_ARN", + * "SecretAccessKey": "SECRET_ACCESS_KEY", + * "Token": "SECURITY_TOKEN_STRING" + * } + */ +AWSCredentials parseCredentialsFromECSTaskIamCredentials(StringData data); + +} // namespace iam +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/client/sasl_iam_client_protocol.idl b/src/mongo/client/sasl_iam_client_protocol.idl new file mode 100644 index 00000000000..32e0e6ec4c9 --- /dev/null +++ b/src/mongo/client/sasl_iam_client_protocol.idl @@ -0,0 +1,60 @@ +# Copyright (C) 2019-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo" + +imports: + - "mongo/idl/basic_types.idl" + +structs: + Ec2SecurityCredentials: + description : "Security Credentials from EC2 Instance Metadata" + # See - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials + # Don't fail if AWS expands this to include more fields in the future + strict: false + fields: + Code: string + LastUpdated: string + Type: string + AccessKeyId: string + SecretAccessKey: string + Token: string + Expiration: string + + EcsTaskSecurityCredentials: + description : "Security Credentials from ECS Task" + # See -https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html + # Don't fail if AWS expands this to include more fields in the future + strict: false + fields: + AccessKeyId: string + Expiration: string + RoleArn: string + SecretAccessKey: string + Token: string diff --git a/src/mongo/client/sasl_iam_protocol_common.h b/src/mongo/client/sasl_iam_protocol_common.h new file mode 100644 index 00000000000..673db5b0720 --- /dev/null +++ b/src/mongo/client/sasl_iam_protocol_common.h @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <boost/optional.hpp> +#include <string> +#include <vector> + +#include "mongo/base/data_range_cursor.h" +#include "mongo/base/data_type_validated.h" +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/client/sasl_iam_protocol_common_gen.h" +#include "mongo/rpc/object_check.h" + +namespace mongo { + +namespace iam { +/** + * Common constants shared by IAM client and server code. + */ +/** + * Client and Server Nonce lengths + */ +static constexpr size_t kClientFirstNonceLength = 32; +static constexpr size_t kServerFirstNoncePieceLength = 32; +static constexpr size_t kServerFirstNonceLength = + kClientFirstNonceLength + kServerFirstNoncePieceLength; + +static constexpr size_t kMaxStsHostNameLength = 255; + +static constexpr auto kMongoServerNonceHeader = "x-mongodb-server-nonce"_sd; +static constexpr auto kMongoGS2CBHeader = "x-mongodb-gs2-cb-flag"_sd; +static constexpr auto kChannelBindingDataHeader = "x-mongodb-channel-binding-data"_sd; +static constexpr auto kChannelBindingTypePrefixHeader = "x-mongodb-channel-type-prefix"_sd; + +static constexpr auto kMongoDefaultGS2CBFlag = "n"_sd; + +static constexpr auto kAwsServiceName = "sts"_sd; + +static constexpr auto kAwsDefaultRegion = "us-east-1"_sd; +static constexpr auto kAwsDefaultStsHost = "sts.amazonaws.com"_sd; + +static constexpr auto kXAmzDateHeader = "X-Amz-Date"_sd; + +/** + * Deserialize a std::string to an IDL object. + */ +template <typename T> +T convertFromByteString(StringData rawString) { + ConstDataRange cdr(rawString.rawData(), rawString.size()); + + auto clientFirstBson = cdr.read<Validated<BSONObj>>(); + + return T::parse(IDLParserErrorContext("sasl"), clientFirstBson); +} + +/** + * Convert an IDL object to a std::string + */ +template <typename T> +std::string convertToByteString(T object) { + BSONObj obj = object.toBSON(); + + return std::string((obj.objdata()), obj.objsize()); +} + +/** + * AWS Credentials holder + */ +struct AWSCredentials { + AWSCredentials() = default; + + /** + * Construct for regular AWS credentials + */ + explicit AWSCredentials(std::string accessKeyIdParam, std::string secretAccessKeyParam) + : accessKeyId(std::move(accessKeyIdParam)), + secretAccessKey(std::move(secretAccessKeyParam)) {} + + /** + * Construct for temporary AWS credentials + */ + explicit AWSCredentials(std::string accessKeyIdParam, + std::string secretAccessKeyParam, + std::string sessionTokenParam) + : accessKeyId(std::move(accessKeyIdParam)), + secretAccessKey(std::move(secretAccessKeyParam)), + sessionToken(std::move(sessionTokenParam)) {} + + // AWS ACCESS_KEY_ID + std::string accessKeyId; + + // AWS SECRET_ACCESS_KEY + std::string secretAccessKey; + + // AWS SESSION TOKEN + // Generated for temporary credentials + boost::optional<std::string> sessionToken; +}; + +} // namespace iam + +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/client/sasl_iam_protocol_common.idl b/src/mongo/client/sasl_iam_protocol_common.idl new file mode 100644 index 00000000000..43337251ef8 --- /dev/null +++ b/src/mongo/client/sasl_iam_protocol_common.idl @@ -0,0 +1,82 @@ +# Copyright (C) 2019-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo::iam" + +imports: + - "mongo/idl/basic_types.idl" + +structs: + iamClientFirst: + description: "SASL IAM Client First message" + fields: + # Must be 32 bytes + r: + type: bindata_generic + cpp_name: nonce + p: + type: int + cpp_name: gs2_cb_flag + + iamServerFirst: + description: "SASL IAM Server First message" + fields: + # Must be 64 bytes + s: + type: bindata_generic + cpp_name: ServerNonce + h: + type: string + cpp_name: StsHost + + iamClientSecond: + description: "SASL IAM Client Second message" + fields: + a: + type: string + cpp_name: AuthHeader + d: + type: string + cpp_name: XAmzDate + t: + type: string + optional: true + cpp_name: XAmzSecurityToken + p: + type: string + optional: true + cpp_name: ChannelBindingPrefix + c: + type: string + optional: true + cpp_name: ChannelBindingData + o: + type: object + optional: true + cpp_name: Object |