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