]> git.proxmox.com Git - ceph.git/blob - ceph/src/rgw/rgw_auth_s3.cc
e46bf32681b8be292f9924bdaffe0e6443524777
[ceph.git] / ceph / src / rgw / rgw_auth_s3.cc
1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab ft=cpp
3
4 #include <algorithm>
5 #include <map>
6 #include <iterator>
7 #include <string>
8 #include <string_view>
9 #include <vector>
10
11 #include "common/armor.h"
12 #include "common/utf8.h"
13 #include "rgw_rest_s3.h"
14 #include "rgw_auth_s3.h"
15 #include "rgw_common.h"
16 #include "rgw_client_io.h"
17 #include "rgw_rest.h"
18 #include "rgw_crypt_sanitize.h"
19
20 #include <boost/container/small_vector.hpp>
21 #include <boost/algorithm/string.hpp>
22 #include <boost/algorithm/string/trim_all.hpp>
23
24 #define dout_context g_ceph_context
25 #define dout_subsys ceph_subsys_rgw
26
27 using namespace std;
28
29 static const auto signed_subresources = {
30 "acl",
31 "cors",
32 "delete",
33 "encryption",
34 "lifecycle",
35 "location",
36 "logging",
37 "notification",
38 "partNumber",
39 "policy",
40 "policyStatus",
41 "publicAccessBlock",
42 "requestPayment",
43 "response-cache-control",
44 "response-content-disposition",
45 "response-content-encoding",
46 "response-content-language",
47 "response-content-type",
48 "response-expires",
49 "tagging",
50 "torrent",
51 "uploadId",
52 "uploads",
53 "versionId",
54 "versioning",
55 "versions",
56 "website",
57 "object-lock"
58 };
59
60 /*
61 * ?get the canonical amazon-style header for something?
62 */
63
64 static std::string
65 get_canon_amz_hdr(const meta_map_t& meta_map)
66 {
67 std::string dest;
68
69 for (const auto& kv : meta_map) {
70 dest.append(kv.first);
71 dest.append(":");
72 dest.append(kv.second);
73 dest.append("\n");
74 }
75
76 return dest;
77 }
78
79 /*
80 * ?get the canonical representation of the object's location
81 */
82 static std::string
83 get_canon_resource(const DoutPrefixProvider *dpp, const char* const request_uri,
84 const std::map<std::string, std::string>& sub_resources)
85 {
86 std::string dest;
87
88 if (request_uri) {
89 dest.append(request_uri);
90 }
91
92 bool initial = true;
93 for (const auto& subresource : signed_subresources) {
94 const auto iter = sub_resources.find(subresource);
95 if (iter == std::end(sub_resources)) {
96 continue;
97 }
98
99 if (initial) {
100 dest.append("?");
101 initial = false;
102 } else {
103 dest.append("&");
104 }
105
106 dest.append(iter->first);
107 if (! iter->second.empty()) {
108 dest.append("=");
109 dest.append(iter->second);
110 }
111 }
112
113 ldpp_dout(dpp, 10) << "get_canon_resource(): dest=" << dest << dendl;
114 return dest;
115 }
116
117 /*
118 * get the header authentication information required to
119 * compute a request's signature
120 */
121 void rgw_create_s3_canonical_header(
122 const DoutPrefixProvider *dpp,
123 const char* const method,
124 const char* const content_md5,
125 const char* const content_type,
126 const char* const date,
127 const meta_map_t& meta_map,
128 const meta_map_t& qs_map,
129 const char* const request_uri,
130 const std::map<std::string, std::string>& sub_resources,
131 std::string& dest_str)
132 {
133 std::string dest;
134
135 if (method) {
136 dest = method;
137 }
138 dest.append("\n");
139
140 if (content_md5) {
141 dest.append(content_md5);
142 }
143 dest.append("\n");
144
145 if (content_type) {
146 dest.append(content_type);
147 }
148 dest.append("\n");
149
150 if (date) {
151 dest.append(date);
152 }
153 dest.append("\n");
154
155 dest.append(get_canon_amz_hdr(meta_map));
156 dest.append(get_canon_amz_hdr(qs_map));
157 dest.append(get_canon_resource(dpp, request_uri, sub_resources));
158
159 dest_str = dest;
160 }
161
162 static inline bool is_base64_for_content_md5(unsigned char c) {
163 return (isalnum(c) || isspace(c) || (c == '+') || (c == '/') || (c == '='));
164 }
165
166 static inline void get_v2_qs_map(const req_info& info,
167 meta_map_t& qs_map) {
168 const auto& params = const_cast<RGWHTTPArgs&>(info.args).get_params();
169 for (const auto& elt : params) {
170 std::string k = boost::algorithm::to_lower_copy(elt.first);
171 if (k.find("x-amz-meta-") == /* offset */ 0) {
172 rgw_add_amz_meta_header(qs_map, k, elt.second);
173 }
174 if (k == "x-amz-security-token") {
175 qs_map[k] = elt.second;
176 }
177 }
178 }
179
180 /*
181 * get the header authentication information required to
182 * compute a request's signature
183 */
184 bool rgw_create_s3_canonical_header(const DoutPrefixProvider *dpp,
185 const req_info& info,
186 utime_t* const header_time,
187 std::string& dest,
188 const bool qsr)
189 {
190 const char* const content_md5 = info.env->get("HTTP_CONTENT_MD5");
191 if (content_md5) {
192 for (const char *p = content_md5; *p; p++) {
193 if (!is_base64_for_content_md5(*p)) {
194 ldpp_dout(dpp, 0) << "NOTICE: bad content-md5 provided (not base64),"
195 << " aborting request p=" << *p << " " << (int)*p << dendl;
196 return false;
197 }
198 }
199 }
200
201 const char *content_type = info.env->get("CONTENT_TYPE");
202
203 std::string date;
204 meta_map_t qs_map;
205
206 if (qsr) {
207 get_v2_qs_map(info, qs_map); // handle qs metadata
208 date = info.args.get("Expires");
209 } else {
210 const char *str = info.env->get("HTTP_X_AMZ_DATE");
211 const char *req_date = str;
212 if (str == NULL) {
213 req_date = info.env->get("HTTP_DATE");
214 if (!req_date) {
215 ldpp_dout(dpp, 0) << "NOTICE: missing date for auth header" << dendl;
216 return false;
217 }
218 date = req_date;
219 }
220
221 if (header_time) {
222 struct tm t;
223 if (!parse_rfc2616(req_date, &t)) {
224 ldpp_dout(dpp, 0) << "NOTICE: failed to parse date for auth header" << dendl;
225 return false;
226 }
227 if (t.tm_year < 70) {
228 ldpp_dout(dpp, 0) << "NOTICE: bad date (predates epoch): " << req_date << dendl;
229 return false;
230 }
231 *header_time = utime_t(internal_timegm(&t), 0);
232 *header_time -= t.tm_gmtoff;
233 }
234 }
235
236 const auto& meta_map = info.x_meta_map;
237 const auto& sub_resources = info.args.get_sub_resources();
238
239 std::string request_uri;
240 if (info.effective_uri.empty()) {
241 request_uri = info.request_uri;
242 } else {
243 request_uri = info.effective_uri;
244 }
245
246 rgw_create_s3_canonical_header(dpp, info.method, content_md5, content_type,
247 date.c_str(), meta_map, qs_map,
248 request_uri.c_str(), sub_resources, dest);
249 return true;
250 }
251
252
253 namespace rgw::auth::s3 {
254
255 bool is_time_skew_ok(time_t t)
256 {
257 auto req_tp = ceph::coarse_real_clock::from_time_t(t);
258 auto cur_tp = ceph::coarse_real_clock::now();
259
260 if (std::chrono::abs(cur_tp - req_tp) > RGW_AUTH_GRACE) {
261 dout(10) << "NOTICE: request time skew too big." << dendl;
262 using ceph::operator<<;
263 dout(10) << "req_tp=" << req_tp << ", cur_tp=" << cur_tp << dendl;
264 return false;
265 }
266
267 return true;
268 }
269
270 static inline int parse_v4_query_string(const req_info& info, /* in */
271 std::string_view& credential, /* out */
272 std::string_view& signedheaders, /* out */
273 std::string_view& signature, /* out */
274 std::string_view& date, /* out */
275 std::string_view& sessiontoken) /* out */
276 {
277 /* auth ships with req params ... */
278
279 /* look for required params */
280 credential = info.args.get("x-amz-credential");
281 if (credential.size() == 0) {
282 return -EPERM;
283 }
284
285 date = info.args.get("x-amz-date");
286 struct tm date_t;
287 if (!parse_iso8601(sview2cstr(date).data(), &date_t, nullptr, false)) {
288 return -EPERM;
289 }
290
291 std::string_view expires = info.args.get("x-amz-expires");
292 if (expires.empty()) {
293 return -EPERM;
294 }
295 /* X-Amz-Expires provides the time period, in seconds, for which
296 the generated presigned URL is valid. The minimum value
297 you can set is 1, and the maximum is 604800 (seven days) */
298 time_t exp = atoll(expires.data());
299 if ((exp < 1) || (exp > 7*24*60*60)) {
300 dout(10) << "NOTICE: exp out of range, exp = " << exp << dendl;
301 return -EPERM;
302 }
303 /* handle expiration in epoch time */
304 uint64_t req_sec = (uint64_t)internal_timegm(&date_t);
305 uint64_t now = ceph_clock_now();
306 if (now >= req_sec + exp) {
307 dout(10) << "NOTICE: now = " << now << ", req_sec = " << req_sec << ", exp = " << exp << dendl;
308 return -EPERM;
309 }
310
311 signedheaders = info.args.get("x-amz-signedheaders");
312 if (signedheaders.size() == 0) {
313 return -EPERM;
314 }
315
316 signature = info.args.get("x-amz-signature");
317 if (signature.size() == 0) {
318 return -EPERM;
319 }
320
321 if (info.args.exists("x-amz-security-token")) {
322 sessiontoken = info.args.get("x-amz-security-token");
323 if (sessiontoken.size() == 0) {
324 return -EPERM;
325 }
326 }
327
328 return 0;
329 }
330
331 static bool get_next_token(const std::string_view& s,
332 size_t& pos,
333 const char* const delims,
334 std::string_view& token)
335 {
336 const size_t start = s.find_first_not_of(delims, pos);
337 if (start == std::string_view::npos) {
338 pos = s.size();
339 return false;
340 }
341
342 size_t end = s.find_first_of(delims, start);
343 if (end != std::string_view::npos)
344 pos = end + 1;
345 else {
346 pos = end = s.size();
347 }
348
349 token = s.substr(start, end - start);
350 return true;
351 }
352
353 template<std::size_t ExpectedStrNum>
354 boost::container::small_vector<std::string_view, ExpectedStrNum>
355 get_str_vec(const std::string_view& str, const char* const delims)
356 {
357 boost::container::small_vector<std::string_view, ExpectedStrNum> str_vec;
358
359 size_t pos = 0;
360 std::string_view token;
361 while (pos < str.size()) {
362 if (get_next_token(str, pos, delims, token)) {
363 if (token.size() > 0) {
364 str_vec.push_back(token);
365 }
366 }
367 }
368
369 return str_vec;
370 }
371
372 template<std::size_t ExpectedStrNum>
373 boost::container::small_vector<std::string_view, ExpectedStrNum>
374 get_str_vec(const std::string_view& str)
375 {
376 const char delims[] = ";,= \t";
377 return get_str_vec<ExpectedStrNum>(str, delims);
378 }
379
380 static inline int parse_v4_auth_header(const req_info& info, /* in */
381 std::string_view& credential, /* out */
382 std::string_view& signedheaders, /* out */
383 std::string_view& signature, /* out */
384 std::string_view& date, /* out */
385 std::string_view& sessiontoken, /* out */
386 const DoutPrefixProvider *dpp)
387 {
388 std::string_view input(info.env->get("HTTP_AUTHORIZATION", ""));
389 try {
390 input = input.substr(::strlen(AWS4_HMAC_SHA256_STR) + 1);
391 } catch (std::out_of_range&) {
392 /* We should never ever run into this situation as the presence of
393 * AWS4_HMAC_SHA256_STR had been verified earlier. */
394 ldpp_dout(dpp, 10) << "credentials string is too short" << dendl;
395 return -EINVAL;
396 }
397
398 std::map<std::string_view, std::string_view> kv;
399 for (const auto& s : get_str_vec<4>(input, ",")) {
400 const auto parsed_pair = parse_key_value(s);
401 if (parsed_pair) {
402 kv[parsed_pair->first] = parsed_pair->second;
403 } else {
404 ldpp_dout(dpp, 10) << "NOTICE: failed to parse auth header (s=" << s << ")"
405 << dendl;
406 return -EINVAL;
407 }
408 }
409
410 static const std::array<std::string_view, 3> required_keys = {
411 "Credential",
412 "SignedHeaders",
413 "Signature"
414 };
415
416 /* Ensure that the presigned required keys are really there. */
417 for (const auto& k : required_keys) {
418 if (kv.find(k) == std::end(kv)) {
419 ldpp_dout(dpp, 10) << "NOTICE: auth header missing key: " << k << dendl;
420 return -EINVAL;
421 }
422 }
423
424 credential = kv["Credential"];
425 signedheaders = kv["SignedHeaders"];
426 signature = kv["Signature"];
427
428 /* sig hex str */
429 ldpp_dout(dpp, 10) << "v4 signature format = " << signature << dendl;
430
431 /* ------------------------- handle x-amz-date header */
432
433 /* grab date */
434
435 const char *d = info.env->get("HTTP_X_AMZ_DATE");
436 struct tm t;
437 if (!parse_iso8601(d, &t, NULL, false)) {
438 ldpp_dout(dpp, 10) << "error reading date via http_x_amz_date" << dendl;
439 return -EACCES;
440 }
441 date = d;
442
443 if (!is_time_skew_ok(internal_timegm(&t))) {
444 return -ERR_REQUEST_TIME_SKEWED;
445 }
446
447 if (info.env->exists("HTTP_X_AMZ_SECURITY_TOKEN")) {
448 sessiontoken = info.env->get("HTTP_X_AMZ_SECURITY_TOKEN");
449 }
450
451 return 0;
452 }
453
454 bool is_non_s3_op(RGWOpType op_type)
455 {
456 if (op_type == RGW_STS_GET_SESSION_TOKEN ||
457 op_type == RGW_STS_ASSUME_ROLE ||
458 op_type == RGW_STS_ASSUME_ROLE_WEB_IDENTITY ||
459 op_type == RGW_OP_CREATE_ROLE ||
460 op_type == RGW_OP_DELETE_ROLE ||
461 op_type == RGW_OP_GET_ROLE ||
462 op_type == RGW_OP_MODIFY_ROLE ||
463 op_type == RGW_OP_LIST_ROLES ||
464 op_type == RGW_OP_PUT_ROLE_POLICY ||
465 op_type == RGW_OP_GET_ROLE_POLICY ||
466 op_type == RGW_OP_LIST_ROLE_POLICIES ||
467 op_type == RGW_OP_DELETE_ROLE_POLICY ||
468 op_type == RGW_OP_PUT_USER_POLICY ||
469 op_type == RGW_OP_GET_USER_POLICY ||
470 op_type == RGW_OP_LIST_USER_POLICIES ||
471 op_type == RGW_OP_DELETE_USER_POLICY ||
472 op_type == RGW_OP_CREATE_OIDC_PROVIDER ||
473 op_type == RGW_OP_DELETE_OIDC_PROVIDER ||
474 op_type == RGW_OP_GET_OIDC_PROVIDER ||
475 op_type == RGW_OP_LIST_OIDC_PROVIDERS ||
476 op_type == RGW_OP_PUBSUB_TOPIC_CREATE ||
477 op_type == RGW_OP_PUBSUB_TOPICS_LIST ||
478 op_type == RGW_OP_PUBSUB_TOPIC_GET ||
479 op_type == RGW_OP_PUBSUB_TOPIC_DELETE ||
480 op_type == RGW_OP_TAG_ROLE ||
481 op_type == RGW_OP_LIST_ROLE_TAGS ||
482 op_type == RGW_OP_UNTAG_ROLE) {
483 return true;
484 }
485 return false;
486 }
487
488 int parse_v4_credentials(const req_info& info, /* in */
489 std::string_view& access_key_id, /* out */
490 std::string_view& credential_scope, /* out */
491 std::string_view& signedheaders, /* out */
492 std::string_view& signature, /* out */
493 std::string_view& date, /* out */
494 std::string_view& session_token, /* out */
495 const bool using_qs, /* in */
496 const DoutPrefixProvider *dpp)
497 {
498 std::string_view credential;
499 int ret;
500 if (using_qs) {
501 ret = parse_v4_query_string(info, credential, signedheaders,
502 signature, date, session_token);
503 } else {
504 ret = parse_v4_auth_header(info, credential, signedheaders,
505 signature, date, session_token, dpp);
506 }
507
508 if (ret < 0) {
509 return ret;
510 }
511
512 /* access_key/YYYYMMDD/region/service/aws4_request */
513 ldpp_dout(dpp, 10) << "v4 credential format = " << credential << dendl;
514
515 if (std::count(credential.begin(), credential.end(), '/') != 4) {
516 return -EINVAL;
517 }
518
519 /* credential must end with 'aws4_request' */
520 if (credential.find("aws4_request") == std::string::npos) {
521 return -EINVAL;
522 }
523
524 /* grab access key id */
525 const size_t pos = credential.find("/");
526 access_key_id = credential.substr(0, pos);
527 ldpp_dout(dpp, 10) << "access key id = " << access_key_id << dendl;
528
529 /* grab credential scope */
530 credential_scope = credential.substr(pos + 1);
531 ldpp_dout(dpp, 10) << "credential scope = " << credential_scope << dendl;
532
533 return 0;
534 }
535
536 string gen_v4_scope(const ceph::real_time& timestamp,
537 const string& region,
538 const string& service)
539 {
540
541 auto sec = real_clock::to_time_t(timestamp);
542
543 struct tm bt;
544 gmtime_r(&sec, &bt);
545
546 auto year = 1900 + bt.tm_year;
547 auto mon = bt.tm_mon + 1;
548 auto day = bt.tm_mday;
549
550 return fmt::format(FMT_STRING("{:d}{:02d}{:02d}/{:s}/{:s}/aws4_request"),
551 year, mon, day, region, service);
552 }
553
554 std::string get_v4_canonical_qs(const req_info& info, const bool using_qs)
555 {
556 const std::string *params = &info.request_params;
557 std::string copy_params;
558 if (params->empty()) {
559 /* Optimize the typical flow. */
560 return std::string();
561 }
562 if (params->find_first_of('+') != std::string::npos) {
563 copy_params = *params;
564 boost::replace_all(copy_params, "+", "%20");
565 params = &copy_params;
566 }
567
568 /* Handle case when query string exists. Step 3 described in: http://docs.
569 * aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */
570 std::map<std::string, std::string> canonical_qs_map;
571 for (const auto& s : get_str_vec<5>(*params, "&")) {
572 std::string_view key, val;
573 const auto parsed_pair = parse_key_value(s);
574 if (parsed_pair) {
575 std::tie(key, val) = *parsed_pair;
576 } else {
577 /* Handling a parameter without any value (even the empty one). That's
578 * it, we've encountered something like "this_param&other_param=val"
579 * which is used by S3 for subresources. */
580 key = s;
581 }
582
583 if (using_qs && boost::iequals(key, "X-Amz-Signature")) {
584 /* Preserving the original behaviour of get_v4_canonical_qs() here. */
585 continue;
586 }
587
588 // while awsv4 specs ask for all slashes to be encoded, s3 itself is relaxed
589 // in its implementation allowing non-url-encoded slashes to be present in
590 // presigned urls for instance
591 canonical_qs_map[aws4_uri_recode(key, true)] = aws4_uri_recode(val, true);
592 }
593
594 /* Thanks to the early exist we have the guarantee that canonical_qs_map has
595 * at least one element. */
596 auto iter = std::begin(canonical_qs_map);
597 std::string canonical_qs;
598 canonical_qs.append(iter->first)
599 .append("=", ::strlen("="))
600 .append(iter->second);
601
602 for (iter++; iter != std::end(canonical_qs_map); iter++) {
603 canonical_qs.append("&", ::strlen("&"))
604 .append(iter->first)
605 .append("=", ::strlen("="))
606 .append(iter->second);
607 }
608
609 return canonical_qs;
610 }
611
612 static void add_v4_canonical_params_from_map(const map<string, string>& m,
613 std::map<string, string> *result)
614 {
615 for (auto& entry : m) {
616 const auto& key = entry.first;
617 if (key.empty()) {
618 continue;
619 }
620
621 (*result)[aws4_uri_recode(key, true)] = aws4_uri_recode(entry.second, true);
622 }
623 }
624
625 std::string gen_v4_canonical_qs(const req_info& info)
626 {
627 std::map<std::string, std::string> canonical_qs_map;
628
629 add_v4_canonical_params_from_map(info.args.get_params(), &canonical_qs_map);
630 add_v4_canonical_params_from_map(info.args.get_sys_params(), &canonical_qs_map);
631
632 if (canonical_qs_map.empty()) {
633 return string();
634 }
635
636 /* Thanks to the early exit we have the guarantee that canonical_qs_map has
637 * at least one element. */
638 auto iter = std::begin(canonical_qs_map);
639 std::string canonical_qs;
640 canonical_qs.append(iter->first)
641 .append("=", ::strlen("="))
642 .append(iter->second);
643
644 for (iter++; iter != std::end(canonical_qs_map); iter++) {
645 canonical_qs.append("&", ::strlen("&"))
646 .append(iter->first)
647 .append("=", ::strlen("="))
648 .append(iter->second);
649 }
650
651 return canonical_qs;
652 }
653
654 boost::optional<std::string>
655 get_v4_canonical_headers(const req_info& info,
656 const std::string_view& signedheaders,
657 const bool using_qs,
658 const bool force_boto2_compat)
659 {
660 std::map<std::string_view, std::string> canonical_hdrs_map;
661 for (const auto& token : get_str_vec<5>(signedheaders, ";")) {
662 /* TODO(rzarzynski): we'd like to switch to sstring here but it should
663 * get push_back() and reserve() first. */
664 std::string token_env = "HTTP_";
665 token_env.reserve(token.length() + std::strlen("HTTP_") + 1);
666
667 std::transform(std::begin(token), std::end(token),
668 std::back_inserter(token_env), [](const int c) {
669 return c == '-' ? '_' : c == '_' ? '-' : std::toupper(c);
670 });
671
672 if (token_env == "HTTP_CONTENT_LENGTH") {
673 token_env = "CONTENT_LENGTH";
674 } else if (token_env == "HTTP_CONTENT_TYPE") {
675 token_env = "CONTENT_TYPE";
676 }
677 const char* const t = info.env->get(token_env.c_str());
678 if (!t) {
679 dout(10) << "warning env var not available " << token_env.c_str() << dendl;
680 continue;
681 }
682
683 std::string token_value(t);
684 if (token_env == "HTTP_CONTENT_MD5" &&
685 !std::all_of(std::begin(token_value), std::end(token_value),
686 is_base64_for_content_md5)) {
687 dout(0) << "NOTICE: bad content-md5 provided (not base64)"
688 << ", aborting request" << dendl;
689 return boost::none;
690 }
691
692 if (force_boto2_compat && using_qs && token == "host") {
693 std::string_view port = info.env->get("SERVER_PORT", "");
694 std::string_view secure_port = info.env->get("SERVER_PORT_SECURE", "");
695
696 if (!secure_port.empty()) {
697 if (secure_port != "443")
698 token_value.append(":", std::strlen(":"))
699 .append(secure_port.data(), secure_port.length());
700 } else if (!port.empty()) {
701 if (port != "80")
702 token_value.append(":", std::strlen(":"))
703 .append(port.data(), port.length());
704 }
705 }
706
707 canonical_hdrs_map[token] = rgw_trim_whitespace(token_value);
708 }
709
710 std::string canonical_hdrs;
711 for (const auto& header : canonical_hdrs_map) {
712 const std::string_view& name = header.first;
713 std::string value = header.second;
714 boost::trim_all<std::string>(value);
715
716 canonical_hdrs.append(name.data(), name.length())
717 .append(":", std::strlen(":"))
718 .append(value)
719 .append("\n", std::strlen("\n"));
720 }
721 return canonical_hdrs;
722 }
723
724 static void handle_header(const string& header, const string& val,
725 std::map<std::string, std::string> *canonical_hdrs_map)
726 {
727 /* TODO(rzarzynski): we'd like to switch to sstring here but it should
728 * get push_back() and reserve() first. */
729
730 std::string token;
731 token.reserve(header.length());
732
733 if (header == "HTTP_CONTENT_LENGTH") {
734 token = "content-length";
735 } else if (header == "HTTP_CONTENT_TYPE") {
736 token = "content-type";
737 } else {
738 auto start = std::begin(header);
739 if (boost::algorithm::starts_with(header, "HTTP_")) {
740 start += 5; /* len("HTTP_") */
741 }
742
743 std::transform(start, std::end(header),
744 std::back_inserter(token), [](const int c) {
745 return c == '_' ? '-' : std::tolower(c);
746 });
747 }
748
749 (*canonical_hdrs_map)[token] = rgw_trim_whitespace(val);
750 }
751
752 std::string gen_v4_canonical_headers(const req_info& info,
753 const map<string, string>& extra_headers,
754 string *signed_hdrs)
755 {
756 std::map<std::string, std::string> canonical_hdrs_map;
757 for (auto& entry : info.env->get_map()) {
758 handle_header(entry.first, entry.second, &canonical_hdrs_map);
759 }
760 for (auto& entry : extra_headers) {
761 handle_header(entry.first, entry.second, &canonical_hdrs_map);
762 }
763
764 std::string canonical_hdrs;
765 signed_hdrs->clear();
766 for (const auto& header : canonical_hdrs_map) {
767 const auto& name = header.first;
768 std::string value = header.second;
769 boost::trim_all<std::string>(value);
770
771 if (!signed_hdrs->empty()) {
772 signed_hdrs->append(";");
773 }
774 signed_hdrs->append(name);
775
776 canonical_hdrs.append(name.data(), name.length())
777 .append(":", std::strlen(":"))
778 .append(value)
779 .append("\n", std::strlen("\n"));
780 }
781
782 return canonical_hdrs;
783 }
784
785 /*
786 * create canonical request for signature version 4
787 *
788 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
789 */
790 sha256_digest_t
791 get_v4_canon_req_hash(CephContext* cct,
792 const std::string_view& http_verb,
793 const std::string& canonical_uri,
794 const std::string& canonical_qs,
795 const std::string& canonical_hdrs,
796 const std::string_view& signed_hdrs,
797 const std::string_view& request_payload_hash,
798 const DoutPrefixProvider *dpp)
799 {
800 ldpp_dout(dpp, 10) << "payload request hash = " << request_payload_hash << dendl;
801
802 const auto canonical_req = string_join_reserve("\n",
803 http_verb,
804 canonical_uri,
805 canonical_qs,
806 canonical_hdrs,
807 signed_hdrs,
808 request_payload_hash);
809
810 const auto canonical_req_hash = calc_hash_sha256(canonical_req);
811
812 using sanitize = rgw::crypt_sanitize::log_content;
813 ldpp_dout(dpp, 10) << "canonical request = " << sanitize{canonical_req} << dendl;
814 ldpp_dout(dpp, 10) << "canonical request hash = "
815 << canonical_req_hash << dendl;
816
817 return canonical_req_hash;
818 }
819
820 /*
821 * create string to sign for signature version 4
822 *
823 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
824 */
825 AWSEngine::VersionAbstractor::string_to_sign_t
826 get_v4_string_to_sign(CephContext* const cct,
827 const std::string_view& algorithm,
828 const std::string_view& request_date,
829 const std::string_view& credential_scope,
830 const sha256_digest_t& canonreq_hash,
831 const DoutPrefixProvider *dpp)
832 {
833 const auto hexed_cr_hash = canonreq_hash.to_str();
834 const std::string_view hexed_cr_hash_str(hexed_cr_hash);
835
836 const auto string_to_sign = string_join_reserve("\n",
837 algorithm,
838 request_date,
839 credential_scope,
840 hexed_cr_hash_str);
841
842 ldpp_dout(dpp, 10) << "string to sign = "
843 << rgw::crypt_sanitize::log_content{string_to_sign}
844 << dendl;
845
846 return string_to_sign;
847 }
848
849
850 static inline std::tuple<std::string_view, /* date */
851 std::string_view, /* region */
852 std::string_view> /* service */
853 parse_cred_scope(std::string_view credential_scope)
854 {
855 /* date cred */
856 size_t pos = credential_scope.find("/");
857 const auto date_cs = credential_scope.substr(0, pos);
858 credential_scope = credential_scope.substr(pos + 1);
859
860 /* region cred */
861 pos = credential_scope.find("/");
862 const auto region_cs = credential_scope.substr(0, pos);
863 credential_scope = credential_scope.substr(pos + 1);
864
865 /* service cred */
866 pos = credential_scope.find("/");
867 const auto service_cs = credential_scope.substr(0, pos);
868
869 return std::make_tuple(date_cs, region_cs, service_cs);
870 }
871
872 static inline std::vector<unsigned char>
873 transform_secret_key(const std::string_view& secret_access_key)
874 {
875 /* TODO(rzarzynski): switch to constexpr when C++14 becomes available. */
876 static const std::initializer_list<unsigned char> AWS4 { 'A', 'W', 'S', '4' };
877
878 /* boost::container::small_vector might be used here if someone wants to
879 * optimize out even more dynamic allocations. */
880 std::vector<unsigned char> secret_key_utf8;
881 secret_key_utf8.reserve(AWS4.size() + secret_access_key.size());
882 secret_key_utf8.assign(AWS4);
883
884 for (const auto c : secret_access_key) {
885 std::array<unsigned char, MAX_UTF8_SZ> buf;
886 const size_t n = encode_utf8(c, buf.data());
887 secret_key_utf8.insert(std::end(secret_key_utf8),
888 std::begin(buf), std::begin(buf) + n);
889 }
890
891 return secret_key_utf8;
892 }
893
894 /*
895 * calculate the SigningKey of AWS auth version 4
896 */
897 static sha256_digest_t
898 get_v4_signing_key(CephContext* const cct,
899 const std::string_view& credential_scope,
900 const std::string_view& secret_access_key,
901 const DoutPrefixProvider *dpp)
902 {
903 std::string_view date, region, service;
904 std::tie(date, region, service) = parse_cred_scope(credential_scope);
905
906 const auto utfed_sec_key = transform_secret_key(secret_access_key);
907 const auto date_k = calc_hmac_sha256(utfed_sec_key, date);
908 const auto region_k = calc_hmac_sha256(date_k, region);
909 const auto service_k = calc_hmac_sha256(region_k, service);
910
911 /* aws4_request */
912 const auto signing_key = calc_hmac_sha256(service_k,
913 std::string_view("aws4_request"));
914
915 ldpp_dout(dpp, 10) << "date_k = " << date_k << dendl;
916 ldpp_dout(dpp, 10) << "region_k = " << region_k << dendl;
917 ldpp_dout(dpp, 10) << "service_k = " << service_k << dendl;
918 ldpp_dout(dpp, 10) << "signing_k = " << signing_key << dendl;
919
920 return signing_key;
921 }
922
923 /*
924 * calculate the AWS signature version 4
925 *
926 * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
927 *
928 * srv_signature_t is an alias over Ceph's basic_sstring. We're using
929 * it to keep everything within the stack boundaries instead of doing
930 * dynamic allocations.
931 */
932 AWSEngine::VersionAbstractor::server_signature_t
933 get_v4_signature(const std::string_view& credential_scope,
934 CephContext* const cct,
935 const std::string_view& secret_key,
936 const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign,
937 const DoutPrefixProvider *dpp)
938 {
939 auto signing_key = get_v4_signing_key(cct, credential_scope, secret_key, dpp);
940
941 /* The server-side generated digest for comparison. */
942 const auto digest = calc_hmac_sha256(signing_key, string_to_sign);
943
944 /* TODO(rzarzynski): I would love to see our sstring having reserve() and
945 * the non-const data() variant like C++17's std::string. */
946 using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
947 srv_signature_t signature(srv_signature_t::initialized_later(),
948 digest.SIZE * 2);
949 buf_to_hex(digest.v, digest.SIZE, signature.begin());
950
951 ldpp_dout(dpp, 10) << "generated signature = " << signature << dendl;
952
953 return signature;
954 }
955
956 AWSEngine::VersionAbstractor::server_signature_t
957 get_v2_signature(CephContext* const cct,
958 const std::string& secret_key,
959 const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign)
960 {
961 if (secret_key.empty()) {
962 throw -EINVAL;
963 }
964
965 const auto digest = calc_hmac_sha1(secret_key, string_to_sign);
966
967 /* 64 is really enough */;
968 char buf[64];
969 const int ret = ceph_armor(std::begin(buf),
970 std::begin(buf) + 64,
971 reinterpret_cast<const char *>(digest.v),
972 reinterpret_cast<const char *>(digest.v + digest.SIZE));
973 if (ret < 0) {
974 ldout(cct, 10) << "ceph_armor failed" << dendl;
975 throw ret;
976 } else {
977 buf[ret] = '\0';
978 using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
979 return srv_signature_t(buf, ret);
980 }
981 }
982
983 bool AWSv4ComplMulti::ChunkMeta::is_new_chunk_in_stream(size_t stream_pos) const
984 {
985 return stream_pos >= (data_offset_in_stream + data_length);
986 }
987
988 size_t AWSv4ComplMulti::ChunkMeta::get_data_size(size_t stream_pos) const
989 {
990 if (stream_pos > (data_offset_in_stream + data_length)) {
991 /* Data in parsing_buf. */
992 return data_length;
993 } else {
994 return data_offset_in_stream + data_length - stream_pos;
995 }
996 }
997
998
999 /* AWSv4 completers begin. */
1000 std::pair<AWSv4ComplMulti::ChunkMeta, size_t /* consumed */>
1001 AWSv4ComplMulti::ChunkMeta::create_next(CephContext* const cct,
1002 ChunkMeta&& old,
1003 const char* const metabuf,
1004 const size_t metabuf_len)
1005 {
1006 std::string_view metastr(metabuf, metabuf_len);
1007
1008 const size_t semicolon_pos = metastr.find(";");
1009 if (semicolon_pos == std::string_view::npos) {
1010 ldout(cct, 20) << "AWSv4ComplMulti cannot find the ';' separator"
1011 << dendl;
1012 throw rgw::io::Exception(EINVAL, std::system_category());
1013 }
1014
1015 char* data_field_end;
1016 /* strtoull ignores the "\r\n" sequence after each non-first chunk. */
1017 const size_t data_length = std::strtoull(metabuf, &data_field_end, 16);
1018 if (data_length == 0 && data_field_end == metabuf) {
1019 ldout(cct, 20) << "AWSv4ComplMulti: cannot parse the data size"
1020 << dendl;
1021 throw rgw::io::Exception(EINVAL, std::system_category());
1022 }
1023
1024 /* Parse the chunk_signature=... part. */
1025 const auto signature_part = metastr.substr(semicolon_pos + 1);
1026 const size_t eq_sign_pos = signature_part.find("=");
1027 if (eq_sign_pos == std::string_view::npos) {
1028 ldout(cct, 20) << "AWSv4ComplMulti: cannot find the '=' separator"
1029 << dendl;
1030 throw rgw::io::Exception(EINVAL, std::system_category());
1031 }
1032
1033 /* OK, we have at least the beginning of a signature. */
1034 const size_t data_sep_pos = signature_part.find("\r\n");
1035 if (data_sep_pos == std::string_view::npos) {
1036 ldout(cct, 20) << "AWSv4ComplMulti: no new line at signature end"
1037 << dendl;
1038 throw rgw::io::Exception(EINVAL, std::system_category());
1039 }
1040
1041 const auto signature = \
1042 signature_part.substr(eq_sign_pos + 1, data_sep_pos - 1 - eq_sign_pos);
1043 if (signature.length() != SIG_SIZE) {
1044 ldout(cct, 20) << "AWSv4ComplMulti: signature.length() != 64"
1045 << dendl;
1046 throw rgw::io::Exception(EINVAL, std::system_category());
1047 }
1048
1049 const size_t data_starts_in_stream = \
1050 + semicolon_pos + strlen(";") + data_sep_pos + strlen("\r\n")
1051 + old.data_offset_in_stream + old.data_length;
1052
1053 ldout(cct, 20) << "parsed new chunk; signature=" << signature
1054 << ", data_length=" << data_length
1055 << ", data_starts_in_stream=" << data_starts_in_stream
1056 << dendl;
1057
1058 return std::make_pair(ChunkMeta(data_starts_in_stream,
1059 data_length,
1060 signature),
1061 semicolon_pos + 83);
1062 }
1063
1064 std::string
1065 AWSv4ComplMulti::calc_chunk_signature(const std::string& payload_hash) const
1066 {
1067 const auto string_to_sign = string_join_reserve("\n",
1068 AWS4_HMAC_SHA256_PAYLOAD_STR,
1069 date,
1070 credential_scope,
1071 prev_chunk_signature,
1072 AWS4_EMPTY_PAYLOAD_HASH,
1073 payload_hash);
1074
1075 ldout(cct, 20) << "AWSv4ComplMulti: string_to_sign=\n" << string_to_sign
1076 << dendl;
1077
1078 /* new chunk signature */
1079 const auto sig = calc_hmac_sha256(signing_key, string_to_sign);
1080 /* FIXME(rzarzynski): std::string here is really unnecessary. */
1081 return sig.to_str();
1082 }
1083
1084
1085 bool AWSv4ComplMulti::is_signature_mismatched()
1086 {
1087 /* The validity of previous chunk can be verified only after getting meta-
1088 * data of the next one. */
1089 const auto payload_hash = calc_hash_sha256_restart_stream(&sha256_hash);
1090 const auto calc_signature = calc_chunk_signature(payload_hash);
1091
1092 if (chunk_meta.get_signature() != calc_signature) {
1093 ldout(cct, 20) << "AWSv4ComplMulti: ERROR: chunk signature mismatch"
1094 << dendl;
1095 ldout(cct, 20) << "AWSv4ComplMulti: declared signature="
1096 << chunk_meta.get_signature() << dendl;
1097 ldout(cct, 20) << "AWSv4ComplMulti: calculated signature="
1098 << calc_signature << dendl;
1099
1100 return true;
1101 } else {
1102 prev_chunk_signature = chunk_meta.get_signature();
1103 return false;
1104 }
1105 }
1106
1107 size_t AWSv4ComplMulti::recv_body(char* const buf, const size_t buf_max)
1108 {
1109 /* Buffer stores only parsed stream. Raw values reflect the stream
1110 * we're getting from a client. */
1111 size_t buf_pos = 0;
1112
1113 if (chunk_meta.is_new_chunk_in_stream(stream_pos)) {
1114 /* Verify signature of the previous chunk. We aren't doing that for new
1115 * one as the procedure requires calculation of payload hash. This code
1116 * won't be triggered for the last, zero-length chunk. Instead, is will
1117 * be checked in the complete() method. */
1118 if (stream_pos >= ChunkMeta::META_MAX_SIZE && is_signature_mismatched()) {
1119 throw rgw::io::Exception(ERR_SIGNATURE_NO_MATCH, std::system_category());
1120 }
1121
1122 /* We don't have metadata for this range. This means a new chunk, so we
1123 * need to parse a fresh portion of the stream. Let's start. */
1124 size_t to_extract = parsing_buf.capacity() - parsing_buf.size();
1125 do {
1126 const size_t orig_size = parsing_buf.size();
1127 parsing_buf.resize(parsing_buf.size() + to_extract);
1128 const size_t received = io_base_t::recv_body(parsing_buf.data() + orig_size,
1129 to_extract);
1130 parsing_buf.resize(parsing_buf.size() - (to_extract - received));
1131 if (received == 0) {
1132 break;
1133 }
1134
1135 stream_pos += received;
1136 to_extract -= received;
1137 } while (to_extract > 0);
1138
1139 size_t consumed;
1140 std::tie(chunk_meta, consumed) = \
1141 ChunkMeta::create_next(cct, std::move(chunk_meta),
1142 parsing_buf.data(), parsing_buf.size());
1143
1144 /* We can drop the bytes consumed during metadata parsing. The remainder
1145 * can be chunk's data plus possibly beginning of next chunks' metadata. */
1146 parsing_buf.erase(std::begin(parsing_buf),
1147 std::begin(parsing_buf) + consumed);
1148 }
1149
1150 size_t stream_pos_was = stream_pos - parsing_buf.size();
1151
1152 size_t to_extract = \
1153 std::min(chunk_meta.get_data_size(stream_pos_was), buf_max);
1154 dout(30) << "AWSv4ComplMulti: stream_pos_was=" << stream_pos_was << ", to_extract=" << to_extract << dendl;
1155
1156 /* It's quite probable we have a couple of real data bytes stored together
1157 * with meta-data in the parsing_buf. We need to extract them and move to
1158 * the final buffer. This is a trade-off between frontend's read overhead
1159 * and memcpy. */
1160 if (to_extract > 0 && parsing_buf.size() > 0) {
1161 const auto data_len = std::min(to_extract, parsing_buf.size());
1162 const auto data_end_iter = std::begin(parsing_buf) + data_len;
1163 dout(30) << "AWSv4ComplMulti: to_extract=" << to_extract << ", data_len=" << data_len << dendl;
1164
1165 std::copy(std::begin(parsing_buf), data_end_iter, buf);
1166 parsing_buf.erase(std::begin(parsing_buf), data_end_iter);
1167
1168 calc_hash_sha256_update_stream(sha256_hash, buf, data_len);
1169
1170 to_extract -= data_len;
1171 buf_pos += data_len;
1172 }
1173
1174 /* Now we can do the bulk read directly from RestfulClient without any extra
1175 * buffering. */
1176 while (to_extract > 0) {
1177 const size_t received = io_base_t::recv_body(buf + buf_pos, to_extract);
1178 dout(30) << "AWSv4ComplMulti: to_extract=" << to_extract << ", received=" << received << dendl;
1179
1180 if (received == 0) {
1181 break;
1182 }
1183
1184 calc_hash_sha256_update_stream(sha256_hash, buf + buf_pos, received);
1185
1186 buf_pos += received;
1187 stream_pos += received;
1188 to_extract -= received;
1189 }
1190
1191 dout(20) << "AWSv4ComplMulti: filled=" << buf_pos << dendl;
1192 return buf_pos;
1193 }
1194
1195 void AWSv4ComplMulti::modify_request_state(const DoutPrefixProvider* dpp, req_state* const s_rw)
1196 {
1197 const char* const decoded_length = \
1198 s_rw->info.env->get("HTTP_X_AMZ_DECODED_CONTENT_LENGTH");
1199
1200 if (!decoded_length) {
1201 throw -EINVAL;
1202 } else {
1203 s_rw->length = decoded_length;
1204 s_rw->content_length = parse_content_length(decoded_length);
1205
1206 if (s_rw->content_length < 0) {
1207 ldpp_dout(dpp, 10) << "negative AWSv4's content length, aborting" << dendl;
1208 throw -EINVAL;
1209 }
1210 }
1211
1212 /* Install the filter over rgw::io::RestfulClient. */
1213 AWS_AUTHv4_IO(s_rw)->add_filter(
1214 std::static_pointer_cast<io_base_t>(shared_from_this()));
1215 }
1216
1217 bool AWSv4ComplMulti::complete()
1218 {
1219 /* Now it's time to verify the signature of the last, zero-length chunk. */
1220 if (is_signature_mismatched()) {
1221 ldout(cct, 10) << "ERROR: signature of last chunk does not match"
1222 << dendl;
1223 return false;
1224 } else {
1225 return true;
1226 }
1227 }
1228
1229 rgw::auth::Completer::cmplptr_t
1230 AWSv4ComplMulti::create(const req_state* const s,
1231 std::string_view date,
1232 std::string_view credential_scope,
1233 std::string_view seed_signature,
1234 const boost::optional<std::string>& secret_key)
1235 {
1236 if (!secret_key) {
1237 /* Some external authorizers (like Keystone) aren't fully compliant with
1238 * AWSv4. They do not provide the secret_key which is necessary to handle
1239 * the streamed upload. */
1240 throw -ERR_NOT_IMPLEMENTED;
1241 }
1242
1243 const auto signing_key = \
1244 rgw::auth::s3::get_v4_signing_key(s->cct, credential_scope, *secret_key, s);
1245
1246 return std::make_shared<AWSv4ComplMulti>(s,
1247 std::move(date),
1248 std::move(credential_scope),
1249 std::move(seed_signature),
1250 signing_key);
1251 }
1252
1253 size_t AWSv4ComplSingle::recv_body(char* const buf, const size_t max)
1254 {
1255 const auto received = io_base_t::recv_body(buf, max);
1256 calc_hash_sha256_update_stream(sha256_hash, buf, received);
1257
1258 return received;
1259 }
1260
1261 void AWSv4ComplSingle::modify_request_state(const DoutPrefixProvider* dpp, req_state* const s_rw)
1262 {
1263 /* Install the filter over rgw::io::RestfulClient. */
1264 AWS_AUTHv4_IO(s_rw)->add_filter(
1265 std::static_pointer_cast<io_base_t>(shared_from_this()));
1266 }
1267
1268 bool AWSv4ComplSingle::complete()
1269 {
1270 /* The completer is only for the cases where signed payload has been
1271 * requested. It won't be used, for instance, during the query string-based
1272 * authentication. */
1273 const auto payload_hash = calc_hash_sha256_close_stream(&sha256_hash);
1274
1275 /* Validate x-amz-sha256 */
1276 if (payload_hash.compare(expected_request_payload_hash) == 0) {
1277 return true;
1278 } else {
1279 ldout(cct, 10) << "ERROR: x-amz-content-sha256 does not match"
1280 << dendl;
1281 ldout(cct, 10) << "ERROR: grab_aws4_sha256_hash()="
1282 << payload_hash << dendl;
1283 ldout(cct, 10) << "ERROR: expected_request_payload_hash="
1284 << expected_request_payload_hash << dendl;
1285 return false;
1286 }
1287 }
1288
1289 AWSv4ComplSingle::AWSv4ComplSingle(const req_state* const s)
1290 : io_base_t(nullptr),
1291 cct(s->cct),
1292 expected_request_payload_hash(get_v4_exp_payload_hash(s->info)),
1293 sha256_hash(calc_hash_sha256_open_stream()) {
1294 }
1295
1296 rgw::auth::Completer::cmplptr_t
1297 AWSv4ComplSingle::create(const req_state* const s,
1298 const boost::optional<std::string>&)
1299 {
1300 return std::make_shared<AWSv4ComplSingle>(s);
1301 }
1302
1303 } // namespace rgw::auth::s3