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