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