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