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