service_credentials_provider.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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  * A copy of the License is located at
7  *
8  * http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
17 
18 #include <aws/core/Aws.h>
19 #include <aws/core/utils/Array.h>
20 #include <aws/core/utils/DateTime.h>
21 #include <aws/core/utils/json/JsonSerializer.h>
22 #include <aws/core/utils/logging/LogMacros.h>
23 #include <curl/curl.h>
24 
27 
29 #define FIELD_CREDENTIALS "credentials"
30 #define FIELD_EXPIRATION "expiration"
32 #define FIELD_ACCESS_KEY "accessKeyId"
34 #define FIELD_SECRET_KEY "secretAccessKey"
36 #define FIELD_SESSION_TOKEN "sessionToken"
38 
40 #define HEADER_THING_NAME "x-amzn-iot-thingname"
41 
42 #ifndef MAX_IOT_CREDENTIAL_BYTES
43 #define MAX_IOT_CREDENTIAL_BYTES 65535
46 #endif
47 
49 static const char * AWS_LOG_TAG = "ServiceCredentialsProviderChain";
50 
52 static const double EXPIRATION_GRACE_BUFFER = 30.0;
53 
54 
55 namespace Aws {
56 namespace Auth {
57 
60 // Helper functions
61 //
62 
63 template <typename T>
64 static bool SetCurlOpt(CURL * curl, CURLoption opt, T lvalue);
65 static bool AppendHeader(struct curl_slist ** headers, const char * name, const char * value);
66 static bool IsIotConfigValid(const IotRoleConfig & config);
67 static bool ParseConfigVale(std::string & value, Aws::String & result);
68 static bool ParseConfigValue(std::string & value, int32_t & result);
69 template <typename T>
70 static bool GetConfigValue(std::map<std::string, std::string> & data, const char * name, T & result,
71  bool optional = false);
72 
81 template <typename T>
82 static bool SetCurlOpt(CURL * curl, CURLoption opt, T lvalue)
83 {
84  CURLcode res = curl_easy_setopt(curl, opt, lvalue);
85  if (res != CURLE_OK) {
86  AWS_LOG_ERROR(AWS_LOG_TAG, "Error setting curl option: %s", curl_easy_strerror(res));
87  return false;
88  }
89 
90  return true;
91 }
92 
104 static bool AppendHeader(struct curl_slist ** headers, const char * name, const char * value)
105 {
106  Aws::StringStream stream;
107  stream << name << ": " << value;
108 
109  struct curl_slist * next = curl_slist_append(*headers, stream.str().c_str());
110  if (next == nullptr) {
111  AWS_LOG_ERROR(AWS_LOG_TAG, "Error setting header[%s]: %s", name, value);
112  return false;
113  }
114 
115  *headers = next;
116  return true;
117 }
118 
124 static bool IsIotConfigValid(const IotRoleConfig & config)
125 {
126  return config.cafile.length() > 0 && config.certfile.length() > 0 &&
127  config.keyfile.length() > 0 && config.host.length() > 0 && config.role.length() > 0 &&
128  config.name.length() > 0 && config.connect_timeout_ms > 0 && config.total_timeout_ms > 0;
129 }
130 
137 static bool ParseConfigValue(std::string & value, Aws::String & result)
138 {
139  result = Aws::String(value.c_str(), value.size());
140  return true;
141 }
142 
151 static bool ParseConfigValue(std::string & value, int32_t & result)
152 {
153  int32_t tmp = Aws::Utils::StringUtils::ConvertToInt32(value.c_str());
154  if (tmp > 0) {
155  result = tmp;
156  return true;
157  }
158  return false;
159 }
160 
170 template <typename T>
171 static bool GetConfigValue(std::map<std::string, std::string> & data, const char * name, T & result,
172  bool optional)
173 {
174  auto it = data.find(name);
175  if (it != data.end()) {
176  if (ParseConfigValue(it->second, result) && !optional) {
177  return true;
178  }
179  }
180 
181  if (!optional) {
182  AWS_LOG_DEBUG(AWS_LOG_TAG, "IoT provider: Missing %s configuration value", name);
183  }
184  return false;
185 }
186 
197  const std::shared_ptr<Aws::Client::ParameterReaderInterface>& parameters)
198 {
199  bool failed = false;
200  Aws::String cafile, certfile, keyfile, host, role, name;
201  int connect_timeout_ms = DEFAULT_AUTH_CONNECT_TIMEOUT_MS;
202  int total_timeout_ms = DEFAULT_AUTH_TOTAL_TIMEOUT_MS;
203 
204  std::map<std::string, std::string> data;
205  if (parameters->ReadParam(Aws::Client::ParameterPath("iot"), data) != AWS_ERR_OK) {
206  failed = true;
207  } else {
208  if (!GetConfigValue(data, CFG_CAFILE, cafile)) { failed = true; }
209  if (!GetConfigValue(data, CFG_CERTFILE, certfile)) { failed = true; }
210  if (!GetConfigValue(data, CFG_KEYFILE, keyfile)) { failed = true; }
211  if (!GetConfigValue(data, CFG_ENDPOINT, host)) { failed = true; }
212  if (!GetConfigValue(data, CFG_ROLE, role)) { failed = true; }
213  if (!GetConfigValue(data, CFG_THING_NAME, name)) { failed = true; }
214 
215  if (!GetConfigValue(data, CFG_CONNECT_TIMEOUT_MS, connect_timeout_ms, true)) {
216  AWS_LOG_INFO(AWS_LOG_TAG, "Could not find config value %s, using default %d",
218  }
219 
220  if (!GetConfigValue(data, CFG_TOTAL_TIMEOUT_MS, total_timeout_ms, true)) {
221  AWS_LOG_INFO(AWS_LOG_TAG, "Could not find config value %s, using default %d",
223  }
224  }
225 
226  if (!failed) {
227  config.iot.cafile = cafile;
228  config.iot.certfile = certfile;
229  config.iot.keyfile = keyfile;
230  config.iot.host = host;
231  config.iot.role = role;
232  config.iot.name = name;
233  config.iot.connect_timeout_ms = connect_timeout_ms;
234  config.iot.total_timeout_ms = total_timeout_ms;
235 
236  AWS_LOG_INFO(AWS_LOG_TAG,
237  "IoT provider config: ca=%s,cert=%s,key=%s,ep=%s,role=%s,thing_name=%s,"
238  "connect_timeout=%d,total_timeout=%d",
239  config.iot.cafile.c_str(), config.iot.certfile.c_str(), config.iot.keyfile.c_str(),
240  config.iot.host.c_str(), config.iot.role.c_str(), config.iot.name.c_str(),
241  config.iot.connect_timeout_ms, config.iot.total_timeout_ms);
242  return true;
243  }
244 
245  AWS_LOG_INFO(AWS_LOG_TAG,
246  "Missing or incomplete 'iot' parameters, skipping IoT credential provider");
247  return false;
248 }
249 
252 // RequestContext Class
253 //
254 
266 {
267 public:
268  RequestContext() = default;
269  ~RequestContext() = default;
270 
281  static size_t WriteData(char * ptr, size_t /*size*/, size_t nmemb, void * userdata)
282  {
283  auto * ctx = static_cast<RequestContext *>(userdata);
284  size_t current = ctx->stream_.tellp();
285 
286  // Returning less than nmemb to curl indicates an error
287  if ((current + nmemb) > MAX_IOT_CREDENTIAL_BYTES) {
288  AWS_LOG_ERROR(AWS_LOG_TAG,
289  "IoT response was too large, current:%d bytes, read:%d bytes, max:%d bytes",
290  current, nmemb, MAX_IOT_CREDENTIAL_BYTES);
291  return 0;
292  }
293 
294  ctx->stream_.write(ptr, nmemb);
295  return nmemb;
296  }
297 
302  Aws::Utils::Json::JsonValue GetValue()
303  {
304  return Aws::Utils::Json::JsonValue(stream_.str());
305  }
306 
307 private:
309  Aws::StringStream stream_;
310 };
311 
314 // IotRoleCredentialsProvider Class
315 //
316 
318  : cached_("", ""), config_(config), expiry_(0.0)
319 {
322 }
323 
325 
327 {
328  if (IsTimeExpired()) {
329  AWS_LOG_DEBUG(AWS_LOG_TAG, "Timer has expired, refreshing AWS IoT Role credentials");
330  Refresh();
331  }
332 
333  return cached_;
334 }
335 
341 {
342  // 30s grace buffer so requests have time to finish before expiry.
343  return Aws::Utils::DateTime::Now().SecondsWithMSPrecision() > (expiry_.load() - EXPIRATION_GRACE_BUFFER);
344 }
345 
346 void IotRoleCredentialsProvider::SetCredentials(AWSCredentials & creds_obj)
347 {
348  cached_ = creds_obj;
349 }
350 
356 bool IotRoleCredentialsProvider::ValidateResponse(Aws::Utils::Json::JsonValue & value)
357 {
358  if (!value.WasParseSuccessful()) {
359  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to parse JSON response from AWS IoT credential provider");
360  return false;
361  }
362 
363  auto value_view = value.View();
364  if (!value_view.ValueExists(FIELD_CREDENTIALS)) {
365  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to find %s field in AWS IoT credential provider response",
367  return false;
368  }
369 
370  auto creds = value_view.GetObject(FIELD_CREDENTIALS);
371 
372  if (!creds.IsObject()) {
373  AWS_LOG_ERROR(AWS_LOG_TAG, "Expected object for %s in AWS IoT credential provider response",
375  return false;
376  }
377 
378  if (!creds.ValueExists(FIELD_EXPIRATION)) {
379  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to find %s field in AWS IoT credential provider response",
381  return false;
382  }
383 
384  if (!creds.ValueExists(FIELD_ACCESS_KEY)) {
385  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to find %s field in AWS IoT credentials", FIELD_ACCESS_KEY);
386  return false;
387  }
388 
389  if (!creds.ValueExists(FIELD_SECRET_KEY)) {
390  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to find %s in AWS IoT credentials", FIELD_SECRET_KEY);
391  return false;
392  }
393 
394  if (!creds.ValueExists(FIELD_SESSION_TOKEN)) {
395  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to find %s in AWS IoT credentials", FIELD_SESSION_TOKEN);
396  return false;
397  }
398 
399  AWS_LOG_INFO(AWS_LOG_TAG, "Found valid credentials response from IoT");
400 
401  return true;
402 }
403 
413 {
414  CURLcode res;
415  CURL * curl = nullptr;
416  struct curl_slist * headers = nullptr;
417 
418  AWS_LOG_INFO(AWS_LOG_TAG, "Retrieving IOT credentials!");
419 
420  std::lock_guard<std::mutex> lock(creds_mutex_);
421  if (!IsTimeExpired()) { return; }
422 
423  // Make at most 1 tps for new creds, we also have a 30s grace buffer
424  expiry_.store(Aws::Utils::DateTime::Now().SecondsWithMSPrecision() + EXPIRATION_GRACE_BUFFER + 1.0);
425 
426  curl = curl_easy_init();
427  if (curl == nullptr) {
428  AWS_LOG_ERROR(AWS_LOG_TAG, "Could not initialize curl!");
429  } else {
430  Aws::StringStream url_stream;
431  url_stream << "https://" << config_.host << "/role-aliases/" << config_.role << "/credentials";
432 
433  // Setup SSL options
434  if (!SetCurlOpt(curl, CURLOPT_SSL_VERIFYPEER, 1L)) { goto cleanup_curl; }
435  if (!SetCurlOpt(curl, CURLOPT_SSL_VERIFYHOST, 2L)) { goto cleanup_curl; }
436  if (!SetCurlOpt(curl, CURLOPT_HTTPGET, 1L)) { goto cleanup_curl; }
437  if (!SetCurlOpt(curl, CURLOPT_HEADER, 0L)) { goto cleanup_curl; }
438  if (!SetCurlOpt(curl, CURLOPT_CONNECTTIMEOUT_MS, config_.connect_timeout_ms)) { goto cleanup_curl; }
439  if (!SetCurlOpt(curl, CURLOPT_TIMEOUT_MS, config_.total_timeout_ms)) { goto cleanup_curl; }
440 
441  if (!SetCurlOpt(curl, CURLOPT_URL, url_stream.str().c_str())) { goto cleanup_curl; }
442 
443  // Configure client auth
444  if (!SetCurlOpt(curl, CURLOPT_CAINFO, config_.cafile.c_str())) { goto cleanup_curl; }
445 
446  if (!SetCurlOpt(curl, CURLOPT_SSLCERTTYPE, "PEM")) { goto cleanup_curl; }
447  if (!SetCurlOpt(curl, CURLOPT_SSLCERT, config_.certfile.c_str())) { goto cleanup_curl; }
448 
449  if (!SetCurlOpt(curl, CURLOPT_SSLKEYTYPE, "PEM")) { goto cleanup_curl; }
450  if (!SetCurlOpt(curl, CURLOPT_SSLKEY, config_.keyfile.c_str())) { goto cleanup_curl; }
451 
452  // Setup response handler
453  RequestContext ctx;
454  if (!SetCurlOpt(curl, CURLOPT_WRITEFUNCTION, &RequestContext::WriteData)) { goto cleanup_curl; }
455  if (!SetCurlOpt(curl, CURLOPT_WRITEDATA, &ctx)) { goto cleanup_curl; }
456 
457  // Setup request headers
458  if (!AppendHeader(&headers, "Accept", "application/json")) { goto cleanup_curl; }
459  if (!AppendHeader(&headers, "Host", config_.host.c_str())) { goto cleanup_curl; }
460  if (!AppendHeader(&headers, HEADER_THING_NAME, config_.name.c_str())) { goto cleanup_curl; }
461 
462  if (!SetCurlOpt(curl, CURLOPT_HTTPHEADER, headers)) { goto cleanup_curl; }
463 
464  // Make the request
465  res = curl_easy_perform(curl);
466  if (res != CURLE_OK) {
467  AWS_LOG_ERROR(AWS_LOG_TAG, "Error when curling endpoint: %s", curl_easy_strerror(res));
468  goto cleanup_curl;
469  }
470 
471  // Parse JSON response
472  auto value = ctx.GetValue();
473  if (!ValidateResponse(value)) { goto cleanup_curl; }
474 
475  auto creds_obj = value.View().GetObject(FIELD_CREDENTIALS);
476 
477  // Retrieve expiration date
478  auto expires_str = creds_obj.GetString(FIELD_EXPIRATION);
479 
480  Aws::Utils::DateTime expiration(expires_str, Aws::Utils::DateFormat::ISO_8601);
481  if (!expiration.WasParseSuccessful()) {
482  AWS_LOG_ERROR(AWS_LOG_TAG, "Unable to parse expiration: %s", expires_str.c_str());
483  goto cleanup_curl;
484  }
485  AWS_LOG_INFO(AWS_LOG_TAG, "Retrieved AWS creds from IoT, next expiration %s",
486  expires_str.c_str());
487 
488  cached_.SetAWSAccessKeyId(creds_obj.GetString(FIELD_ACCESS_KEY));
489  cached_.SetAWSSecretKey(creds_obj.GetString(FIELD_SECRET_KEY));
490  cached_.SetSessionToken(creds_obj.GetString(FIELD_SESSION_TOKEN));
491 
492  expiry_.store(expiration.SecondsWithMSPrecision());
493  }
494 
495 cleanup_curl:
496  if (headers != nullptr) {
497  curl_slist_free_all(headers);
498  headers = nullptr;
499  }
500 
501  if (curl != nullptr) {
502  curl_easy_cleanup(curl);
503  curl = nullptr;
504  }
505 }
506 
509 // ServiceCredentialsProviderChain Class
510 //
511 
513 
515 {
516  if (IsIotConfigValid(config.iot)) {
517  AWS_LOG_INFO(AWS_LOG_TAG, "Found valid IoT auth config, adding IotRoleCredentialsProvider");
518  auto provider = Aws::MakeShared<IotRoleCredentialsProvider>(__func__, config.iot);
519  AddProvider(provider);
520  } else {
521  AWS_LOG_INFO(AWS_LOG_TAG, "No valid IoT auth config, skipping IotRoleCredentialsProvider");
522  }
523 }
524 
525 } /* namespace Auth */
526 } /* namespace Aws */
static const char CFG_THING_NAME[]
void Refresh()
Refreshes the cached AWS credentials.
static const char * AWS_LOG_TAG
Logging tag used for all messages emitting from this module.
Aws::String host
Host name of the iot:CredentialProvider endpoint.
static bool SetCurlOpt(CURL *curl, CURLoption opt, T lvalue)
Helper to set a libcurl option or log an error if a problem occurred.
static bool ParseConfigValue(std::string &value, int32_t &result)
Helper to parse a configuration value into an Aws::String variable This function assumes that the val...
static const char CFG_TOTAL_TIMEOUT_MS[]
std::mutex creds_mutex_
Mutex to ensure only a single request is outstanding at any given time.
IotRoleConfig iot
IoT-specific configuration.
static const char CFG_CONNECT_TIMEOUT_MS[]
#define HEADER_THING_NAME
Header name to append to the request containing the thing name.
static const char CFG_CAFILE[]
static const long DEFAULT_AUTH_TOTAL_TIMEOUT_MS
Default number of milliseconds to wait before timing out when retrieving credentials from IoT...
void SetCredentials(AWSCredentials &creds_obj)
Sets the cached credentials.
Encapsulates the response from a curl request Curl uses a callback when performing a request for chun...
static const char CFG_CERTFILE[]
static bool IsIotConfigValid(const IotRoleConfig &config)
Validates an instance of an IotRoleConfig struct.
static size_t WriteData(char *ptr, size_t, size_t nmemb, void *userdata)
Curl callback for CURLOPT_WRITEFUNCTION Recieves callbacks from the curl library any time data is rec...
#define MAX_IOT_CREDENTIAL_BYTES
long connect_timeout_ms
Number of ms to wait before timing out when connecting to the endpoint.
static const char CFG_ROLE[]
#define FIELD_SESSION_TOKEN
Name of the field containing the session token in the json response from IoT.
static const char CFG_KEYFILE[]
Aws::String cafile
Path to the Root CA for the endpoint.
IotRoleConfig config_
Configuration for connecting to IoT.
Auth configuration needed to retrieve AWS credentials via the IoT service.
Aws::String name
Thing name for the device.
#define FIELD_CREDENTIALS
Name of the credentials object in the json response from IoT.
#define FIELD_ACCESS_KEY
Name of the field containing the access key id in the json response from IoT.
static const long DEFAULT_AUTH_CONNECT_TIMEOUT_MS
Default number of milliseconds to wait before timing out when connecting to retrieve credentials from...
Aws::String certfile
Path to the certificate which identifies the device.
#define FIELD_EXPIRATION
Name of the field containing the expiration date in the json response from IoT.
bool IsTimeExpired()
Returns true if the credentials have expired.
Aws::String role
Name of the AWS IoT Role Alias for the device.
#define FIELD_SECRET_KEY
Name of the field containing the secret key in the json response from IoT.
Contains common Error handling functionality for AWS ROS libraries.
static bool AppendHeader(struct curl_slist **headers, const char *name, const char *value)
Appends a name/value pair to a list of curl headers The libcurl API potentially returns new list poin...
static bool ParseConfigVale(std::string &value, Aws::String &result)
static const char CFG_ENDPOINT[]
std::atomic< double > expiry_
Future epoch when the cached credentials will expire.
Aws::Utils::Json::JsonValue GetValue()
Create a Json::JsonValue object from the current value of the buffer.
bool ValidateResponse(Aws::Utils::Json::JsonValue &value)
Validates the json response from the AWS IoT service.
Aws::Auth::AWSCredentials cached_
Current cached credentials.
long total_timeout_ms
Total number of ms to wait for the entire connect/request/response transaction.
bool GetServiceAuthConfig(ServiceAuthConfig &config, const std::shared_ptr< Aws::Client::ParameterReaderInterface > &parameters)
Retrieves service authorization data from a ParameterReaderInterface and populates the ServiceAuthCon...
Aws::String keyfile
Path to the related private key for the certificate.
static bool GetConfigValue(std::map< std::string, std::string > &data, const char *name, T &result, bool optional=false)
Helper to retrieve a specific config value from a map Simple helper to try to retrieve data from a ma...
Aws::StringStream stream_
Current data that has be received so far.
static const double EXPIRATION_GRACE_BUFFER
Go ahead and try to refresh credentials 30s before expiration.
Auth configuration for ROS AWS service integration.


aws_common
Author(s): AWS RoboMaker
autogenerated on Sat Mar 6 2021 03:11:38