1 //
2 // Copyright 2020 gRPC authors.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 #include <grpc/support/port_platform.h>
17
18 #include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
19
20 #include "absl/strings/str_format.h"
21 #include "absl/strings/str_join.h"
22 #include "absl/strings/str_replace.h"
23
24 #include "src/core/lib/gpr/env.h"
25
26 namespace grpc_core {
27
28 namespace {
29
30 const char* kExpectedEnvironmentId = "aws1";
31
32 const char* kRegionEnvVar = "AWS_REGION";
33 const char* kDefaultRegionEnvVar = "AWS_DEFAULT_REGION";
34 const char* kAccessKeyIdEnvVar = "AWS_ACCESS_KEY_ID";
35 const char* kSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY";
36 const char* kSessionTokenEnvVar = "AWS_SESSION_TOKEN";
37
UrlEncode(const absl::string_view & s)38 std::string UrlEncode(const absl::string_view& s) {
39 const char* hex = "0123456789ABCDEF";
40 std::string result;
41 result.reserve(s.length());
42 for (auto c : s) {
43 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
44 (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
45 c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
46 result.push_back(c);
47 } else {
48 result.push_back('%');
49 result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
50 result.push_back(hex[static_cast<unsigned char>(c) & 15]);
51 }
52 }
53 return result;
54 }
55
56 } // namespace
57
58 RefCountedPtr<AwsExternalAccountCredentials>
Create(Options options,std::vector<std::string> scopes,grpc_error_handle * error)59 AwsExternalAccountCredentials::Create(Options options,
60 std::vector<std::string> scopes,
61 grpc_error_handle* error) {
62 auto creds = MakeRefCounted<AwsExternalAccountCredentials>(
63 std::move(options), std::move(scopes), error);
64 if (*error == GRPC_ERROR_NONE) {
65 return creds;
66 } else {
67 return nullptr;
68 }
69 }
70
AwsExternalAccountCredentials(Options options,std::vector<std::string> scopes,grpc_error_handle * error)71 AwsExternalAccountCredentials::AwsExternalAccountCredentials(
72 Options options, std::vector<std::string> scopes, grpc_error_handle* error)
73 : ExternalAccountCredentials(options, std::move(scopes)) {
74 audience_ = options.audience;
75 auto it = options.credential_source.object_value().find("environment_id");
76 if (it == options.credential_source.object_value().end()) {
77 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
78 "environment_id field not present.");
79 return;
80 }
81 if (it->second.type() != Json::Type::STRING) {
82 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
83 "environment_id field must be a string.");
84 return;
85 }
86 if (it->second.string_value() != kExpectedEnvironmentId) {
87 *error =
88 GRPC_ERROR_CREATE_FROM_STATIC_STRING("environment_id does not match.");
89 return;
90 }
91 it = options.credential_source.object_value().find("region_url");
92 if (it == options.credential_source.object_value().end()) {
93 *error =
94 GRPC_ERROR_CREATE_FROM_STATIC_STRING("region_url field not present.");
95 return;
96 }
97 if (it->second.type() != Json::Type::STRING) {
98 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
99 "region_url field must be a string.");
100 return;
101 }
102 region_url_ = it->second.string_value();
103 it = options.credential_source.object_value().find("url");
104 if (it != options.credential_source.object_value().end() &&
105 it->second.type() == Json::Type::STRING) {
106 url_ = it->second.string_value();
107 }
108 it = options.credential_source.object_value().find(
109 "regional_cred_verification_url");
110 if (it == options.credential_source.object_value().end()) {
111 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
112 "regional_cred_verification_url field not present.");
113 return;
114 }
115 if (it->second.type() != Json::Type::STRING) {
116 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
117 "regional_cred_verification_url field must be a string.");
118 return;
119 }
120 regional_cred_verification_url_ = it->second.string_value();
121 }
122
RetrieveSubjectToken(HTTPRequestContext * ctx,const Options &,std::function<void (std::string,grpc_error_handle)> cb)123 void AwsExternalAccountCredentials::RetrieveSubjectToken(
124 HTTPRequestContext* ctx, const Options& /*options*/,
125 std::function<void(std::string, grpc_error_handle)> cb) {
126 if (ctx == nullptr) {
127 FinishRetrieveSubjectToken(
128 "",
129 GRPC_ERROR_CREATE_FROM_STATIC_STRING(
130 "Missing HTTPRequestContext to start subject token retrieval."));
131 return;
132 }
133 ctx_ = ctx;
134 cb_ = cb;
135 if (signer_ != nullptr) {
136 BuildSubjectToken();
137 } else {
138 RetrieveRegion();
139 }
140 }
141
RetrieveRegion()142 void AwsExternalAccountCredentials::RetrieveRegion() {
143 UniquePtr<char> region_from_env(gpr_getenv(kRegionEnvVar));
144 if (region_from_env == nullptr) {
145 region_from_env = UniquePtr<char>(gpr_getenv(kDefaultRegionEnvVar));
146 }
147 if (region_from_env != nullptr) {
148 region_ = std::string(region_from_env.get());
149 if (url_.empty()) {
150 RetrieveSigningKeys();
151 } else {
152 RetrieveRoleName();
153 }
154 return;
155 }
156 absl::StatusOr<URI> uri = URI::Parse(region_url_);
157 if (!uri.ok()) {
158 FinishRetrieveSubjectToken("", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
159 absl::StrFormat("Invalid region url. %s",
160 uri.status().ToString())
161 .c_str()));
162 return;
163 }
164 grpc_httpcli_request request;
165 memset(&request, 0, sizeof(grpc_httpcli_request));
166 request.host = const_cast<char*>(uri->authority().c_str());
167 request.http.path = gpr_strdup(uri->path().c_str());
168 request.handshaker =
169 uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
170 grpc_resource_quota* resource_quota =
171 grpc_resource_quota_create("external_account_credentials");
172 grpc_http_response_destroy(&ctx_->response);
173 ctx_->response = {};
174 GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRegion, this, nullptr);
175 grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
176 &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
177 grpc_resource_quota_unref_internal(resource_quota);
178 grpc_http_request_destroy(&request.http);
179 }
180
OnRetrieveRegion(void * arg,grpc_error_handle error)181 void AwsExternalAccountCredentials::OnRetrieveRegion(void* arg,
182 grpc_error_handle error) {
183 AwsExternalAccountCredentials* self =
184 static_cast<AwsExternalAccountCredentials*>(arg);
185 self->OnRetrieveRegionInternal(GRPC_ERROR_REF(error));
186 }
187
OnRetrieveRegionInternal(grpc_error_handle error)188 void AwsExternalAccountCredentials::OnRetrieveRegionInternal(
189 grpc_error_handle error) {
190 if (error != GRPC_ERROR_NONE) {
191 FinishRetrieveSubjectToken("", error);
192 return;
193 }
194 // Remove the last letter of availability zone to get pure region
195 absl::string_view response_body(ctx_->response.body,
196 ctx_->response.body_length);
197 region_ = std::string(response_body.substr(0, response_body.size() - 1));
198 if (url_.empty()) {
199 RetrieveSigningKeys();
200 } else {
201 RetrieveRoleName();
202 }
203 }
204
RetrieveRoleName()205 void AwsExternalAccountCredentials::RetrieveRoleName() {
206 absl::StatusOr<URI> uri = URI::Parse(url_);
207 if (!uri.ok()) {
208 FinishRetrieveSubjectToken(
209 "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
210 absl::StrFormat("Invalid url: %s.", uri.status().ToString())
211 .c_str()));
212 return;
213 }
214 grpc_httpcli_request request;
215 memset(&request, 0, sizeof(grpc_httpcli_request));
216 request.host = const_cast<char*>(uri->authority().c_str());
217 request.http.path = gpr_strdup(uri->path().c_str());
218 request.handshaker =
219 uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
220 grpc_resource_quota* resource_quota =
221 grpc_resource_quota_create("external_account_credentials");
222 grpc_http_response_destroy(&ctx_->response);
223 ctx_->response = {};
224 GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRoleName, this, nullptr);
225 grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
226 &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
227 grpc_resource_quota_unref_internal(resource_quota);
228 grpc_http_request_destroy(&request.http);
229 }
230
OnRetrieveRoleName(void * arg,grpc_error_handle error)231 void AwsExternalAccountCredentials::OnRetrieveRoleName(
232 void* arg, grpc_error_handle error) {
233 AwsExternalAccountCredentials* self =
234 static_cast<AwsExternalAccountCredentials*>(arg);
235 self->OnRetrieveRoleNameInternal(GRPC_ERROR_REF(error));
236 }
237
OnRetrieveRoleNameInternal(grpc_error_handle error)238 void AwsExternalAccountCredentials::OnRetrieveRoleNameInternal(
239 grpc_error_handle error) {
240 if (error != GRPC_ERROR_NONE) {
241 FinishRetrieveSubjectToken("", error);
242 return;
243 }
244 role_name_ = std::string(ctx_->response.body, ctx_->response.body_length);
245 RetrieveSigningKeys();
246 }
247
RetrieveSigningKeys()248 void AwsExternalAccountCredentials::RetrieveSigningKeys() {
249 UniquePtr<char> access_key_id_from_env(gpr_getenv(kAccessKeyIdEnvVar));
250 UniquePtr<char> secret_access_key_from_env(
251 gpr_getenv(kSecretAccessKeyEnvVar));
252 UniquePtr<char> token_from_env(gpr_getenv(kSessionTokenEnvVar));
253 if (access_key_id_from_env != nullptr &&
254 secret_access_key_from_env != nullptr && token_from_env != nullptr) {
255 access_key_id_ = std::string(access_key_id_from_env.get());
256 secret_access_key_ = std::string(secret_access_key_from_env.get());
257 token_ = std::string(token_from_env.get());
258 BuildSubjectToken();
259 return;
260 }
261 if (role_name_.empty()) {
262 FinishRetrieveSubjectToken(
263 "", GRPC_ERROR_CREATE_FROM_STATIC_STRING(
264 "Missing role name when retrieving signing keys."));
265 return;
266 }
267 std::string url_with_role_name = absl::StrCat(url_, "/", role_name_);
268 absl::StatusOr<URI> uri = URI::Parse(url_with_role_name);
269 if (!uri.ok()) {
270 FinishRetrieveSubjectToken(
271 "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
272 absl::StrFormat("Invalid url with role name: %s.",
273 uri.status().ToString())
274 .c_str()));
275 return;
276 }
277 grpc_httpcli_request request;
278 memset(&request, 0, sizeof(grpc_httpcli_request));
279 request.host = const_cast<char*>(uri->authority().c_str());
280 request.http.path = gpr_strdup(uri->path().c_str());
281 request.handshaker =
282 uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
283 grpc_resource_quota* resource_quota =
284 grpc_resource_quota_create("external_account_credentials");
285 grpc_http_response_destroy(&ctx_->response);
286 ctx_->response = {};
287 GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveSigningKeys, this, nullptr);
288 grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
289 &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
290 grpc_resource_quota_unref_internal(resource_quota);
291 grpc_http_request_destroy(&request.http);
292 }
293
OnRetrieveSigningKeys(void * arg,grpc_error_handle error)294 void AwsExternalAccountCredentials::OnRetrieveSigningKeys(
295 void* arg, grpc_error_handle error) {
296 AwsExternalAccountCredentials* self =
297 static_cast<AwsExternalAccountCredentials*>(arg);
298 self->OnRetrieveSigningKeysInternal(GRPC_ERROR_REF(error));
299 }
300
OnRetrieveSigningKeysInternal(grpc_error_handle error)301 void AwsExternalAccountCredentials::OnRetrieveSigningKeysInternal(
302 grpc_error_handle error) {
303 if (error != GRPC_ERROR_NONE) {
304 FinishRetrieveSubjectToken("", error);
305 return;
306 }
307 absl::string_view response_body(ctx_->response.body,
308 ctx_->response.body_length);
309 Json json = Json::Parse(response_body, &error);
310 if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
311 FinishRetrieveSubjectToken(
312 "", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
313 "Invalid retrieve signing keys response.", &error, 1));
314 GRPC_ERROR_UNREF(error);
315 return;
316 }
317 auto it = json.object_value().find("AccessKeyId");
318 if (it != json.object_value().end() &&
319 it->second.type() == Json::Type::STRING) {
320 access_key_id_ = it->second.string_value();
321 } else {
322 FinishRetrieveSubjectToken(
323 "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
324 absl::StrFormat("Missing or invalid AccessKeyId in %s.",
325 response_body)
326 .c_str()));
327 return;
328 }
329 it = json.object_value().find("SecretAccessKey");
330 if (it != json.object_value().end() &&
331 it->second.type() == Json::Type::STRING) {
332 secret_access_key_ = it->second.string_value();
333 } else {
334 FinishRetrieveSubjectToken(
335 "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
336 absl::StrFormat("Missing or invalid SecretAccessKey in %s.",
337 response_body)
338 .c_str()));
339 return;
340 }
341 it = json.object_value().find("Token");
342 if (it != json.object_value().end() &&
343 it->second.type() == Json::Type::STRING) {
344 token_ = it->second.string_value();
345 } else {
346 FinishRetrieveSubjectToken(
347 "",
348 GRPC_ERROR_CREATE_FROM_COPIED_STRING(
349 absl::StrFormat("Missing or invalid Token in %s.", response_body)
350 .c_str()));
351 return;
352 }
353 BuildSubjectToken();
354 }
355
BuildSubjectToken()356 void AwsExternalAccountCredentials::BuildSubjectToken() {
357 grpc_error_handle error = GRPC_ERROR_NONE;
358 if (signer_ == nullptr) {
359 cred_verification_url_ = absl::StrReplaceAll(
360 regional_cred_verification_url_, {{"{region}", region_}});
361 signer_ = absl::make_unique<AwsRequestSigner>(
362 access_key_id_, secret_access_key_, token_, "POST",
363 cred_verification_url_, region_, "",
364 std::map<std::string, std::string>(), &error);
365 if (error != GRPC_ERROR_NONE) {
366 FinishRetrieveSubjectToken(
367 "", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
368 "Creating aws request signer failed.", &error, 1));
369 GRPC_ERROR_UNREF(error);
370 return;
371 }
372 }
373 auto signed_headers = signer_->GetSignedRequestHeaders();
374 if (error != GRPC_ERROR_NONE) {
375 FinishRetrieveSubjectToken("",
376 GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
377 "Invalid getting signed request"
378 "headers.",
379 &error, 1));
380 GRPC_ERROR_UNREF(error);
381 return;
382 }
383 // Construct subject token
384 Json::Array headers;
385 headers.push_back(Json(
386 {{"key", "Authorization"}, {"value", signed_headers["Authorization"]}}));
387 headers.push_back(Json({{"key", "host"}, {"value", signed_headers["host"]}}));
388 headers.push_back(
389 Json({{"key", "x-amz-date"}, {"value", signed_headers["x-amz-date"]}}));
390 headers.push_back(Json({{"key", "x-amz-security-token"},
391 {"value", signed_headers["x-amz-security-token"]}}));
392 headers.push_back(
393 Json({{"key", "x-goog-cloud-target-resource"}, {"value", audience_}}));
394 Json::Object object{{"url", Json(cred_verification_url_)},
395 {"method", Json("POST")},
396 {"headers", Json(headers)}};
397 Json subject_token_json(object);
398 std::string subject_token = UrlEncode(subject_token_json.Dump());
399 FinishRetrieveSubjectToken(subject_token, GRPC_ERROR_NONE);
400 }
401
FinishRetrieveSubjectToken(std::string subject_token,grpc_error_handle error)402 void AwsExternalAccountCredentials::FinishRetrieveSubjectToken(
403 std::string subject_token, grpc_error_handle error) {
404 // Reset context
405 ctx_ = nullptr;
406 // Move object state into local variables.
407 auto cb = cb_;
408 cb_ = nullptr;
409 // Invoke the callback.
410 if (error != GRPC_ERROR_NONE) {
411 cb("", error);
412 } else {
413 cb(subject_token, GRPC_ERROR_NONE);
414 }
415 }
416
417 } // namespace grpc_core
418