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