]> git.proxmox.com Git - ceph.git/blob - ceph/src/rgw/rgw_auth_s3.cc
update sources to v12.1.2
[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 if (info.request_params.empty()) {
505 /* Optimize the typical flow. */
506 return std::string();
507 }
508
509 /* Handle case when query string exists. Step 3 described in: http://docs.
510 * aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */
511 std::map<std::string, std::string> canonical_qs_map;
512 for (const auto& s : get_str_vec<5>(info.request_params, "&")) {
513 boost::string_view key, val;
514 const auto parsed_pair = parse_key_value(s);
515 if (parsed_pair) {
516 std::tie(key, val) = *parsed_pair;
517 } else {
518 /* Handling a parameter without any value (even the empty one). That's
519 * it, we've encountered something like "this_param&other_param=val"
520 * which is used by S3 for subresources. */
521 key = s;
522 }
523
524 if (using_qs && key == "X-Amz-Signature") {
525 /* Preserving the original behaviour of get_v4_canonical_qs() here. */
526 continue;
527 }
528
529 if (key == "X-Amz-Credential") {
530 /* FIXME(rzarzynski): I can't find any comment in the previously linked
531 * Amazon's docs saying that X-Amz-Credential should be handled in this
532 * way. */
533 canonical_qs_map[key.to_string()] = val.to_string();
534 } else {
535 canonical_qs_map[aws4_uri_recode(key)] = aws4_uri_recode(val);
536 }
537 }
538
539 /* Thanks to the early exist we have the guarantee that canonical_qs_map has
540 * at least one element. */
541 auto iter = std::begin(canonical_qs_map);
542 std::string canonical_qs;
543 canonical_qs.append(iter->first)
544 .append("=", ::strlen("="))
545 .append(iter->second);
546
547 for (iter++; iter != std::end(canonical_qs_map); iter++) {
548 canonical_qs.append("&", ::strlen("&"))
549 .append(iter->first)
550 .append("=", ::strlen("="))
551 .append(iter->second);
552 }
553
554 return canonical_qs;
555 }
556
557 boost::optional<std::string>
558 get_v4_canonical_headers(const req_info& info,
559 const boost::string_view& signedheaders,
560 const bool using_qs,
561 const bool force_boto2_compat)
562 {
563 std::map<boost::string_view, std::string> canonical_hdrs_map;
564 for (const auto& token : get_str_vec<5>(signedheaders, ";")) {
565 /* TODO(rzarzynski): we'd like to switch to sstring here but it should
566 * get push_back() and reserve() first. */
567 std::string token_env = "HTTP_";
568 token_env.reserve(token.length() + std::strlen("HTTP_") + 1);
569
570 std::transform(std::begin(token), std::end(token),
571 std::back_inserter(token_env), [](const int c) {
572 return c == '-' ? '_' : std::toupper(c);
573 });
574
575 if (token_env == "HTTP_CONTENT_LENGTH") {
576 token_env = "CONTENT_LENGTH";
577 } else if (token_env == "HTTP_CONTENT_TYPE") {
578 token_env = "CONTENT_TYPE";
579 }
580 const char* const t = info.env->get(token_env.c_str());
581 if (!t) {
582 dout(10) << "warning env var not available" << dendl;
583 continue;
584 }
585
586 std::string token_value(t);
587 if (token_env == "HTTP_CONTENT_MD5" &&
588 !std::all_of(std::begin(token_value), std::end(token_value),
589 is_base64_for_content_md5)) {
590 dout(0) << "NOTICE: bad content-md5 provided (not base64)"
591 << ", aborting request" << dendl;
592 return boost::none;
593 }
594
595 if (force_boto2_compat && using_qs && token == "host") {
596 boost::string_view port = info.env->get("SERVER_PORT", "");
597 boost::string_view secure_port = info.env->get("SERVER_PORT_SECURE", "");
598
599 if (!secure_port.empty()) {
600 if (secure_port != "443")
601 token_value.append(":", std::strlen(":"))
602 .append(secure_port.data(), secure_port.length());
603 } else if (!port.empty()) {
604 if (port != "80")
605 token_value.append(":", std::strlen(":"))
606 .append(port.data(), port.length());
607 }
608 }
609
610 canonical_hdrs_map[token] = rgw_trim_whitespace(token_value);
611 }
612
613 std::string canonical_hdrs;
614 for (const auto& header : canonical_hdrs_map) {
615 const boost::string_view& name = header.first;
616 const std::string& value = header.second;
617
618 canonical_hdrs.append(name.data(), name.length())
619 .append(":", std::strlen(":"))
620 .append(value)
621 .append("\n", std::strlen("\n"));
622 }
623
624 return canonical_hdrs;
625 }
626
627 /*
628 * create canonical request for signature version 4
629 *
630 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
631 */
632 sha256_digest_t
633 get_v4_canon_req_hash(CephContext* cct,
634 const boost::string_view& http_verb,
635 const std::string& canonical_uri,
636 const std::string& canonical_qs,
637 const std::string& canonical_hdrs,
638 const boost::string_view& signed_hdrs,
639 const boost::string_view& request_payload_hash)
640 {
641 ldout(cct, 10) << "payload request hash = " << request_payload_hash << dendl;
642
643 const auto canonical_req = string_join_reserve("\n",
644 http_verb,
645 canonical_uri,
646 canonical_qs,
647 canonical_hdrs,
648 signed_hdrs,
649 request_payload_hash);
650
651 const auto canonical_req_hash = calc_hash_sha256(canonical_req);
652
653 ldout(cct, 10) << "canonical request = " << canonical_req << dendl;
654 ldout(cct, 10) << "canonical request hash = "
655 << buf_to_hex(canonical_req_hash).data() << dendl;
656
657 return canonical_req_hash;
658 }
659
660 /*
661 * create string to sign for signature version 4
662 *
663 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
664 */
665 AWSEngine::VersionAbstractor::string_to_sign_t
666 get_v4_string_to_sign(CephContext* const cct,
667 const boost::string_view& algorithm,
668 const boost::string_view& request_date,
669 const boost::string_view& credential_scope,
670 const sha256_digest_t& canonreq_hash)
671 {
672 const auto hexed_cr_hash = buf_to_hex(canonreq_hash);
673 const boost::string_view hexed_cr_hash_str(hexed_cr_hash.data(),
674 hexed_cr_hash.size() - 1);
675
676 const auto string_to_sign = string_join_reserve("\n",
677 algorithm,
678 request_date,
679 credential_scope,
680 hexed_cr_hash_str);
681
682 ldout(cct, 10) << "string to sign = "
683 << rgw::crypt_sanitize::log_content{string_to_sign}
684 << dendl;
685
686 return string_to_sign;
687 }
688
689
690 static inline std::tuple<boost::string_view, /* date */
691 boost::string_view, /* region */
692 boost::string_view> /* service */
693 parse_cred_scope(boost::string_view credential_scope)
694 {
695 /* date cred */
696 size_t pos = credential_scope.find("/");
697 const auto date_cs = credential_scope.substr(0, pos);
698 credential_scope = credential_scope.substr(pos + 1);
699
700 /* region cred */
701 pos = credential_scope.find("/");
702 const auto region_cs = credential_scope.substr(0, pos);
703 credential_scope = credential_scope.substr(pos + 1);
704
705 /* service cred */
706 pos = credential_scope.find("/");
707 const auto service_cs = credential_scope.substr(0, pos);
708
709 return std::make_tuple(date_cs, region_cs, service_cs);
710 }
711
712 static inline std::vector<unsigned char>
713 transform_secret_key(const boost::string_view& secret_access_key)
714 {
715 /* TODO(rzarzynski): switch to constexpr when C++14 becomes available. */
716 static const std::initializer_list<unsigned char> AWS4 { 'A', 'W', 'S', '4' };
717
718 /* boost::container::small_vector might be used here if someone wants to
719 * optimize out even more dynamic allocations. */
720 std::vector<unsigned char> secret_key_utf8;
721 secret_key_utf8.reserve(AWS4.size() + secret_access_key.size());
722 secret_key_utf8.assign(AWS4);
723
724 for (const auto c : secret_access_key) {
725 std::array<unsigned char, MAX_UTF8_SZ> buf;
726 const size_t n = encode_utf8(c, buf.data());
727 secret_key_utf8.insert(std::end(secret_key_utf8),
728 std::begin(buf), std::begin(buf) + n);
729 }
730
731 return secret_key_utf8;
732 }
733
734 /*
735 * calculate the SigningKey of AWS auth version 4
736 */
737 static sha256_digest_t
738 get_v4_signing_key(CephContext* const cct,
739 const boost::string_view& credential_scope,
740 const boost::string_view& secret_access_key)
741 {
742 boost::string_view date, region, service;
743 std::tie(date, region, service) = parse_cred_scope(credential_scope);
744
745 const auto utfed_sec_key = transform_secret_key(secret_access_key);
746 const auto date_k = calc_hmac_sha256(utfed_sec_key, date);
747 const auto region_k = calc_hmac_sha256(date_k, region);
748 const auto service_k = calc_hmac_sha256(region_k, service);
749
750 /* aws4_request */
751 const auto signing_key = calc_hmac_sha256(service_k,
752 boost::string_view("aws4_request"));
753
754 ldout(cct, 10) << "date_k = " << buf_to_hex(date_k).data() << dendl;
755 ldout(cct, 10) << "region_k = " << buf_to_hex(region_k).data() << dendl;
756 ldout(cct, 10) << "service_k = " << buf_to_hex(service_k).data() << dendl;
757 ldout(cct, 10) << "signing_k = " << buf_to_hex(signing_key).data() << dendl;
758
759 return signing_key;
760 }
761
762 /*
763 * calculate the AWS signature version 4
764 *
765 * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
766 *
767 * srv_signature_t is an alias over Ceph's basic_sstring. We're using
768 * it to keep everything within the stack boundaries instead of doing
769 * dynamic allocations.
770 */
771 AWSEngine::VersionAbstractor::server_signature_t
772 get_v4_signature(const boost::string_view& credential_scope,
773 CephContext* const cct,
774 const boost::string_view& secret_key,
775 const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign)
776 {
777 auto signing_key = get_v4_signing_key(cct, credential_scope, secret_key);
778
779 /* The server-side generated digest for comparison. */
780 const auto digest = calc_hmac_sha256(signing_key, string_to_sign);
781
782 /* TODO(rzarzynski): I would love to see our sstring having reserve() and
783 * the non-const data() variant like C++17's std::string. */
784 using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
785 srv_signature_t signature(srv_signature_t::initialized_later(),
786 digest.size() * 2);
787 buf_to_hex(digest.data(), digest.size(), signature.begin());
788
789 ldout(cct, 10) << "generated signature = " << signature << dendl;
790
791 return signature;
792 }
793
794 AWSEngine::VersionAbstractor::server_signature_t
795 get_v2_signature(CephContext* const cct,
796 const std::string& secret_key,
797 const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign)
798 {
799 if (secret_key.empty()) {
800 throw -EINVAL;
801 }
802
803 const auto digest = calc_hmac_sha1(secret_key, string_to_sign);
804
805 /* 64 is really enough */;
806 char buf[64];
807 const int ret = ceph_armor(std::begin(buf),
808 std::begin(buf) + 64,
809 std::begin(digest),
810 std::begin(digest) + digest.size());
811 if (ret < 0) {
812 ldout(cct, 10) << "ceph_armor failed" << dendl;
813 throw ret;
814 } else {
815 buf[ret] = '\0';
816 using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
817 return srv_signature_t(buf, ret);
818 }
819 }
820
821 bool AWSv4ComplMulti::ChunkMeta::is_new_chunk_in_stream(size_t stream_pos) const
822 {
823 return stream_pos >= (data_offset_in_stream + data_length);
824 }
825
826 size_t AWSv4ComplMulti::ChunkMeta::get_data_size(size_t stream_pos) const
827 {
828 if (stream_pos > (data_offset_in_stream + data_length)) {
829 /* Data in parsing_buf. */
830 return data_length;
831 } else {
832 return data_offset_in_stream + data_length - stream_pos;
833 }
834 }
835
836
837 /* AWSv4 completers begin. */
838 std::pair<AWSv4ComplMulti::ChunkMeta, size_t /* consumed */>
839 AWSv4ComplMulti::ChunkMeta::create_next(CephContext* const cct,
840 ChunkMeta&& old,
841 const char* const metabuf,
842 const size_t metabuf_len)
843 {
844 boost::string_ref metastr(metabuf, metabuf_len);
845
846 const size_t semicolon_pos = metastr.find(";");
847 if (semicolon_pos == boost::string_ref::npos) {
848 ldout(cct, 20) << "AWSv4ComplMulti cannot find the ';' separator"
849 << dendl;
850 throw rgw::io::Exception(EINVAL, std::system_category());
851 }
852
853 char* data_field_end;
854 /* strtoull ignores the "\r\n" sequence after each non-first chunk. */
855 const size_t data_length = std::strtoull(metabuf, &data_field_end, 16);
856 if (data_length == 0 && data_field_end == metabuf) {
857 ldout(cct, 20) << "AWSv4ComplMulti: cannot parse the data size"
858 << dendl;
859 throw rgw::io::Exception(EINVAL, std::system_category());
860 }
861
862 /* Parse the chunk_signature=... part. */
863 const auto signature_part = metastr.substr(semicolon_pos + 1);
864 const size_t eq_sign_pos = signature_part.find("=");
865 if (eq_sign_pos == boost::string_ref::npos) {
866 ldout(cct, 20) << "AWSv4ComplMulti: cannot find the '=' separator"
867 << dendl;
868 throw rgw::io::Exception(EINVAL, std::system_category());
869 }
870
871 /* OK, we have at least the beginning of a signature. */
872 const size_t data_sep_pos = signature_part.find("\r\n");
873 if (data_sep_pos == boost::string_ref::npos) {
874 ldout(cct, 20) << "AWSv4ComplMulti: no new line at signature end"
875 << dendl;
876 throw rgw::io::Exception(EINVAL, std::system_category());
877 }
878
879 const auto signature = \
880 signature_part.substr(eq_sign_pos + 1, data_sep_pos - 1 - eq_sign_pos);
881 if (signature.length() != SIG_SIZE) {
882 ldout(cct, 20) << "AWSv4ComplMulti: signature.length() != 64"
883 << dendl;
884 throw rgw::io::Exception(EINVAL, std::system_category());
885 }
886
887 const size_t data_starts_in_stream = \
888 + semicolon_pos + strlen(";") + data_sep_pos + strlen("\r\n")
889 + old.data_offset_in_stream + old.data_length;
890
891 ldout(cct, 20) << "parsed new chunk; signature=" << signature
892 << ", data_length=" << data_length
893 << ", data_starts_in_stream=" << data_starts_in_stream
894 << dendl;
895
896 return std::make_pair(ChunkMeta(data_starts_in_stream,
897 data_length,
898 signature),
899 semicolon_pos + 83);
900 }
901
902 std::string
903 AWSv4ComplMulti::calc_chunk_signature(const std::string& payload_hash) const
904 {
905 const auto string_to_sign = string_join_reserve("\n",
906 AWS4_HMAC_SHA256_PAYLOAD_STR,
907 date,
908 credential_scope,
909 prev_chunk_signature,
910 AWS4_EMPTY_PAYLOAD_HASH,
911 payload_hash);
912
913 ldout(cct, 20) << "AWSv4ComplMulti: string_to_sign=\n" << string_to_sign
914 << dendl;
915
916 /* new chunk signature */
917 const auto sighex = buf_to_hex(calc_hmac_sha256(signing_key,
918 string_to_sign));
919 /* FIXME(rzarzynski): std::string here is really unnecessary. */
920 return std::string(sighex.data(), sighex.size() - 1);
921 }
922
923
924 bool AWSv4ComplMulti::is_signature_mismatched()
925 {
926 /* The validity of previous chunk can be verified only after getting meta-
927 * data of the next one. */
928 const auto payload_hash = calc_hash_sha256_restart_stream(&sha256_hash);
929 const auto calc_signature = calc_chunk_signature(payload_hash);
930
931 if (chunk_meta.get_signature() != calc_signature) {
932 ldout(cct, 20) << "AWSv4ComplMulti: ERROR: chunk signature mismatch"
933 << dendl;
934 ldout(cct, 20) << "AWSv4ComplMulti: declared signature="
935 << chunk_meta.get_signature() << dendl;
936 ldout(cct, 20) << "AWSv4ComplMulti: calculated signature="
937 << calc_signature << dendl;
938
939 return true;
940 } else {
941 prev_chunk_signature = chunk_meta.get_signature();
942 return false;
943 }
944 }
945
946 size_t AWSv4ComplMulti::recv_body(char* const buf, const size_t buf_max)
947 {
948 /* Buffer stores only parsed stream. Raw values reflect the stream
949 * we're getting from a client. */
950 size_t buf_pos = 0;
951
952 if (chunk_meta.is_new_chunk_in_stream(stream_pos)) {
953 /* Verify signature of the previous chunk. We aren't doing that for new
954 * one as the procedure requires calculation of payload hash. This code
955 * won't be triggered for the last, zero-length chunk. Instead, is will
956 * be checked in the complete() method. */
957 if (stream_pos >= ChunkMeta::META_MAX_SIZE && is_signature_mismatched()) {
958 throw rgw::io::Exception(ERR_SIGNATURE_NO_MATCH, std::system_category());
959 }
960
961 /* We don't have metadata for this range. This means a new chunk, so we
962 * need to parse a fresh portion of the stream. Let's start. */
963 size_t to_extract = parsing_buf.capacity() - parsing_buf.size();
964 do {
965 const size_t orig_size = parsing_buf.size();
966 parsing_buf.resize(parsing_buf.size() + to_extract);
967 const size_t received = io_base_t::recv_body(parsing_buf.data() + orig_size,
968 to_extract);
969 parsing_buf.resize(parsing_buf.size() - (to_extract - received));
970 if (received == 0) {
971 break;
972 }
973
974 stream_pos += received;
975 to_extract -= received;
976 } while (to_extract > 0);
977
978 size_t consumed;
979 std::tie(chunk_meta, consumed) = \
980 ChunkMeta::create_next(cct, std::move(chunk_meta),
981 parsing_buf.data(), parsing_buf.size());
982
983 /* We can drop the bytes consumed during metadata parsing. The remainder
984 * can be chunk's data plus possibly beginning of next chunks' metadata. */
985 parsing_buf.erase(std::begin(parsing_buf),
986 std::begin(parsing_buf) + consumed);
987 }
988
989 size_t to_extract = \
990 std::min(chunk_meta.get_data_size(stream_pos), buf_max);
991
992 /* It's quite probable we have a couple of real data bytes stored together
993 * with meta-data in the parsing_buf. We need to extract them and move to
994 * the final buffer. This is a trade-off between frontend's read overhead
995 * and memcpy. */
996 if (to_extract > 0 && parsing_buf.size() > 0) {
997 const auto data_len = std::min(to_extract, parsing_buf.size());
998 const auto data_end_iter = std::begin(parsing_buf) + data_len;
999
1000 std::copy(std::begin(parsing_buf), data_end_iter, buf);
1001 parsing_buf.erase(std::begin(parsing_buf), data_end_iter);
1002
1003 calc_hash_sha256_update_stream(sha256_hash, buf, data_len);
1004
1005 to_extract -= data_len;
1006 buf_pos += data_len;
1007 }
1008
1009 /* Now we can do the bulk read directly from RestfulClient without any extra
1010 * buffering. */
1011 while (to_extract > 0) {
1012 const size_t received = io_base_t::recv_body(buf + buf_pos, to_extract);
1013
1014 if (received == 0) {
1015 break;
1016 }
1017
1018 calc_hash_sha256_update_stream(sha256_hash, buf + buf_pos, received);
1019
1020 buf_pos += received;
1021 stream_pos += received;
1022 to_extract -= received;
1023 }
1024
1025 dout(20) << "AWSv4ComplMulti: filled=" << buf_pos << dendl;
1026 return buf_pos;
1027 }
1028
1029 void AWSv4ComplMulti::modify_request_state(req_state* const s_rw)
1030 {
1031 const char* const decoded_length = \
1032 s_rw->info.env->get("HTTP_X_AMZ_DECODED_CONTENT_LENGTH");
1033
1034 if (!decoded_length) {
1035 throw -EINVAL;
1036 } else {
1037 s_rw->length = decoded_length;
1038 s_rw->content_length = parse_content_length(decoded_length);
1039
1040 if (s_rw->content_length < 0) {
1041 ldout(cct, 10) << "negative AWSv4's content length, aborting" << dendl;
1042 throw -EINVAL;
1043 }
1044 }
1045
1046 /* Install the filter over rgw::io::RestfulClient. */
1047 AWS_AUTHv4_IO(s_rw)->add_filter(
1048 std::static_pointer_cast<io_base_t>(shared_from_this()));
1049 }
1050
1051 bool AWSv4ComplMulti::complete()
1052 {
1053 /* Now it's time to verify the signature of the last, zero-length chunk. */
1054 if (is_signature_mismatched()) {
1055 ldout(cct, 10) << "ERROR: signature of last chunk does not match"
1056 << dendl;
1057 return false;
1058 } else {
1059 return true;
1060 }
1061 }
1062
1063 rgw::auth::Completer::cmplptr_t
1064 AWSv4ComplMulti::create(const req_state* const s,
1065 boost::string_view date,
1066 boost::string_view credential_scope,
1067 boost::string_view seed_signature,
1068 const boost::optional<std::string>& secret_key)
1069 {
1070 if (!secret_key) {
1071 /* Some external authorizers (like Keystone) aren't fully compliant with
1072 * AWSv4. They do not provide the secret_key which is necessary to handle
1073 * the streamed upload. */
1074 throw -ERR_NOT_IMPLEMENTED;
1075 }
1076
1077 const auto signing_key = \
1078 rgw::auth::s3::get_v4_signing_key(s->cct, credential_scope, *secret_key);
1079
1080 return std::make_shared<AWSv4ComplMulti>(s,
1081 std::move(date),
1082 std::move(credential_scope),
1083 std::move(seed_signature),
1084 signing_key);
1085 }
1086
1087 size_t AWSv4ComplSingle::recv_body(char* const buf, const size_t max)
1088 {
1089 const auto received = io_base_t::recv_body(buf, max);
1090 calc_hash_sha256_update_stream(sha256_hash, buf, received);
1091
1092 return received;
1093 }
1094
1095 void AWSv4ComplSingle::modify_request_state(req_state* const s_rw)
1096 {
1097 /* Install the filter over rgw::io::RestfulClient. */
1098 AWS_AUTHv4_IO(s_rw)->add_filter(
1099 std::static_pointer_cast<io_base_t>(shared_from_this()));
1100 }
1101
1102 bool AWSv4ComplSingle::complete()
1103 {
1104 /* The completer is only for the cases where signed payload has been
1105 * requested. It won't be used, for instance, during the query string-based
1106 * authentication. */
1107 const auto payload_hash = calc_hash_sha256_close_stream(&sha256_hash);
1108
1109 /* Validate x-amz-sha256 */
1110 if (payload_hash.compare(expected_request_payload_hash) == 0) {
1111 return true;
1112 } else {
1113 ldout(cct, 10) << "ERROR: x-amz-content-sha256 does not match"
1114 << dendl;
1115 ldout(cct, 10) << "ERROR: grab_aws4_sha256_hash()="
1116 << payload_hash << dendl;
1117 ldout(cct, 10) << "ERROR: expected_request_payload_hash="
1118 << expected_request_payload_hash << dendl;
1119 return false;
1120 }
1121 }
1122
1123 AWSv4ComplSingle::AWSv4ComplSingle(const req_state* const s)
1124 : io_base_t(nullptr),
1125 cct(s->cct),
1126 expected_request_payload_hash(get_v4_exp_payload_hash(s->info)),
1127 sha256_hash(calc_hash_sha256_open_stream()) {
1128 }
1129
1130 rgw::auth::Completer::cmplptr_t
1131 AWSv4ComplSingle::create(const req_state* const s,
1132 const boost::optional<std::string>&)
1133 {
1134 return std::make_shared<AWSv4ComplSingle>(s);
1135 }
1136
1137 } /* namespace s3 */
1138 } /* namespace auth */
1139 } /* namespace rgw */