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