bes Updated for version 3.20.13
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
54namespace 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
301 const std::string compute_awsv4_signature(
302 std::shared_ptr<http::url> &uri,
303 const std::time_t &request_date,
304 const std::string &public_key,
305 const std::string &secret_key,
306 const std::string &region,
307 const std::string &service) {
308
309
310 // canonical_uri is the path component of the URL. Later we will need the host.
311 const auto canonical_uri = uri->path(); // AWSV4::canonicalize_uri(uri);
312 // The query string is null for our code.
313 const auto canonical_query = uri->query(); // AWSV4::canonicalize_query(uri);
314
315 // We can eliminate one call to sha256 if the payload is null, which
316 // is the case for a GET request. jhrg 11/25/19
317 const std::string sha256_empty_payload = {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
318 // All AWS V4 signature require x-amz-content-sha256. jhrg 11/24/19
319
320 // We used to do it like the code in the other half of this #if
321 // But it seems we don't need the x-amz-content-sha256 header for empty payload
322 // so here it is without.
323 //
324 // NOTE: Changing this will break the awsv4_test using tests. jhrg 1/3/20
325 std::vector<std::string> headers{"host: ", "x-amz-date: "};
326 headers[0].append(uri->host()); // headers[0].append(uri.getHost());
327 headers[1].append(ISO8601_date(request_date));
328
329 const auto canonical_headers_map = canonicalize_headers(headers);
330 if (canonical_headers_map.empty()) {
331 throw std::runtime_error("Empty header list while building AWS V4 request signature");
332 }
333 const auto headers_string = map_headers_string(canonical_headers_map);
334 const auto signed_headers = map_signed_headers(canonical_headers_map);
335 const auto canonical_request = canonicalize_request(AWSV4::GET,
336 canonical_uri,
337 canonical_query,
338 headers_string,
339 signed_headers,
340 sha256_empty_payload);
341
342 BESDEBUG(CREDS, prolog << "Canonical Request: " << canonical_request << std::endl );
343
344 auto hashed_canonical_request = sha256_base16(canonical_request);
345 auto credential_scope = AWSV4::credential_scope(request_date,region,service);
346 auto string_to_sign = AWSV4::string_to_sign(STRING_TO_SIGN_ALGO,
347 request_date,
348 credential_scope,
349 hashed_canonical_request);
350
351 BESDEBUG(CREDS, prolog << "String to Sign: " << string_to_sign << std::endl );
352
353 auto signature = calculate_signature(request_date,
354 secret_key,
355 region,
356 service,
357 string_to_sign);
358
359 BESDEBUG(CREDS, prolog << "signature: " << signature << std::endl );
360
361 const std::string authorization_header = STRING_TO_SIGN_ALGO + " Credential=" + public_key + "/"
362 + credential_scope + ", SignedHeaders=" + signed_headers + ", Signature=" + signature;
363
364 BESDEBUG(CREDS, prolog << "authorization_header: " << authorization_header << std::endl );
365
366 return authorization_header;
367 }
368
369
370
371}
exception thrown if internal error encountered