bes Updated for version 3.21.1
The Backend Server (BES) is the lower two tiers of the Hyrax data server
awsv4_master.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 "HttpNames.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
57const int SHA256_DIGEST_STRING_LENGTH = (SHA256_DIGEST_LENGTH << 1);
58
65std::string join(const std::vector<std::string> &ss, const std::string &delim) {
66 if (ss.empty())
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
78std::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.size());
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
95static 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.
114std::map<std::string, std::string> canonicalize_headers(const std::vector<std::string> &headers) {
115 std::map<std::string, std::string> header_key2val;
116 for (const auto &header: headers) {
117 auto i = header.find(':');
118 if (i == std::string::npos) {
119 header_key2val.clear();
120 return header_key2val;
121 }
122
123 std::string key = trim(header.substr(0, i));
124 const std::string val = trim(header.substr(i + 1));
125 if (key.empty() || val.empty()) {
126 header_key2val.clear();
127 return header_key2val;
128 }
129
130 std::transform(key.begin(), key.end(), key.begin(), ::tolower);
131 header_key2val[key] = val;
132 }
133 return header_key2val;
134}
135
136// get a string representation of header:value lines
137std::string map_headers_string(const std::map<std::string, std::string> &header_key2val) {
138 const std::string pair_delim{":"};
139 std::string h;
140 for (const auto &kv: header_key2val) {
141 h.append(kv.first).append(pair_delim).append(kv.second).append(ENDL);
142 }
143 return h;
144}
145
146// get a string representation of the header names
147std::string map_signed_headers(const std::map<std::string, std::string> &header_key2val) {
148 const std::string signed_headers_delim{";"};
149 std::vector<std::string> ks;
150 for ( const auto &kv: header_key2val) {
151 ks.push_back(kv.first);
152 }
153 return join(ks, signed_headers_delim);
154}
155
156std::string canonicalize_request(const std::string &http_request_method,
157 const std::string &canonical_uri,
158 const std::string &canonical_query_string,
159 const std::string &canonical_headers,
160 const std::string &signed_headers,
161 const std::string &shar256_of_payload) {
162 return std::string(http_request_method).append(ENDL).append(canonical_uri).append(ENDL)
163 .append(canonical_query_string).append(ENDL).append(canonical_headers).append(ENDL)
164 .append(signed_headers).append(ENDL).append(shar256_of_payload);
165}
166
167// -----------------------------------------------------------------------------------
168// TASK 2 - create a string-to-sign
169// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
170
171std::string string_to_sign(const std::string &algorithm,
172 const std::time_t &request_date,
173 const std::string &credential_scope,
174 const std::string &hashed_canonical_request) {
175 return algorithm + ENDL +
176 ISO8601_date(request_date) + ENDL +
177 credential_scope + ENDL +
178 hashed_canonical_request;
179}
180
181std::string credential_scope(const std::time_t &request_date,
182 const std::string &region,
183 const std::string &service) {
184 const std::string s{"/"};
185 return utc_yyyymmdd(request_date).append(s).append(region).append(s).append(service).append(s).append(AWS4_REQUEST);
186}
187
188// time_t -> 20131222T043039Z
189std::string ISO8601_date(const std::time_t &t) {
190 char buf[sizeof "20111008T070709Z"];
191 struct tm tm_buf{};
192 std::strftime(buf, sizeof buf, "%Y%m%dT%H%M%SZ", gmtime_r(&t, &tm_buf));
193 return buf;
194}
195
196// time_t -> 20131222
197std::string utc_yyyymmdd(const std::time_t &t) {
198 char buf[sizeof "20111008"];
199 struct tm tm_buf{};
200 std::strftime(buf, sizeof buf, "%Y%m%d", gmtime_r(&t, &tm_buf));
201 return buf;
202}
203
204// HMAC --> string. jhrg 11/25/19
205std::string hmac_to_string(const unsigned char *hmac) {
206 // Added to print the kSigning value to check against AWS example. jhrg 11/24/19
207 char buf[SHA256_DIGEST_STRING_LENGTH + 1];
208 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
209 // size is 3 for each call (2 chars plus null). jhrg 1/3/20
210 snprintf(buf + (i * 2), 3, "%02x", hmac[i]);
211 }
212 buf[SHA256_DIGEST_STRING_LENGTH] = 0;
213 return buf;
214}
215
216// -----------------------------------------------------------------------------------
217// TASK 3
218// http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
219
220/*
221 * unsigned char *HMAC(const EVP_MD *evp_md,
222 * const void *key, int key_len,
223 * const unsigned char *d, int n,
224 * unsigned char *md, unsigned int *md_len);
225 * where md must be EVP_MAX_MD_SIZE in size
226 */
227
228std::string calculate_signature(const std::time_t &request_date,
229 const std::string &secret,
230 const std::string &region,
231 const std::string &service,
232 const std::string &string_to_sign) {
233
234 // These are used/re-used for the various signatures. jhrg 1/3/20
235 unsigned char md[EVP_MAX_MD_SIZE + 1];
236 unsigned int md_len;
237
238 const std::string k1 = AWS4 + secret;
239 const std::string yyyymmdd = utc_yyyymmdd(request_date);
240 // NB: The third argument for HMAC is an unsigned int. jhrg 10/31/22
241 const unsigned char *kDate = HMAC(EVP_sha256(), (const void *)k1.c_str(), (unsigned int)k1.size(),
242 (const unsigned char *) yyyymmdd.c_str(), yyyymmdd.size(), md, &md_len);
243 if (!kDate)
244 throw BESInternalError("Could not compute AWS V4 request signature.", __FILE__, __LINE__);
245
246 md[md_len] = '\0';
247 BESDEBUG(HTTP_MODULE,
248 prolog << "kDate: " << hmac_to_string(kDate) << " md_len: " << md_len << " md: " << hmac_to_string(md)
249 << std::endl);
250
251 const unsigned char *kRegion = HMAC(EVP_sha256(), md, md_len,
252 (const unsigned char *) region.c_str(), region.size(), md, &md_len);
253 if (!kRegion)
254 throw BESInternalError("Could not compute AWS V4 request signature.", __FILE__, __LINE__);
255
256 md[md_len] = '\0';
257 BESDEBUG(HTTP_MODULE,
258 prolog << "kRegion: " << hmac_to_string(kRegion) << " md_len: " << md_len << " md: " << hmac_to_string(md)
259 << std::endl);
260
261 const unsigned char *kService = HMAC(EVP_sha256(), md, md_len,
262 (const unsigned char *) service.c_str(), service.size(), md, &md_len);
263 if (!kService)
264 throw BESInternalError("Could not compute AWS V4 request signature.", __FILE__, __LINE__);
265
266 md[md_len] = '\0';
267 BESDEBUG(HTTP_MODULE, prolog << "kService: " << hmac_to_string(kService) << " md_len: " << md_len << " md: "
268 << hmac_to_string(md) << std::endl);
269
270 const unsigned char *kSigning = HMAC(EVP_sha256(), md, md_len,
271 (const unsigned char *) AWS4_REQUEST.c_str(), AWS4_REQUEST.size(), md, &md_len);
272 if (!kSigning)
273 throw BESInternalError("Could not compute AWS V4 request signature.", __FILE__, __LINE__);
274
275 md[md_len] = '\0';
276 BESDEBUG(HTTP_MODULE,
277 prolog << "kSigning: " << hmac_to_string(kRegion) << " md_len: " << md_len << " md: " << hmac_to_string(md)
278 << std::endl);
279
280 const unsigned char *kSig = HMAC(EVP_sha256(), md, md_len,
281 (const unsigned char *) string_to_sign.c_str(), string_to_sign.size(), md, &md_len);
282 if (!kSig)
283 throw BESInternalError("Could not compute AWS V4 request signature.", __FILE__, __LINE__);
284
285 md[md_len] = '\0';
286 auto sig = hmac_to_string(md);
287 BESDEBUG(HTTP_MODULE, prolog << "kSig: " << sig << " md_len: " << md_len << " md: " << hmac_to_string(md) << std::endl);
288 return sig;
289}
290
305std::string compute_awsv4_signature(const std::string &canonical_uri, const std::string &canonical_query,
306 const std::string &host, const std::time_t &request_date,
307 const std::string &public_key, const std::string &secret_key,
308 const std::string &region, const std::string &service) {
309 // We can eliminate one call to sha256 if the payload is null, which
310 // is the case for a GET request. jhrg 11/25/19
311 const std::string sha256_empty_payload = {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
312 // All AWS V4 signature require x-amz-content-sha256. jhrg 11/24/19
313
314 // We used to do it like the code in the other half of this #if
315 // But it seems we don't need the x-amz-content-sha256 header for empty payload
316 // so here it is without.
317 //
318 // NOTE: Changing this will break the awsv4_test using tests. jhrg 1/3/20
319 std::vector<std::string> headers{"host:", "x-amz-date:"};
320 headers[0].append(host);
321 headers[1].append(ISO8601_date(request_date));
322
323 const auto canonical_headers_map = canonicalize_headers(headers);
324 if (canonical_headers_map.empty()) {
325 throw std::runtime_error("Empty header list while building AWS V4 request signature");
326 }
327 const auto headers_string = map_headers_string(canonical_headers_map);
328 const auto signed_headers = map_signed_headers(canonical_headers_map);
329 const auto canonical_request = canonicalize_request(AWSV4::GET,
330 canonical_uri,
331 canonical_query,
332 headers_string,
333 signed_headers,
334 sha256_empty_payload);
335
336 BESDEBUG(HTTP_MODULE, prolog << "Canonical Request: " << canonical_request << std::endl);
337
338 auto hashed_canonical_request = sha256_base16(canonical_request);
339 auto credential_scope = AWSV4::credential_scope(request_date, region, service);
340 auto string_to_sign = AWSV4::string_to_sign(STRING_TO_SIGN_ALGO,
341 request_date,
342 credential_scope,
343 hashed_canonical_request);
344
345 BESDEBUG(HTTP_MODULE, prolog << "String to Sign: " << string_to_sign << std::endl);
346
347 auto signature = calculate_signature(request_date,
348 secret_key,
349 region,
350 service,
351 string_to_sign);
352
353 BESDEBUG(HTTP_MODULE, prolog << "signature: " << signature << std::endl);
354
355 std::string authorization_header = STRING_TO_SIGN_ALGO + " Credential=" + public_key + "/"
356 + credential_scope + ", SignedHeaders=" + signed_headers + ", Signature=" +
357 signature;
358
359 BESDEBUG(HTTP_MODULE, prolog << "authorization_header: " << authorization_header << std::endl);
360
361 return authorization_header;
362}
363
376std::string compute_awsv4_signature(
377 const http::url &uri,
378 const std::time_t &request_date,
379 const std::string &public_key,
380 const std::string &secret_key,
381 const std::string &region,
382 const std::string &service) {
383 return compute_awsv4_signature(uri.path(), uri.query(), uri.host(), request_date, public_key, secret_key,
384 region, service);
385}
386
387}