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