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