bes  Updated for version 3.20.8
awsv4.cc
1 
2 
3 // -*- mode: c++; c-basic-offset:4 -*-
4 
5 // This file is part of the Hyrax data server.
6 
7 // This code is derived from https://github.com/bradclawsie/awsv4-cpp
8 // Copyright (c) 2013, brad clawsie
9 // All rights reserved.
10 // see the file AWSV4_LICENSE
11 
12 // Copyright (c) 2019 OPeNDAP, Inc.
13 // Modifications Author: James Gallagher <jgallagher@opendap.org>
14 //
15 // This library is free software; you can redistribute it and/or
16 // modify it under the terms of the GNU Lesser General Public
17 // License as published by the Free Software Foundation; either
18 // version 2.1 of the License, or (at your option) any later version.
19 //
20 // This library is distributed in the hope that it will be useful,
21 // but WITHOUT ANY WARRANTY; without even the implied warranty of
22 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23 // Lesser General Public License for more details.
24 //
25 // You should have received a copy of the GNU Lesser General Public
26 // License along with this library; if not, write to the Free Software
27 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
28 //
29 // You can contact OPeNDAP, Inc. at PO Box 112, Saunderstown, RI. 02874-0112.
30 
31 #include "config.h"
32 
33 #include "awsv4.h"
34 
35 #include <cstring>
36 
37 #include <stdexcept>
38 #include <algorithm>
39 #include <map>
40 #include <ctime>
41 #include <iostream>
42 #include <sstream>
43 
44 #include <openssl/sha.h>
45 #include <openssl/hmac.h>
46 
47 #include "url_impl.h"
48 #include "BESInternalError.h"
49 #include "BESDebug.h"
50 #include "DmrppNames.h"
51 
52 #define prolog std::string("AWSV4::").append(__func__).append("() - ")
53 
54 namespace AWSV4 {
55 
56  // used in sha256_base16() and hmac_to_string(). jhrg 1/5/20
57  const int SHA256_DIGEST_STRING_LENGTH = (SHA256_DIGEST_LENGTH << 1);
58 
65  std::string join(const std::vector<std::string> &ss, const std::string &delim) {
66  if (ss.size() == 0)
67  return "";
68 
69  std::stringstream sstream;
70  const size_t l = ss.size() - 1;
71  for (size_t i = 0; i < l; i++) {
72  sstream << ss[i] << delim;
73  }
74  sstream << ss.back();
75  return sstream.str();
76  }
77 
78  std::string sha256_base16(const std::string &str) {
79 
80  unsigned char hashOut[SHA256_DIGEST_LENGTH];
81  SHA256_CTX sha256;
82  SHA256_Init(&sha256);
83  SHA256_Update(&sha256, (const unsigned char *)str.c_str(), str.length());
84  SHA256_Final(hashOut, &sha256);
85 
86  char outputBuffer[SHA256_DIGEST_STRING_LENGTH + 1];
87  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
88  snprintf(outputBuffer + (i * 2), 3, "%02x", hashOut[i]);
89  }
90  outputBuffer[SHA256_DIGEST_STRING_LENGTH] = 0;
91  return std::string{outputBuffer};
92  }
93 
94  // From https://stackoverflow.com/questions/1798112/removing-leading-and-trailing-spaces-from-a-string
95  static std::string trim(const std::string& str, const std::string& whitespace = " \t") {
96  const auto strBegin = str.find_first_not_of(whitespace);
97  if (strBegin == std::string::npos)
98  return ""; // no content
99 
100  const auto strEnd = str.find_last_not_of(whitespace);
101  const auto strRange = strEnd - strBegin + 1;
102 
103  return str.substr(strBegin, strRange);
104  }
105 
106  // -----------------------------------------------------------------------------------
107  // TASK 1 - create a canonical request
108  // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
109 
110  // create a map of the "canonicalized" headers
111  // will return empty map on malformed input.
112  //
113  // headers A vector where each element is a header name and value, separated by a colon. No spaces.
114  std::map<std::string,std::string> canonicalize_headers(const std::vector<std::string>& headers) {
115  std::map<std::string,std::string> header_key2val;
116  for( auto h = headers.begin(), end = headers.end(); h != end; ++h ) {
117  // CentOS6 does not appear to support auth and the range-based for loop together.
118  // jhrg 1/5/20
119  // for (const auto & h: headers) {
120  // h is a header <key> : <val>
121 
122  auto i = h->find(':');
123  if (i == std::string::npos) {
124  header_key2val.clear();
125  return header_key2val;
126  }
127 
128  std::string key = trim(h->substr(0, i));
129  const std::string val = trim(h->substr(i+1));
130  if (key.empty() || val.empty()) {
131  header_key2val.clear();
132  return header_key2val;
133  }
134 
135  std::transform(key.begin(), key.end(), key.begin(),::tolower);
136  header_key2val[key] = val;
137  }
138  return header_key2val;
139  }
140 
141  // get a string representation of header:value lines
142  const std::string map_headers_string(const std::map<std::string,std::string>& header_key2val) {
143  const std::string pair_delim{":"};
144  std::string h;
145  for (auto kv = header_key2val.begin(), end = header_key2val.end(); kv != end; ++kv) {
146  h.append(kv->first + pair_delim + kv->second + ENDL);
147  }
148  return h;
149  }
150 
151  // get a string representation of the header names
152  const std::string map_signed_headers(const std::map<std::string,std::string>& header_key2val) {
153  const std::string signed_headers_delim{";"};
154  std::vector<std::string> ks;
155  // CentOS6 compat hack "for (const auto& kv:header_key2val) {"
156  for (auto kv = header_key2val.begin(), end = header_key2val.end(); kv != end; ++kv) {
157  ks.push_back(kv->first);
158  }
159  return join(ks,signed_headers_delim);
160  }
161 
162  const std::string canonicalize_request(const std::string& http_request_method,
163  const std::string& canonical_uri,
164  const std::string& canonical_query_string,
165  const std::string& canonical_headers,
166  const std::string& signed_headers,
167  const std::string& shar256_of_payload) {
168  return http_request_method + ENDL +
169  canonical_uri + ENDL +
170  canonical_query_string + ENDL +
171  canonical_headers + ENDL +
172  signed_headers + ENDL +
173  shar256_of_payload;
174  }
175 
176  // -----------------------------------------------------------------------------------
177  // TASK 2 - create a string-to-sign
178  // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
179 
180  const std::string string_to_sign(const std::string& algorithm,
181  const std::time_t& request_date,
182  const std::string& credential_scope,
183  const std::string& hashed_canonical_request) {
184  return algorithm + ENDL +
185  ISO8601_date(request_date) + ENDL +
186  credential_scope + ENDL +
187  hashed_canonical_request;
188  }
189 
190  const std::string credential_scope(const std::time_t& request_date,
191  const std::string region,
192  const std::string service) {
193  const std::string s{"/"};
194  return utc_yyyymmdd(request_date) + s + region + s + service + s + AWS4_REQUEST;
195  }
196 
197  // time_t -> 20131222T043039Z
198  const std::string ISO8601_date(const std::time_t& t) {
199  char buf[sizeof "20111008T070709Z"];
200  std::strftime(buf, sizeof buf, "%Y%m%dT%H%M%SZ", std::gmtime(&t));
201  return std::string{buf};
202  }
203 
204  // time_t -> 20131222
205  const std::string utc_yyyymmdd(const std::time_t& t) {
206  char buf[sizeof "20111008"];
207  std::strftime(buf, sizeof buf, "%Y%m%d", std::gmtime(&t));
208  return std::string{buf};
209  }
210 
211  // HMAC --> string. jhrg 11/25/19
212  const std::string hmac_to_string(const unsigned char *hmac) {
213  // Added to print the kSigning value to check against AWS example. jhrg 11/24/19
214  char buf[SHA256_DIGEST_STRING_LENGTH + 1];
215  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
216  // size is 3 for each call (2 chars plus null). jhrg 1/3/20
217  snprintf(buf + (i * 2), 3, "%02x", hmac[i]);
218  }
219  buf[SHA256_DIGEST_STRING_LENGTH] = 0;
220  return std::string{buf};
221  }
222 
223  // -----------------------------------------------------------------------------------
224  // TASK 3
225  // http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
226 
227  /*
228  * unsigned char *HMAC(const EVP_MD *evp_md,
229  * const void *key, int key_len,
230  * const unsigned char *d, int n,
231  * unsigned char *md, unsigned int *md_len);
232  * where md must be EVP_MAX_MD_SIZE in size
233  */
234 
235  const std::string calculate_signature(const std::time_t& request_date,
236  const std::string secret,
237  const std::string region,
238  const std::string service,
239  const std::string string_to_sign) {
240 
241  // These are used/re-used for the various signatures. jhrg 1/3/20
242  unsigned char md[EVP_MAX_MD_SIZE+1];
243  unsigned int md_len;
244 
245  const std::string k1 = AWS4 + secret;
246  const std::string yyyymmdd = utc_yyyymmdd(request_date);
247  unsigned char* kDate = HMAC(EVP_sha256(), (const void *)k1.c_str(), k1.length(),
248  (const unsigned char *)yyyymmdd.c_str(), yyyymmdd.length(), md, &md_len);
249  if (!kDate)
250  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
251 
252  md[md_len] = '\0';
253  BESDEBUG(CREDS, prolog << "kDate: " << hmac_to_string(kDate) << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl );
254 
255  unsigned char *kRegion = HMAC(EVP_sha256(), md, (size_t)md_len,
256  (const unsigned char*)region.c_str(), region.length(), md, &md_len);
257  if (!kRegion)
258  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
259 
260  md[md_len] = '\0';
261  BESDEBUG(CREDS, prolog << "kRegion: " << hmac_to_string(kRegion) << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl );
262 
263  unsigned char *kService = HMAC(EVP_sha256(), md, (size_t)md_len,
264  (const unsigned char*)service.c_str(), service.length(), md, &md_len);
265  if (!kService)
266  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
267 
268  md[md_len] = '\0';
269  BESDEBUG(CREDS, prolog << "kService: " << hmac_to_string(kService) << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl );
270 
271  unsigned char *kSigning = HMAC(EVP_sha256(), md, (size_t)md_len,
272  (const unsigned char*)AWS4_REQUEST.c_str(), AWS4_REQUEST.length(), md, &md_len);
273  if (!kSigning)
274  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
275 
276  md[md_len] = '\0';
277  BESDEBUG(CREDS, prolog << "kSigning: " << hmac_to_string(kRegion) << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl );
278 
279  unsigned char *kSig = HMAC(EVP_sha256(), md, (size_t)md_len,
280  (const unsigned char*)string_to_sign.c_str(), string_to_sign.length(), md, &md_len);
281  if (!kSig)
282  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
283 
284  md[md_len] = '\0';
285  auto sig = hmac_to_string(md);
286  BESDEBUG(CREDS, prolog << "kSig: " << sig << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl );
287  return sig;
288  }
289 
290 
302  const std::string compute_awsv4_signature(
303  const std::string &uri_str,
304  const std::time_t &request_date,
305  const std::string &public_key,
306  const std::string &secret_key,
307  const std::string &region,
308  const std::string &service) {
309 
310  http::url uri(uri_str);
311 
312  // canonical_uri is the path component of the URL. Later we will need the host.
313  const auto canonical_uri = uri.path(); // AWSV4::canonicalize_uri(uri);
314  // The query string is null for our code.
315  const auto canonical_query = uri.query(); // AWSV4::canonicalize_query(uri);
316 
317  // We can eliminate one call to sha256 if the payload is null, which
318  // is the case for a GET request. jhrg 11/25/19
319  const std::string sha256_empty_payload = {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
320  // All AWS V4 signature require x-amz-content-sha256. jhrg 11/24/19
321 
322  // We used to do it like the code in the other half of this #if
323  // But it seems we don't need the x-amz-content-sha256 header for empty payload
324  // so here it is without.
325  //
326  // NOTE: Changing this will break the awsv4_test using tests. jhrg 1/3/20
327  std::vector<std::string> headers{"host: ", "x-amz-date: "};
328  headers[0].append(uri.host()); // headers[0].append(uri.getHost());
329  headers[1].append(ISO8601_date(request_date));
330 
331  const auto canonical_headers_map = canonicalize_headers(headers);
332  if (canonical_headers_map.empty()) {
333  throw std::runtime_error("Empty header list while building AWS V4 request signature");
334  }
335  const auto headers_string = map_headers_string(canonical_headers_map);
336  const auto signed_headers = map_signed_headers(canonical_headers_map);
337  const auto canonical_request = canonicalize_request(AWSV4::GET,
338  canonical_uri,
339  canonical_query,
340  headers_string,
341  signed_headers,
342  sha256_empty_payload);
343 
344  BESDEBUG(CREDS, prolog << "Canonical Request: " << canonical_request << std::endl );
345 
346  auto hashed_canonical_request = sha256_base16(canonical_request);
347  auto credential_scope = AWSV4::credential_scope(request_date,region,service);
348  auto string_to_sign = AWSV4::string_to_sign(STRING_TO_SIGN_ALGO,
349  request_date,
350  credential_scope,
351  hashed_canonical_request);
352 
353  BESDEBUG(CREDS, prolog << "String to Sign: " << string_to_sign << std::endl );
354 
355  auto signature = calculate_signature(request_date,
356  secret_key,
357  region,
358  service,
359  string_to_sign);
360 
361  BESDEBUG(CREDS, prolog << "signature: " << signature << std::endl );
362 
363  const std::string authorization_header = STRING_TO_SIGN_ALGO + " Credential=" + public_key + "/"
364  + credential_scope + ", SignedHeaders=" + signed_headers + ", Signature=" + signature;
365 
366  BESDEBUG(CREDS, prolog << "authorization_header: " << authorization_header << std::endl );
367 
368  return authorization_header;
369  }
370 }
exception thrown if internal error encountered