bes  Updated for version 3.20.6
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 "awsv4.h"
32 
33 #include <cstring>
34 
35 #include <stdexcept>
36 #include <algorithm>
37 #include <map>
38 #include <ctime>
39 #include <iostream>
40 #include <sstream>
41 
42 #if 0
43 #include <regex>
44 #endif
45 
46 #include <openssl/sha.h>
47 #include <openssl/hmac.h>
48 
49 #include "BESInternalError.h"
50 
51 #include "url_parser.h"
52 
53 namespace AWSV4 {
54 
55  // used in sha256_base16() and hmac_to_string(). jhrg 1/5/20
56  const int SHA256_DIGEST_STRING_LENGTH = (SHA256_DIGEST_LENGTH << 1);
57 
58  std::string join(const std::vector<std::string>& ss, const std::string delim) {
59  std::stringstream sstream;
60  const auto l = ss.size() - 1;
61  std::vector<int>::size_type i;
62  for (i = 0; i < l; i++) {
63  sstream << ss.at(i) << delim;
64  }
65  sstream << ss.back();
66  return sstream.str();
67  }
68 
69 #if 0
70  // http://stackoverflow.com/questions/2262386/generate-sha256-with-openssl-and-c
71  void sha256(const std::string str, unsigned char outputBuffer[SHA256_DIGEST_LENGTH]) {
72  char *c_string = new char [str.length()+1];
73  std::strcpy(c_string, str.c_str());
74 
75  SHA256_CTX sha256;
76  SHA256_Init(&sha256);
77  SHA256_Update(&sha256, c_string, strlen(c_string));
78  unsigned char hash[SHA256_DIGEST_LENGTH];
79  SHA256_Final(hash, &sha256);
80 
81  for (int i=0;i<SHA256_DIGEST_LENGTH;i++) {
82  outputBuffer[i] = hash[i];
83  }
84  }
85 #endif
86 
87  std::string sha256_base16(const std::string str) {
88 
89  unsigned char hashOut[SHA256_DIGEST_LENGTH];
90 #if 1
91  SHA256_CTX sha256;
92  SHA256_Init(&sha256);
93  SHA256_Update(&sha256, (const unsigned char *)str.c_str(), str.length());
94  SHA256_Final(hashOut, &sha256);
95 #else
96  AWSV4::sha256(str,hashOut);
97 #endif
98 
99  char outputBuffer[SHA256_DIGEST_STRING_LENGTH + 1];
100  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
101  snprintf(outputBuffer + (i * 2), 3, "%02x", hashOut[i]);
102  }
103  outputBuffer[SHA256_DIGEST_STRING_LENGTH] = 0;
104  return std::string{outputBuffer};
105  }
106 
107  // From https://stackoverflow.com/questions/1798112/removing-leading-and-trailing-spaces-from-a-string
108  static std::string trim(const std::string& str, const std::string& whitespace = " \t")
109  {
110  const auto strBegin = str.find_first_not_of(whitespace);
111  if (strBegin == std::string::npos)
112  return ""; // no content
113 
114  const auto strEnd = str.find_last_not_of(whitespace);
115  const auto strRange = strEnd - strBegin + 1;
116 
117  return str.substr(strBegin, strRange);
118  }
119 
120  // -----------------------------------------------------------------------------------
121  // TASK 1 - create a canonical request
122  // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
123 
124  // create a map of the "canonicalized" headers
125  // will return empty map on malformed input.
126  //
127  // headers A vector where each element is a header name and value, separated by a colon. No spaces.
128  std::map<std::string,std::string> canonicalize_headers(const std::vector<std::string>& headers) {
129  std::map<std::string,std::string> header_key2val;
130  for( auto h = headers.begin(), end = headers.end(); h != end; ++h ) {
131  // CentOS6 does not appear to support auth and the range-based for loop together.
132  // jhrg 1/5/20
133  // for (const auto & h: headers) {
134  // h is a header <key> : <val>
135 
136  auto i = h->find(':');
137  if (i == std::string::npos) {
138  header_key2val.clear();
139  return header_key2val;
140  }
141 
142  std::string key = trim(h->substr(0, i));
143  const std::string val = trim(h->substr(i+1));
144  if (key.empty() || val.empty()) {
145  header_key2val.clear();
146  return header_key2val;
147  }
148 
149  std::transform(key.begin(), key.end(), key.begin(),::tolower);
150  header_key2val[key] = val;
151  }
152  return header_key2val;
153  }
154 
155  // get a string representation of header:value lines
156  const std::string map_headers_string(const std::map<std::string,std::string>& header_key2val) {
157  const std::string pair_delim{":"};
158  std::string h;
159  for (auto kv = header_key2val.begin(), end = header_key2val.end(); kv != end; ++kv) {
160  h.append(kv->first + pair_delim + kv->second + ENDL);
161  }
162  return h;
163  }
164 
165  // get a string representation of the header names
166  const std::string map_signed_headers(const std::map<std::string,std::string>& header_key2val) {
167  const std::string signed_headers_delim{";"};
168  std::vector<std::string> ks;
169  // CentOS6 compat hack "for (const auto& kv:header_key2val) {"
170  for (auto kv = header_key2val.begin(), end = header_key2val.end(); kv != end; ++kv) {
171  ks.push_back(kv->first);
172  }
173  return join(ks,signed_headers_delim);
174  }
175 
176  const std::string canonicalize_request(const std::string& http_request_method,
177  const std::string& canonical_uri,
178  const std::string& canonical_query_string,
179  const std::string& canonical_headers,
180  const std::string& signed_headers,
181  const std::string& shar256_of_payload) {
182  return http_request_method + ENDL +
183  canonical_uri + ENDL +
184  canonical_query_string + ENDL +
185  canonical_headers + ENDL +
186  signed_headers + ENDL +
187  shar256_of_payload;
188  }
189 
190  // -----------------------------------------------------------------------------------
191  // TASK 2 - create a string-to-sign
192  // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
193 
194  const std::string string_to_sign(const std::string& algorithm,
195  const std::time_t& request_date,
196  const std::string& credential_scope,
197  const std::string& hashed_canonical_request) {
198  return algorithm + ENDL +
199  ISO8601_date(request_date) + ENDL +
200  credential_scope + ENDL +
201  hashed_canonical_request;
202  }
203 
204  const std::string credential_scope(const std::time_t& request_date,
205  const std::string region,
206  const std::string service) {
207  const std::string s{"/"};
208  return utc_yyyymmdd(request_date) + s + region + s + service + s + AWS4_REQUEST;
209  }
210 
211  // time_t -> 20131222T043039Z
212  const std::string ISO8601_date(const std::time_t& t) {
213  char buf[sizeof "20111008T070709Z"];
214  std::strftime(buf, sizeof buf, "%Y%m%dT%H%M%SZ", std::gmtime(&t));
215  return std::string{buf};
216  }
217 
218  // time_t -> 20131222
219  const std::string utc_yyyymmdd(const std::time_t& t) {
220  char buf[sizeof "20111008"];
221  std::strftime(buf, sizeof buf, "%Y%m%d", std::gmtime(&t));
222  return std::string{buf};
223  }
224 
225  // HMAC --> string. jhrg 11/25/19
226  const std::string hmac_to_string(const unsigned char *hmac) {
227  // Added to print the kSigning value to check against AWS example. jhrg 11/24/19
228  char buf[SHA256_DIGEST_STRING_LENGTH + 1];
229  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
230  // size is 3 for each call (2 chars plus null). jhrg 1/3/20
231  snprintf(buf + (i * 2), 3, "%02x", hmac[i]);
232  }
233  buf[SHA256_DIGEST_STRING_LENGTH] = 0;
234  return std::string{buf};
235  }
236 
237  // -----------------------------------------------------------------------------------
238  // TASK 3
239  // http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
240 
241  /*
242  * unsigned char *HMAC(const EVP_MD *evp_md,
243  * const void *key, int key_len,
244  * const unsigned char *d, int n,
245  * unsigned char *md, unsigned int *md_len);
246  * where md must be EVP_MAX_MD_SIZE in size
247  */
248 
249  const std::string calculate_signature(const std::time_t& request_date,
250  const std::string secret,
251  const std::string region,
252  const std::string service,
253  const std::string string_to_sign,
254  const bool verbose) {
255 
256  // These are used/re-used for the various signatures. jhrg 1/3/20
257  unsigned char md[EVP_MAX_MD_SIZE+1];
258  unsigned int md_len;
259 
260  const std::string k1 = AWS4 + secret;
261  const std::string yyyymmdd = utc_yyyymmdd(request_date);
262  unsigned char* kDate = HMAC(EVP_sha256(), (const void *)k1.c_str(), k1.length(),
263  (const unsigned char *)yyyymmdd.c_str(), yyyymmdd.length(), md, &md_len);
264  if (!kDate)
265  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
266 
267  if (verbose) {
268  std::cerr << "kDate: " << hmac_to_string(kDate) << std::endl;
269  std::cerr << "md_len: " << md_len << std::endl;
270  md[md_len] = '\0';
271  std::cerr << "md: " << hmac_to_string(md) << std::endl;
272  }
273 
274  unsigned char *kRegion = HMAC(EVP_sha256(), md, (size_t)md_len,
275  (const unsigned char*)region.c_str(), region.length(), md, &md_len);
276  if (!kRegion)
277  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
278 
279  if (verbose) {
280  std::cerr << "kRegion: " << hmac_to_string(kRegion) << std::endl;
281  std::cerr << "md_len: " << md_len << std::endl;
282  md[md_len] = '\0';
283  std::cerr << "md: " << hmac_to_string(md) << std::endl;
284  }
285 
286  unsigned char *kService = HMAC(EVP_sha256(), md, (size_t)md_len,
287  (const unsigned char*)service.c_str(), service.length(), md, &md_len);
288  if (!kService)
289  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
290 
291  if (verbose) {
292  std::cerr << "kService: " << hmac_to_string(kService) << std::endl;
293  std::cerr << "md_len: " << md_len << std::endl;
294  md[md_len] = '\0';
295  std::cerr << "md: " << hmac_to_string(md) << std::endl;
296  }
297 
298  unsigned char *kSigning = HMAC(EVP_sha256(), md, (size_t)md_len,
299  (const unsigned char*)AWS4_REQUEST.c_str(), AWS4_REQUEST.length(), md, &md_len);
300  if (!kSigning)
301  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
302 
303  if (verbose) {
304  std::cerr << "kSigning " << hmac_to_string(kSigning) << std::endl;
305  std::cerr << "md_len: " << md_len << std::endl;
306  md[md_len] = '\0';
307  std::cerr << "md: " << hmac_to_string(md) << std::endl;
308  }
309 
310  unsigned char *kSig = HMAC(EVP_sha256(), md, (size_t)md_len,
311  (const unsigned char*)string_to_sign.c_str(), string_to_sign.length(), md, &md_len);
312  if (!kSig)
313  throw BESInternalError("Could not compute AWS V4 requst signature." ,__FILE__, __LINE__);
314 
315  md[md_len] = '\0';
316  auto sig = hmac_to_string(md);
317  return sig;
318  }
319 
320 
333  const std::string compute_awsv4_signature(
334  const std::string &uri_str,
335  const std::time_t &request_date,
336  const std::string &public_key,
337  const std::string &secret_key,
338  const std::string &region,
339  const std::string &service,
340  const bool &verbose) {
341 
342  url_parser uri(uri_str);
343 
344  // canonical_uri is the path component of the URL. Later we will need the host.
345  const auto canonical_uri = uri.path(); // AWSV4::canonicalize_uri(uri);
346  // The query string is null for our code.
347  const auto canonical_query = uri.query(); // AWSV4::canonicalize_query(uri);
348 
349  // We can eliminate one call to sha256 if the payload is null, which
350  // is the case for a GET request. jhrg 11/25/19
351  const std::string sha256_empty_payload = {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
352  // All AWS V4 signature require x-amz-content-sha256. jhrg 11/24/19
353 
354  // We used to do it like the code in the other half of this #if
355  // But it seems we don't need the x-amz-content-sha256 header for empty payload
356  // so here it is without.
357  //
358  // NOTE: Changing this will break the awsv4_test using tests. jhrg 1/3/20
359  std::vector<std::string> headers{"host: ", "x-amz-date: "};
360  headers[0].append(uri.host()); // headers[0].append(uri.getHost());
361  headers[1].append(ISO8601_date(request_date));
362 
363  const auto canonical_headers_map = canonicalize_headers(headers);
364  if (canonical_headers_map.empty()) {
365  throw std::runtime_error("Empty header list while building AWS V4 request signature");
366  }
367  const auto headers_string = map_headers_string(canonical_headers_map);
368  const auto signed_headers = map_signed_headers(canonical_headers_map);
369  const auto canonical_request = canonicalize_request(AWSV4::GET,
370  canonical_uri,
371  canonical_query,
372  headers_string,
373  signed_headers,
374  sha256_empty_payload);
375 
376  if (verbose)
377  std::cerr << "-- Canonical Request\n" << canonical_request << "\n--\n" << std::endl;
378 
379  auto hashed_canonical_request = sha256_base16(canonical_request);
380  auto credential_scope = AWSV4::credential_scope(request_date,region,service);
381  auto string_to_sign = AWSV4::string_to_sign(STRING_TO_SIGN_ALGO,
382  request_date,
383  credential_scope,
384  hashed_canonical_request);
385 
386  if (verbose)
387  std::cerr << "-- String to Sign\n" << string_to_sign << "\n----\n" << std::endl;
388 
389  auto signature = calculate_signature(request_date,
390  secret_key,
391  region,
392  service,
393  string_to_sign,
394  verbose);
395  if (verbose)
396  std::cerr << "-- signature\n" << signature << "\n----\n" << std::endl;
397 
398  const std::string authorization_header = STRING_TO_SIGN_ALGO + " Credential=" + public_key + "/"
399  + credential_scope + ", SignedHeaders=" + signed_headers + ", Signature=" + signature;
400 
401  if (verbose)
402  std::cerr << "-- authorization_header\n" << authorization_header << "\n----\n" << std::endl;
403 
404  return authorization_header;
405  }
406 }
BESInternalError
exception thrown if internal error encountered
Definition: BESInternalError.h:43