1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab
10 #include "common/armor.h"
11 #include "common/utf8.h"
12 #include "rgw_auth_s3.h"
13 #include "rgw_common.h"
14 #include "rgw_client_io.h"
16 #include "rgw_crypt_sanitize.h"
18 #include <boost/container/small_vector.hpp>
19 #include <boost/utility/string_view.hpp>
21 #define dout_context g_ceph_context
22 #define dout_subsys ceph_subsys_rgw
24 static const auto signed_subresources
= {
35 "response-cache-control",
36 "response-content-disposition",
37 "response-content-encoding",
38 "response-content-language",
39 "response-content-type",
53 * ?get the canonical amazon-style header for something?
57 get_canon_amz_hdr(const std::map
<std::string
, std::string
>& meta_map
)
61 for (const auto& kv
: meta_map
) {
62 dest
.append(kv
.first
);
64 dest
.append(kv
.second
);
72 * ?get the canonical representation of the object's location
75 get_canon_resource(const char* const request_uri
,
76 const std::map
<std::string
, std::string
>& sub_resources
)
81 dest
.append(request_uri
);
85 for (const auto& subresource
: signed_subresources
) {
86 const auto iter
= sub_resources
.find(subresource
);
87 if (iter
== std::end(sub_resources
)) {
98 dest
.append(iter
->first
);
99 if (! iter
->second
.empty()) {
101 dest
.append(iter
->second
);
105 dout(10) << "get_canon_resource(): dest=" << dest
<< dendl
;
110 * get the header authentication information required to
111 * compute a request's signature
113 void 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
)
131 dest
.append(content_md5
);
136 dest
.append(content_type
);
145 dest
.append(get_canon_amz_hdr(meta_map
));
146 dest
.append(get_canon_resource(request_uri
, sub_resources
));
151 static inline bool is_base64_for_content_md5(unsigned char c
) {
152 return (isalnum(c
) || isspace(c
) || (c
== '+') || (c
== '/') || (c
== '='));
156 * get the header authentication information required to
157 * compute a request's signature
159 bool rgw_create_s3_canonical_header(const req_info
& info
,
160 utime_t
* const header_time
,
164 const char* const content_md5
= info
.env
->get("HTTP_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
;
175 const char *content_type
= info
.env
->get("CONTENT_TYPE");
179 date
= info
.args
.get("Expires");
181 const char *str
= info
.env
->get("HTTP_DATE");
182 const char *req_date
= str
;
186 req_date
= info
.env
->get("HTTP_X_AMZ_DATE");
188 dout(0) << "NOTICE: missing date for auth header" << dendl
;
195 if (!parse_rfc2616(req_date
, &t
)) {
196 dout(0) << "NOTICE: failed to parse date for auth header" << dendl
;
199 if (t
.tm_year
< 70) {
200 dout(0) << "NOTICE: bad date (predates epoch): " << req_date
<< dendl
;
203 *header_time
= utime_t(internal_timegm(&t
), 0);
207 const auto& meta_map
= info
.x_meta_map
;
208 const auto& sub_resources
= info
.args
.get_sub_resources();
210 std::string request_uri
;
211 if (info
.effective_uri
.empty()) {
212 request_uri
= info
.request_uri
;
214 request_uri
= info
.effective_uri
;
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
);
228 /* FIXME(rzarzynski): duplicated from rgw_rest_s3.h. */
229 #define RGW_AUTH_GRACE_MINS 15
231 static 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 */
237 /* auth ships with req params ... */
239 /* look for required params */
240 credential
= info
.args
.get("X-Amz-Credential");
241 if (credential
.size() == 0) {
245 date
= info
.args
.get("X-Amz-Date");
247 if (!parse_iso8601(sview2cstr(date
).data(), &date_t
, nullptr, false)) {
251 /* Used for pre-signatured url, We shouldn't return -ERR_REQUEST_TIME_SKEWED
252 * when current time <= X-Amz-Expires */
255 uint64_t now_req
= 0;
256 uint64_t now
= ceph_clock_now();
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
;
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
;
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
;
288 signedheaders
= info
.args
.get("X-Amz-SignedHeaders");
289 if (signedheaders
.size() == 0) {
293 signature
= info
.args
.get("X-Amz-Signature");
294 if (signature
.size() == 0) {
302 static bool get_next_token(const boost::string_view
& s
,
304 const char* const delims
,
305 boost::string_view
& token
)
307 const size_t start
= s
.find_first_not_of(delims
, pos
);
308 if (start
== boost::string_view::npos
) {
313 size_t end
= s
.find_first_of(delims
, start
);
314 if (end
!= boost::string_view::npos
)
317 pos
= end
= s
.size();
320 token
= s
.substr(start
, end
- start
);
324 template<std::size_t ExpectedStrNum
>
325 boost::container::small_vector
<boost::string_view
, ExpectedStrNum
>
326 get_str_vec(const boost::string_view
& str
, const char* const delims
)
328 boost::container::small_vector
<boost::string_view
, ExpectedStrNum
> str_vec
;
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
);
343 template<std::size_t ExpectedStrNum
>
344 boost::container::small_vector
<boost::string_view
, ExpectedStrNum
>
345 get_str_vec(const boost::string_view
& str
)
347 const char delims
[] = ";,= \t";
348 return get_str_vec
<ExpectedStrNum
>(str
, delims
);
352 static 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 */
358 boost::string_view
input(info
.env
->get("HTTP_AUTHORIZATION", ""));
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
;
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
);
372 kv
[parsed_pair
->first
] = parsed_pair
->second
;
374 dout(10) << "NOTICE: failed to parse auth header (s=" << s
<< ")"
380 static const std::array
<boost::string_view
, 3> required_keys
= {
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
;
394 credential
= kv
["Credential"];
395 signedheaders
= kv
["SignedHeaders"];
396 signature
= kv
["Signature"];
399 dout(10) << "v4 signature format = " << signature
<< dendl
;
401 /* ------------------------- handle x-amz-date header */
405 const char *d
= info
.env
->get("HTTP_X_AMZ_DATE");
407 if (!parse_iso8601(d
, &t
, NULL
, false)) {
408 dout(10) << "error reading date via http_x_amz_date" << dendl
;
416 int 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 */
424 const char* const http_auth
= info
.env
->get("HTTP_AUTHORIZATION");
425 using_qs
= http_auth
== nullptr || http_auth
[0] == '\0';
428 boost::string_view credential
;
430 ret
= parse_v4_query_string(info
, credential
, signedheaders
,
433 ret
= parse_v4_auth_header(info
, credential
, signedheaders
,
441 /* AKIAIVKTAZLOCF43WNQD/AAAAMMDD/region/host/aws4_request */
442 dout(10) << "v4 credential format = " << credential
<< dendl
;
444 if (std::count(credential
.begin(), credential
.end(), '/') != 4) {
448 /* credential must end with 'aws4_request' */
449 if (credential
.find("aws4_request") == std::string::npos
) {
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
;
458 /* grab credential scope */
459 credential_scope
= credential
.substr(pos
+ 1);
460 dout(10) << "credential scope = " << credential_scope
<< dendl
;
465 static inline bool char_needs_aws4_escaping(const char c
)
467 if ((c
>= 'a' && c
<= 'z') ||
468 (c
>= 'A' && c
<= 'Z') ||
469 (c
>= '0' && c
<= '9')) {
483 static inline std::string
aws4_uri_encode(const std::string
& src
)
487 for (const std::string::value_type c
: src
) {
488 if (char_needs_aws4_escaping(c
)) {
489 rgw_uri_escape_char(c
, result
);
498 static inline std::string
aws4_uri_recode(const boost::string_view
& src
)
500 std::string decoded
= url_decode(src
);
501 if (decoded
.length() != src
.length()) {
502 return src
.to_string();
504 return aws4_uri_encode(decoded
);
508 std::string
get_v4_canonical_qs(const req_info
& info
, const bool using_qs
)
510 if (info
.request_params
.empty()) {
511 /* Optimize the typical flow. */
512 return std::string();
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
);
522 std::tie(key
, val
) = *parsed_pair
;
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. */
530 if (using_qs
&& key
== "X-Amz-Signature") {
531 /* Preserving the original behaviour of get_v4_canonical_qs() here. */
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
539 canonical_qs_map
[key
.to_string()] = val
.to_string();
541 canonical_qs_map
[aws4_uri_recode(key
)] = aws4_uri_recode(val
);
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
);
553 for (iter
++; iter
!= std::end(canonical_qs_map
); iter
++) {
554 canonical_qs
.append("&", ::strlen("&"))
556 .append("=", ::strlen("="))
557 .append(iter
->second
);
563 boost::optional
<std::string
>
564 get_v4_canonical_headers(const req_info
& info
,
565 const boost::string_view
& signedheaders
,
567 const bool force_boto2_compat
)
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);
576 std::transform(std::begin(token
), std::end(token
),
577 std::back_inserter(token_env
), [](const int c
) {
578 return c
== '-' ? '_' : std::toupper(c
);
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";
586 const char* const t
= info
.env
->get(token_env
.c_str());
588 dout(10) << "warning env var not available" << dendl
;
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
;
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", "");
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()) {
611 token_value
.append(":", std::strlen(":"))
612 .append(port
.data(), port
.length());
616 canonical_hdrs_map
[token
] = rgw_trim_whitespace(token_value
);
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
;
624 canonical_hdrs
.append(name
.data(), name
.length())
625 .append(":", std::strlen(":"))
627 .append("\n", std::strlen("\n"));
630 return canonical_hdrs
;
634 * create canonical request for signature version 4
636 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
639 get_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
)
647 ldout(cct
, 10) << "payload request hash = " << request_payload_hash
<< dendl
;
649 const auto canonical_req
= string_join_reserve("\n",
655 request_payload_hash
);
657 const auto canonical_req_hash
= calc_hash_sha256(canonical_req
);
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
;
663 return canonical_req_hash
;
667 * create string to sign for signature version 4
669 * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
671 AWSEngine::VersionAbstractor::string_to_sign_t
672 get_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
)
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);
682 const auto string_to_sign
= string_join_reserve("\n",
688 ldout(cct
, 10) << "string to sign = "
689 << rgw::crypt_sanitize::log_content
{string_to_sign
}
692 return string_to_sign
;
696 static inline std::tuple
<boost::string_view
, /* date */
697 boost::string_view
, /* region */
698 boost::string_view
> /* service */
699 parse_cred_scope(boost::string_view credential_scope
)
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);
707 pos
= credential_scope
.find("/");
708 const auto region_cs
= credential_scope
.substr(0, pos
);
709 credential_scope
= credential_scope
.substr(pos
+ 1);
712 pos
= credential_scope
.find("/");
713 const auto service_cs
= credential_scope
.substr(0, pos
);
715 return std::make_tuple(date_cs
, region_cs
, service_cs
);
718 static inline std::vector
<unsigned char>
719 transform_secret_key(const boost::string_view
& secret_access_key
)
721 /* TODO(rzarzynski): switch to constexpr when C++14 becomes available. */
722 static const std::initializer_list
<unsigned char> AWS4
{ 'A', 'W', 'S', '4' };
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
);
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
);
737 return secret_key_utf8
;
741 * calculate the SigningKey of AWS auth version 4
743 static sha256_digest_t
744 get_v4_signing_key(CephContext
* const cct
,
745 const boost::string_view
& credential_scope
,
746 const boost::string_view
& secret_access_key
)
748 boost::string_view date
, region
, service
;
749 std::tie(date
, region
, service
) = parse_cred_scope(credential_scope
);
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
);
757 const auto signing_key
= calc_hmac_sha256(service_k
,
758 boost::string_view("aws4_request"));
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
;
769 * calculate the AWS signature version 4
771 * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
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.
777 AWSEngine::VersionAbstractor::server_signature_t
778 get_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
)
783 auto signing_key
= get_v4_signing_key(cct
, credential_scope
, secret_key
);
785 /* The server-side generated digest for comparison. */
786 const auto digest
= calc_hmac_sha256(signing_key
, string_to_sign
);
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(),
793 buf_to_hex(digest
.data(), digest
.size(), signature
.begin());
795 ldout(cct
, 10) << "generated signature = " << signature
<< dendl
;
800 AWSEngine::VersionAbstractor::server_signature_t
801 get_v2_signature(CephContext
* const cct
,
802 const std::string
& secret_key
,
803 const AWSEngine::VersionAbstractor::string_to_sign_t
& string_to_sign
)
805 if (secret_key
.empty()) {
809 const auto digest
= calc_hmac_sha1(secret_key
, string_to_sign
);
811 /* 64 is really enough */;
813 const int ret
= ceph_armor(std::begin(buf
),
814 std::begin(buf
) + 64,
816 std::begin(digest
) + digest
.size());
818 ldout(cct
, 10) << "ceph_armor failed" << dendl
;
822 using srv_signature_t
= AWSEngine::VersionAbstractor::server_signature_t
;
823 return srv_signature_t(buf
, ret
);
827 bool AWSv4ComplMulti::ChunkMeta::is_new_chunk_in_stream(size_t stream_pos
) const
829 return stream_pos
>= (data_offset_in_stream
+ data_length
);
832 size_t AWSv4ComplMulti::ChunkMeta::get_data_size(size_t stream_pos
) const
834 if (stream_pos
> (data_offset_in_stream
+ data_length
)) {
835 /* Data in parsing_buf. */
838 return data_offset_in_stream
+ data_length
- stream_pos
;
843 /* AWSv4 completers begin. */
844 std::pair
<AWSv4ComplMulti::ChunkMeta
, size_t /* consumed */>
845 AWSv4ComplMulti::ChunkMeta::create_next(CephContext
* const cct
,
847 const char* const metabuf
,
848 const size_t metabuf_len
)
850 boost::string_ref
metastr(metabuf
, metabuf_len
);
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"
856 throw rgw::io::Exception(EINVAL
, std::system_category());
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"
865 throw rgw::io::Exception(EINVAL
, std::system_category());
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"
874 throw rgw::io::Exception(EINVAL
, std::system_category());
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"
882 throw rgw::io::Exception(EINVAL
, std::system_category());
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"
890 throw rgw::io::Exception(EINVAL
, std::system_category());
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
;
897 ldout(cct
, 20) << "parsed new chunk; signature=" << signature
898 << ", data_length=" << data_length
899 << ", data_starts_in_stream=" << data_starts_in_stream
902 return std::make_pair(ChunkMeta(data_starts_in_stream
,
909 AWSv4ComplMulti::calc_chunk_signature(const std::string
& payload_hash
) const
911 const auto string_to_sign
= string_join_reserve("\n",
912 AWS4_HMAC_SHA256_STR
,
915 prev_chunk_signature
,
916 AWS4_EMPTY_PAYLOAD_HASH
,
919 ldout(cct
, 20) << "AWSv4ComplMulti: string_to_sign=\n" << string_to_sign
922 /* new chunk signature */
923 const auto sighex
= buf_to_hex(calc_hmac_sha256(signing_key
,
925 /* FIXME(rzarzynski): std::string here is really unnecessary. */
926 return std::string(sighex
.data(), sighex
.size() - 1);
930 bool AWSv4ComplMulti::is_signature_mismatched()
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
);
937 if (chunk_meta
.get_signature() != calc_signature
) {
938 ldout(cct
, 20) << "AWSv4ComplMulti: ERROR: chunk signature mismatch"
940 ldout(cct
, 20) << "AWSv4ComplMulti: declared signature="
941 << chunk_meta
.get_signature() << dendl
;
942 ldout(cct
, 20) << "AWSv4ComplMulti: calculated signature="
943 << calc_signature
<< dendl
;
947 prev_chunk_signature
= chunk_meta
.get_signature();
952 size_t AWSv4ComplMulti::recv_body(char* const buf
, const size_t buf_max
)
954 /* Buffer stores only parsed stream. Raw values reflect the stream
955 * we're getting from a client. */
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());
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();
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
,
975 parsing_buf
.resize(parsing_buf
.size() - (to_extract
- received
));
980 stream_pos
+= received
;
981 to_extract
-= received
;
982 } while (to_extract
> 0);
985 std::tie(chunk_meta
, consumed
) = \
986 ChunkMeta::create_next(cct
, std::move(chunk_meta
),
987 parsing_buf
.data(), parsing_buf
.size());
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
);
995 size_t to_extract
= \
996 std::min(chunk_meta
.get_data_size(stream_pos
), buf_max
);
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
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
;
1006 std::copy(std::begin(parsing_buf
), data_end_iter
, buf
);
1007 parsing_buf
.erase(std::begin(parsing_buf
), data_end_iter
);
1009 calc_hash_sha256_update_stream(sha256_hash
, buf
, data_len
);
1011 to_extract
-= data_len
;
1012 buf_pos
+= data_len
;
1015 /* Now we can do the bulk read directly from RestfulClient without any extra
1017 while (to_extract
> 0) {
1018 const size_t received
= io_base_t::recv_body(buf
+ buf_pos
, to_extract
);
1020 if (received
== 0) {
1024 calc_hash_sha256_update_stream(sha256_hash
, buf
+ buf_pos
, received
);
1026 buf_pos
+= received
;
1027 stream_pos
+= received
;
1028 to_extract
-= received
;
1031 dout(20) << "AWSv4ComplMulti: filled=" << buf_pos
<< dendl
;
1035 void AWSv4ComplMulti::modify_request_state(req_state
* const s_rw
)
1037 const char* const decoded_length
= \
1038 s_rw
->info
.env
->get("HTTP_X_AMZ_DECODED_CONTENT_LENGTH");
1040 if (!decoded_length
) {
1043 s_rw
->length
= decoded_length
;
1044 s_rw
->content_length
= parse_content_length(decoded_length
);
1046 if (s_rw
->content_length
< 0) {
1047 ldout(cct
, 10) << "negative AWSv4's content length, aborting" << dendl
;
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()));
1057 bool AWSv4ComplMulti::complete()
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"
1069 rgw::auth::Completer::cmplptr_t
1070 AWSv4ComplMulti::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
)
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
;
1083 const auto signing_key
= \
1084 rgw::auth::s3::get_v4_signing_key(s
->cct
, credential_scope
, *secret_key
);
1086 return std::make_shared
<AWSv4ComplMulti
>(s
,
1088 std::move(credential_scope
),
1089 std::move(seed_signature
),
1093 size_t AWSv4ComplSingle::recv_body(char* const buf
, const size_t max
)
1095 const auto received
= io_base_t::recv_body(buf
, max
);
1096 calc_hash_sha256_update_stream(sha256_hash
, buf
, received
);
1101 void AWSv4ComplSingle::modify_request_state(req_state
* const s_rw
)
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()));
1108 bool AWSv4ComplSingle::complete()
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
);
1115 /* Validate x-amz-sha256 */
1116 if (payload_hash
.compare(expected_request_payload_hash
) == 0) {
1119 ldout(cct
, 10) << "ERROR: x-amz-content-sha256 does not match"
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
;
1129 AWSv4ComplSingle::AWSv4ComplSingle(const req_state
* const s
)
1130 : io_base_t(nullptr),
1132 expected_request_payload_hash(get_v4_exp_payload_hash(s
->info
)),
1133 sha256_hash(calc_hash_sha256_open_stream()) {
1136 rgw::auth::Completer::cmplptr_t
1137 AWSv4ComplSingle::create(const req_state
* const s
,
1138 const boost::optional
<std::string
>&)
1140 return std::make_shared
<AWSv4ComplSingle
>(s
);
1143 } /* namespace s3 */
1144 } /* namespace auth */
1145 } /* namespace rgw */