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 "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
17
18 #include <grpc/credentials.h>
19 #include <grpc/grpc.h>
20 #include <grpc/grpc_security.h>
21 #include <grpc/support/alloc.h>
22 #include <grpc/support/json.h>
23 #include <grpc/support/port_platform.h>
24 #include <grpc/support/string_util.h>
25 #include <string.h>
26
27 #include <map>
28 #include <utility>
29
30 #include "absl/log/check.h"
31 #include "absl/status/status.h"
32 #include "absl/status/statusor.h"
33 #include "absl/strings/str_cat.h"
34 #include "absl/strings/str_format.h"
35 #include "absl/strings/str_replace.h"
36 #include "absl/strings/string_view.h"
37 #include "absl/types/optional.h"
38 #include "src/core/lib/iomgr/closure.h"
39 #include "src/core/lib/security/credentials/credentials.h"
40 #include "src/core/util/env.h"
41 #include "src/core/util/http_client/httpcli_ssl_credentials.h"
42 #include "src/core/util/json/json.h"
43 #include "src/core/util/json/json_reader.h"
44 #include "src/core/util/json/json_writer.h"
45 #include "src/core/util/uri.h"
46
47 namespace grpc_core {
48
49 namespace {
50
51 const char* kExpectedEnvironmentId = "aws1";
52
53 const char* kRegionEnvVar = "AWS_REGION";
54 const char* kDefaultRegionEnvVar = "AWS_DEFAULT_REGION";
55 const char* kAccessKeyIdEnvVar = "AWS_ACCESS_KEY_ID";
56 const char* kSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY";
57 const char* kSessionTokenEnvVar = "AWS_SESSION_TOKEN";
58
ShouldUseMetadataServer()59 bool ShouldUseMetadataServer() {
60 return !((GetEnv(kRegionEnvVar).has_value() ||
61 GetEnv(kDefaultRegionEnvVar).has_value()) &&
62 (GetEnv(kAccessKeyIdEnvVar).has_value() &&
63 GetEnv(kSecretAccessKeyEnvVar).has_value()));
64 }
65
UrlEncode(const absl::string_view & s)66 std::string UrlEncode(const absl::string_view& s) {
67 const char* hex = "0123456789ABCDEF";
68 std::string result;
69 result.reserve(s.length());
70 for (auto c : s) {
71 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
72 (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
73 c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
74 result.push_back(c);
75 } else {
76 result.push_back('%');
77 result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
78 result.push_back(hex[static_cast<unsigned char>(c) & 15]);
79 }
80 }
81 return result;
82 }
83
84 } // namespace
85
86 //
87 // AwsExternalAccountCredentials::AwsFetchBody
88 //
89
AwsFetchBody(absl::AnyInvocable<void (absl::StatusOr<std::string>)> on_done,AwsExternalAccountCredentials * creds,Timestamp deadline)90 AwsExternalAccountCredentials::AwsFetchBody::AwsFetchBody(
91 absl::AnyInvocable<void(absl::StatusOr<std::string>)> on_done,
92 AwsExternalAccountCredentials* creds, Timestamp deadline)
93 : FetchBody(std::move(on_done)), creds_(creds), deadline_(deadline) {
94 MutexLock lock(&mu_);
95 // Do a quick async hop here, so that we can invoke the callback at
96 // any time without deadlocking.
97 fetch_body_ = MakeOrphanable<NoOpFetchBody>(
98 creds->event_engine(),
99 [self = RefAsSubclass<AwsFetchBody>()](
100 absl::StatusOr<std::string> /*result*/) { self->Start(); },
101 "");
102 }
103
Shutdown()104 void AwsExternalAccountCredentials::AwsFetchBody::Shutdown() {
105 MutexLock lock(&mu_);
106 fetch_body_.reset();
107 }
108
AsyncFinish(absl::StatusOr<std::string> result)109 void AwsExternalAccountCredentials::AwsFetchBody::AsyncFinish(
110 absl::StatusOr<std::string> result) {
111 creds_->event_engine().Run(
112 [this, self = Ref(), result = std::move(result)]() mutable {
113 ApplicationCallbackExecCtx application_exec_ctx;
114 ExecCtx exec_ctx;
115 Finish(std::move(result));
116 self.reset();
117 });
118 }
119
MaybeFail(absl::Status status)120 bool AwsExternalAccountCredentials::AwsFetchBody::MaybeFail(
121 absl::Status status) {
122 if (!status.ok()) {
123 AsyncFinish(std::move(status));
124 return true;
125 }
126 if (fetch_body_ == nullptr) {
127 AsyncFinish(
128 absl::CancelledError("external account credentials fetch cancelled"));
129 return true;
130 }
131 return false;
132 }
133
Start()134 void AwsExternalAccountCredentials::AwsFetchBody::Start() {
135 MutexLock lock(&mu_);
136 if (MaybeFail(absl::OkStatus())) return;
137 if (!creds_->imdsv2_session_token_url_.empty() && ShouldUseMetadataServer()) {
138 RetrieveImdsV2SessionToken();
139 } else if (creds_->signer_ != nullptr) {
140 BuildSubjectToken();
141 } else {
142 RetrieveRegion();
143 }
144 }
145
RetrieveImdsV2SessionToken()146 void AwsExternalAccountCredentials::AwsFetchBody::RetrieveImdsV2SessionToken() {
147 absl::StatusOr<URI> uri = URI::Parse(creds_->imdsv2_session_token_url_);
148 if (!uri.ok()) {
149 AsyncFinish(uri.status());
150 return;
151 }
152 fetch_body_ = MakeOrphanable<HttpFetchBody>(
153 [&](grpc_http_response* response, grpc_closure* on_http_response) {
154 grpc_http_header* headers = static_cast<grpc_http_header*>(
155 gpr_malloc(sizeof(grpc_http_header)));
156 headers[0].key = gpr_strdup("x-aws-ec2-metadata-token-ttl-seconds");
157 headers[0].value = gpr_strdup("300");
158 grpc_http_request request;
159 memset(&request, 0, sizeof(grpc_http_request));
160 request.hdr_count = 1;
161 request.hdrs = headers;
162 RefCountedPtr<grpc_channel_credentials> http_request_creds;
163 if (uri->scheme() == "http") {
164 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
165 grpc_insecure_credentials_create());
166 } else {
167 http_request_creds = CreateHttpRequestSSLCredentials();
168 }
169 auto http_request = HttpRequest::Put(
170 std::move(*uri), /*args=*/nullptr, creds_->pollent(), &request,
171 deadline_, on_http_response, response,
172 std::move(http_request_creds));
173 http_request->Start();
174 grpc_http_request_destroy(&request);
175 return http_request;
176 },
177 [self =
178 RefAsSubclass<AwsFetchBody>()](absl::StatusOr<std::string> result) {
179 MutexLock lock(&self->mu_);
180 if (self->MaybeFail(result.status())) return;
181 self->imdsv2_session_token_ = std::move(*result);
182 if (self->creds_->signer_ != nullptr) {
183 self->BuildSubjectToken();
184 } else {
185 self->RetrieveRegion();
186 }
187 });
188 }
189
RetrieveRegion()190 void AwsExternalAccountCredentials::AwsFetchBody::RetrieveRegion() {
191 auto region_from_env = GetEnv(kRegionEnvVar);
192 if (!region_from_env.has_value()) {
193 region_from_env = GetEnv(kDefaultRegionEnvVar);
194 }
195 if (region_from_env.has_value()) {
196 region_ = std::move(*region_from_env);
197 if (creds_->url_.empty()) {
198 RetrieveSigningKeys();
199 } else {
200 RetrieveRoleName();
201 }
202 return;
203 }
204 absl::StatusOr<URI> uri = URI::Parse(creds_->region_url_);
205 if (!uri.ok()) {
206 AsyncFinish(GRPC_ERROR_CREATE(
207 absl::StrFormat("Invalid region url. %s", uri.status().ToString())));
208 return;
209 }
210 fetch_body_ = MakeOrphanable<HttpFetchBody>(
211 [&](grpc_http_response* response, grpc_closure* on_http_response) {
212 grpc_http_request request;
213 memset(&request, 0, sizeof(grpc_http_request));
214 AddMetadataRequestHeaders(&request);
215 RefCountedPtr<grpc_channel_credentials> http_request_creds;
216 if (uri->scheme() == "http") {
217 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
218 grpc_insecure_credentials_create());
219 } else {
220 http_request_creds = CreateHttpRequestSSLCredentials();
221 }
222 auto http_request = HttpRequest::Get(
223 std::move(*uri), /*args=*/nullptr, creds_->pollent(), &request,
224 deadline_, on_http_response, response,
225 std::move(http_request_creds));
226 http_request->Start();
227 grpc_http_request_destroy(&request);
228 return http_request;
229 },
230 [self =
231 RefAsSubclass<AwsFetchBody>()](absl::StatusOr<std::string> result) {
232 MutexLock lock(&self->mu_);
233 if (self->MaybeFail(result.status())) return;
234 // Remove the last letter of availability zone to get pure region
235 self->region_ = result->substr(0, result->size() - 1);
236 if (self->creds_->url_.empty()) {
237 self->RetrieveSigningKeys();
238 } else {
239 self->RetrieveRoleName();
240 }
241 });
242 }
243
RetrieveRoleName()244 void AwsExternalAccountCredentials::AwsFetchBody::RetrieveRoleName() {
245 absl::StatusOr<URI> uri = URI::Parse(creds_->url_);
246 if (!uri.ok()) {
247 AsyncFinish(GRPC_ERROR_CREATE(
248 absl::StrFormat("Invalid url: %s.", uri.status().ToString())));
249 return;
250 }
251 fetch_body_ = MakeOrphanable<HttpFetchBody>(
252 [&](grpc_http_response* response, grpc_closure* on_http_response) {
253 grpc_http_request request;
254 memset(&request, 0, sizeof(grpc_http_request));
255 AddMetadataRequestHeaders(&request);
256 // TODO(ctiller): use the caller's resource quota.
257 RefCountedPtr<grpc_channel_credentials> http_request_creds;
258 if (uri->scheme() == "http") {
259 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
260 grpc_insecure_credentials_create());
261 } else {
262 http_request_creds = CreateHttpRequestSSLCredentials();
263 }
264 auto http_request = HttpRequest::Get(
265 std::move(*uri), /*args=*/nullptr, creds_->pollent(), &request,
266 deadline_, on_http_response, response,
267 std::move(http_request_creds));
268 http_request->Start();
269 grpc_http_request_destroy(&request);
270 return http_request;
271 },
272 [self =
273 RefAsSubclass<AwsFetchBody>()](absl::StatusOr<std::string> result) {
274 MutexLock lock(&self->mu_);
275 if (self->MaybeFail(result.status())) return;
276 self->role_name_ = std::move(*result);
277 self->RetrieveSigningKeys();
278 });
279 }
280
RetrieveSigningKeys()281 void AwsExternalAccountCredentials::AwsFetchBody::RetrieveSigningKeys() {
282 auto access_key_id_from_env = GetEnv(kAccessKeyIdEnvVar);
283 auto secret_access_key_from_env = GetEnv(kSecretAccessKeyEnvVar);
284 auto token_from_env = GetEnv(kSessionTokenEnvVar);
285 if (access_key_id_from_env.has_value() &&
286 secret_access_key_from_env.has_value()) {
287 access_key_id_ = std::move(*access_key_id_from_env);
288 secret_access_key_ = std::move(*secret_access_key_from_env);
289 if (token_from_env.has_value()) {
290 token_ = std::move(*token_from_env);
291 }
292 BuildSubjectToken();
293 return;
294 }
295 if (role_name_.empty()) {
296 AsyncFinish(
297 GRPC_ERROR_CREATE("Missing role name when retrieving signing keys."));
298 return;
299 }
300 std::string url_with_role_name = absl::StrCat(creds_->url_, "/", role_name_);
301 absl::StatusOr<URI> uri = URI::Parse(url_with_role_name);
302 if (!uri.ok()) {
303 AsyncFinish(GRPC_ERROR_CREATE(absl::StrFormat(
304 "Invalid url with role name: %s.", uri.status().ToString())));
305 return;
306 }
307 fetch_body_ = MakeOrphanable<HttpFetchBody>(
308 [&](grpc_http_response* response, grpc_closure* on_http_response) {
309 grpc_http_request request;
310 memset(&request, 0, sizeof(grpc_http_request));
311 AddMetadataRequestHeaders(&request);
312 // TODO(ctiller): use the caller's resource quota.
313 RefCountedPtr<grpc_channel_credentials> http_request_creds;
314 if (uri->scheme() == "http") {
315 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
316 grpc_insecure_credentials_create());
317 } else {
318 http_request_creds = CreateHttpRequestSSLCredentials();
319 }
320 auto http_request = HttpRequest::Get(
321 std::move(*uri), /*args=*/nullptr, creds_->pollent(), &request,
322 deadline_, on_http_response, response,
323 std::move(http_request_creds));
324 http_request->Start();
325 grpc_http_request_destroy(&request);
326 return http_request;
327 },
328 [self =
329 RefAsSubclass<AwsFetchBody>()](absl::StatusOr<std::string> result) {
330 MutexLock lock(&self->mu_);
331 if (self->MaybeFail(result.status())) return;
332 self->OnRetrieveSigningKeys(std::move(*result));
333 });
334 }
335
OnRetrieveSigningKeys(std::string result)336 void AwsExternalAccountCredentials::AwsFetchBody::OnRetrieveSigningKeys(
337 std::string result) {
338 auto json = JsonParse(result);
339 if (!json.ok()) {
340 AsyncFinish(GRPC_ERROR_CREATE(absl::StrCat(
341 "Invalid retrieve signing keys response: ", json.status().ToString())));
342 return;
343 }
344 if (json->type() != Json::Type::kObject) {
345 AsyncFinish(
346 GRPC_ERROR_CREATE("Invalid retrieve signing keys response: "
347 "JSON type is not object"));
348 return;
349 }
350 auto it = json->object().find("AccessKeyId");
351 if (it != json->object().end() && it->second.type() == Json::Type::kString) {
352 access_key_id_ = it->second.string();
353 } else {
354 AsyncFinish(GRPC_ERROR_CREATE(
355 absl::StrFormat("Missing or invalid AccessKeyId in %s.", result)));
356 return;
357 }
358 it = json->object().find("SecretAccessKey");
359 if (it != json->object().end() && it->second.type() == Json::Type::kString) {
360 secret_access_key_ = it->second.string();
361 } else {
362 AsyncFinish(GRPC_ERROR_CREATE(
363 absl::StrFormat("Missing or invalid SecretAccessKey in %s.", result)));
364 return;
365 }
366 it = json->object().find("Token");
367 if (it != json->object().end() && it->second.type() == Json::Type::kString) {
368 token_ = it->second.string();
369 } else {
370 AsyncFinish(GRPC_ERROR_CREATE(
371 absl::StrFormat("Missing or invalid Token in %s.", result)));
372 return;
373 }
374 BuildSubjectToken();
375 }
376
BuildSubjectToken()377 void AwsExternalAccountCredentials::AwsFetchBody::BuildSubjectToken() {
378 grpc_error_handle error;
379 if (creds_->signer_ == nullptr) {
380 creds_->cred_verification_url_ = absl::StrReplaceAll(
381 creds_->regional_cred_verification_url_, {{"{region}", region_}});
382 creds_->signer_ = std::make_unique<AwsRequestSigner>(
383 access_key_id_, secret_access_key_, token_, "POST",
384 creds_->cred_verification_url_, region_, "",
385 std::map<std::string, std::string>(), &error);
386 if (!error.ok()) {
387 AsyncFinish(GRPC_ERROR_CREATE_REFERENCING(
388 "Creating aws request signer failed.", &error, 1));
389 return;
390 }
391 }
392 auto signed_headers = creds_->signer_->GetSignedRequestHeaders();
393 if (!error.ok()) {
394 AsyncFinish(GRPC_ERROR_CREATE_REFERENCING(
395 "Invalid getting signed request headers.", &error, 1));
396 return;
397 }
398 // Construct subject token
399 Json::Array headers;
400 headers.push_back(Json::FromObject(
401 {{"key", Json::FromString("Authorization")},
402 {"value", Json::FromString(signed_headers["Authorization"])}}));
403 headers.push_back(
404 Json::FromObject({{"key", Json::FromString("host")},
405 {"value", Json::FromString(signed_headers["host"])}}));
406 headers.push_back(Json::FromObject(
407 {{"key", Json::FromString("x-amz-date")},
408 {"value", Json::FromString(signed_headers["x-amz-date"])}}));
409 headers.push_back(Json::FromObject(
410 {{"key", Json::FromString("x-amz-security-token")},
411 {"value", Json::FromString(signed_headers["x-amz-security-token"])}}));
412 headers.push_back(Json::FromObject(
413 {{"key", Json::FromString("x-goog-cloud-target-resource")},
414 {"value", Json::FromString(creds_->audience_)}}));
415 Json subject_token_json = Json::FromObject(
416 {{"url", Json::FromString(creds_->cred_verification_url_)},
417 {"method", Json::FromString("POST")},
418 {"headers", Json::FromArray(headers)}});
419 std::string subject_token = UrlEncode(JsonDump(subject_token_json));
420 AsyncFinish(std::move(subject_token));
421 }
422
AddMetadataRequestHeaders(grpc_http_request * request)423 void AwsExternalAccountCredentials::AwsFetchBody::AddMetadataRequestHeaders(
424 grpc_http_request* request) {
425 if (!imdsv2_session_token_.empty()) {
426 CHECK_EQ(request->hdr_count, 0u);
427 CHECK_EQ(request->hdrs, nullptr);
428 grpc_http_header* headers =
429 static_cast<grpc_http_header*>(gpr_malloc(sizeof(grpc_http_header)));
430 headers[0].key = gpr_strdup("x-aws-ec2-metadata-token");
431 headers[0].value = gpr_strdup(imdsv2_session_token_.c_str());
432 request->hdr_count = 1;
433 request->hdrs = headers;
434 }
435 }
436
437 //
438 // AwsExternalAccountCredentials
439 //
440
441 absl::StatusOr<RefCountedPtr<AwsExternalAccountCredentials>>
Create(Options options,std::vector<std::string> scopes,std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine)442 AwsExternalAccountCredentials::Create(
443 Options options, std::vector<std::string> scopes,
444 std::shared_ptr<grpc_event_engine::experimental::EventEngine>
445 event_engine) {
446 grpc_error_handle error;
447 auto creds = MakeRefCounted<AwsExternalAccountCredentials>(
448 std::move(options), std::move(scopes), std::move(event_engine), &error);
449 if (!error.ok()) return error;
450 return creds;
451 }
452
AwsExternalAccountCredentials(Options options,std::vector<std::string> scopes,std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine,grpc_error_handle * error)453 AwsExternalAccountCredentials::AwsExternalAccountCredentials(
454 Options options, std::vector<std::string> scopes,
455 std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine,
456 grpc_error_handle* error)
457 : ExternalAccountCredentials(options, std::move(scopes),
458 std::move(event_engine)) {
459 audience_ = options.audience;
460 auto it = options.credential_source.object().find("environment_id");
461 if (it == options.credential_source.object().end()) {
462 *error = GRPC_ERROR_CREATE("environment_id field not present.");
463 return;
464 }
465 if (it->second.type() != Json::Type::kString) {
466 *error = GRPC_ERROR_CREATE("environment_id field must be a string.");
467 return;
468 }
469 if (it->second.string() != kExpectedEnvironmentId) {
470 *error = GRPC_ERROR_CREATE("environment_id does not match.");
471 return;
472 }
473 it = options.credential_source.object().find("region_url");
474 if (it == options.credential_source.object().end()) {
475 *error = GRPC_ERROR_CREATE("region_url field not present.");
476 return;
477 }
478 if (it->second.type() != Json::Type::kString) {
479 *error = GRPC_ERROR_CREATE("region_url field must be a string.");
480 return;
481 }
482 region_url_ = it->second.string();
483 it = options.credential_source.object().find("url");
484 if (it != options.credential_source.object().end() &&
485 it->second.type() == Json::Type::kString) {
486 url_ = it->second.string();
487 }
488 it =
489 options.credential_source.object().find("regional_cred_verification_url");
490 if (it == options.credential_source.object().end()) {
491 *error =
492 GRPC_ERROR_CREATE("regional_cred_verification_url field not present.");
493 return;
494 }
495 if (it->second.type() != Json::Type::kString) {
496 *error = GRPC_ERROR_CREATE(
497 "regional_cred_verification_url field must be a string.");
498 return;
499 }
500 regional_cred_verification_url_ = it->second.string();
501 it = options.credential_source.object().find("imdsv2_session_token_url");
502 if (it != options.credential_source.object().end() &&
503 it->second.type() == Json::Type::kString) {
504 imdsv2_session_token_url_ = it->second.string();
505 }
506 }
507
debug_string()508 std::string AwsExternalAccountCredentials::debug_string() {
509 return absl::StrCat("AwsExternalAccountCredentials{Audience:", audience(),
510 ")");
511 }
512
Type()513 UniqueTypeName AwsExternalAccountCredentials::Type() {
514 static UniqueTypeName::Factory kFactory("AwsExternalAccountCredentials");
515 return kFactory.Create();
516 }
517
518 OrphanablePtr<ExternalAccountCredentials::FetchBody>
RetrieveSubjectToken(Timestamp deadline,absl::AnyInvocable<void (absl::StatusOr<std::string>)> on_done)519 AwsExternalAccountCredentials::RetrieveSubjectToken(
520 Timestamp deadline,
521 absl::AnyInvocable<void(absl::StatusOr<std::string>)> on_done) {
522 return MakeOrphanable<AwsFetchBody>(std::move(on_done), this, deadline);
523 }
524
CredentialSourceType()525 absl::string_view AwsExternalAccountCredentials::CredentialSourceType() {
526 return "aws";
527 }
528
529 } // namespace grpc_core
530