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