• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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