summaryrefslogtreecommitdiffstats
path: root/src/rgw/rgw_auth_s3.cc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:54:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:54:28 +0000
commite6918187568dbd01842d8d1d2c808ce16a894239 (patch)
tree64f88b554b444a49f656b6c656111a145cbbaa28 /src/rgw/rgw_auth_s3.cc
parentInitial commit. (diff)
downloadceph-e6918187568dbd01842d8d1d2c808ce16a894239.tar.xz
ceph-e6918187568dbd01842d8d1d2c808ce16a894239.zip
Adding upstream version 18.2.2.upstream/18.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/rgw/rgw_auth_s3.cc')
-rw-r--r--src/rgw/rgw_auth_s3.cc1355
1 files changed, 1355 insertions, 0 deletions
diff --git a/src/rgw/rgw_auth_s3.cc b/src/rgw/rgw_auth_s3.cc
new file mode 100644
index 000000000..0797f8184
--- /dev/null
+++ b/src/rgw/rgw_auth_s3.cc
@@ -0,0 +1,1355 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+#include <algorithm>
+#include <map>
+#include <iterator>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "common/armor.h"
+#include "common/utf8.h"
+#include "rgw_rest_s3.h"
+#include "rgw_auth_s3.h"
+#include "rgw_common.h"
+#include "rgw_client_io.h"
+#include "rgw_rest.h"
+#include "rgw_crypt_sanitize.h"
+
+#include <boost/container/small_vector.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/trim_all.hpp>
+
+#define dout_context g_ceph_context
+#define dout_subsys ceph_subsys_rgw
+
+using namespace std;
+
+static const auto signed_subresources = {
+ "acl",
+ "cors",
+ "delete",
+ "encryption",
+ "lifecycle",
+ "location",
+ "logging",
+ "notification",
+ "partNumber",
+ "policy",
+ "policyStatus",
+ "publicAccessBlock",
+ "requestPayment",
+ "response-cache-control",
+ "response-content-disposition",
+ "response-content-encoding",
+ "response-content-language",
+ "response-content-type",
+ "response-expires",
+ "tagging",
+ "torrent",
+ "uploadId",
+ "uploads",
+ "versionId",
+ "versioning",
+ "versions",
+ "website",
+ "object-lock"
+};
+
+/*
+ * ?get the canonical amazon-style header for something?
+ */
+
+static std::string
+get_canon_amz_hdr(const meta_map_t& meta_map)
+{
+ std::string dest;
+
+ for (const auto& kv : meta_map) {
+ dest.append(kv.first);
+ dest.append(":");
+ dest.append(kv.second);
+ dest.append("\n");
+ }
+
+ return dest;
+}
+
+/*
+ * ?get the canonical representation of the object's location
+ */
+static std::string
+get_canon_resource(const DoutPrefixProvider *dpp, const char* const request_uri,
+ const std::map<std::string, std::string>& sub_resources)
+{
+ std::string dest;
+
+ if (request_uri) {
+ dest.append(request_uri);
+ }
+
+ bool initial = true;
+ for (const auto& subresource : signed_subresources) {
+ const auto iter = sub_resources.find(subresource);
+ if (iter == std::end(sub_resources)) {
+ continue;
+ }
+
+ if (initial) {
+ dest.append("?");
+ initial = false;
+ } else {
+ dest.append("&");
+ }
+
+ dest.append(iter->first);
+ if (! iter->second.empty()) {
+ dest.append("=");
+ dest.append(iter->second);
+ }
+ }
+
+ ldpp_dout(dpp, 10) << "get_canon_resource(): dest=" << dest << dendl;
+ return dest;
+}
+
+/*
+ * get the header authentication information required to
+ * compute a request's signature
+ */
+void rgw_create_s3_canonical_header(
+ const DoutPrefixProvider *dpp,
+ const char* const method,
+ const char* const content_md5,
+ const char* const content_type,
+ const char* const date,
+ const meta_map_t& meta_map,
+ const meta_map_t& qs_map,
+ const char* const request_uri,
+ const std::map<std::string, std::string>& sub_resources,
+ std::string& dest_str)
+{
+ std::string dest;
+
+ if (method) {
+ dest = method;
+ }
+ dest.append("\n");
+
+ if (content_md5) {
+ dest.append(content_md5);
+ }
+ dest.append("\n");
+
+ if (content_type) {
+ dest.append(content_type);
+ }
+ dest.append("\n");
+
+ if (date) {
+ dest.append(date);
+ }
+ dest.append("\n");
+
+ dest.append(get_canon_amz_hdr(meta_map));
+ dest.append(get_canon_amz_hdr(qs_map));
+ dest.append(get_canon_resource(dpp, request_uri, sub_resources));
+
+ dest_str = dest;
+}
+
+static inline bool is_base64_for_content_md5(unsigned char c) {
+ return (isalnum(c) || isspace(c) || (c == '+') || (c == '/') || (c == '='));
+}
+
+static inline void get_v2_qs_map(const req_info& info,
+ meta_map_t& qs_map) {
+ const auto& params = const_cast<RGWHTTPArgs&>(info.args).get_params();
+ for (const auto& elt : params) {
+ std::string k = boost::algorithm::to_lower_copy(elt.first);
+ if (k.find("x-amz-meta-") == /* offset */ 0) {
+ rgw_add_amz_meta_header(qs_map, k, elt.second);
+ }
+ if (k == "x-amz-security-token") {
+ qs_map[k] = elt.second;
+ }
+ }
+}
+
+/*
+ * get the header authentication information required to
+ * compute a request's signature
+ */
+bool rgw_create_s3_canonical_header(const DoutPrefixProvider *dpp,
+ const req_info& info,
+ utime_t* const header_time,
+ std::string& dest,
+ const bool qsr)
+{
+ const char* const content_md5 = info.env->get("HTTP_CONTENT_MD5");
+ if (content_md5) {
+ for (const char *p = content_md5; *p; p++) {
+ if (!is_base64_for_content_md5(*p)) {
+ ldpp_dout(dpp, 0) << "NOTICE: bad content-md5 provided (not base64),"
+ << " aborting request p=" << *p << " " << (int)*p << dendl;
+ return false;
+ }
+ }
+ }
+
+ const char *content_type = info.env->get("CONTENT_TYPE");
+
+ std::string date;
+ meta_map_t qs_map;
+
+ if (qsr) {
+ get_v2_qs_map(info, qs_map); // handle qs metadata
+ date = info.args.get("Expires");
+ } else {
+ const char *str = info.env->get("HTTP_X_AMZ_DATE");
+ const char *req_date = str;
+ if (str == NULL) {
+ req_date = info.env->get("HTTP_DATE");
+ if (!req_date) {
+ ldpp_dout(dpp, 0) << "NOTICE: missing date for auth header" << dendl;
+ return false;
+ }
+ date = req_date;
+ }
+
+ if (header_time) {
+ struct tm t;
+ uint32_t ns = 0;
+ if (!parse_rfc2616(req_date, &t) && !parse_iso8601(req_date, &t, &ns, false)) {
+ ldpp_dout(dpp, 0) << "NOTICE: failed to parse date <" << req_date << "> for auth header" << dendl;
+ return false;
+ }
+ if (t.tm_year < 70) {
+ ldpp_dout(dpp, 0) << "NOTICE: bad date (predates epoch): " << req_date << dendl;
+ return false;
+ }
+ *header_time = utime_t(internal_timegm(&t), 0);
+ *header_time -= t.tm_gmtoff;
+ }
+ }
+
+ const auto& meta_map = info.x_meta_map;
+ const auto& sub_resources = info.args.get_sub_resources();
+
+ std::string request_uri;
+ if (info.effective_uri.empty()) {
+ request_uri = info.request_uri;
+ } else {
+ request_uri = info.effective_uri;
+ }
+
+ rgw_create_s3_canonical_header(dpp, info.method, content_md5, content_type,
+ date.c_str(), meta_map, qs_map,
+ request_uri.c_str(), sub_resources, dest);
+ return true;
+}
+
+
+namespace rgw::auth::s3 {
+
+bool is_time_skew_ok(time_t t)
+{
+ auto req_tp = ceph::coarse_real_clock::from_time_t(t);
+ auto cur_tp = ceph::coarse_real_clock::now();
+
+ if (std::chrono::abs(cur_tp - req_tp) > RGW_AUTH_GRACE) {
+ dout(10) << "NOTICE: request time skew too big." << dendl;
+ using ceph::operator<<;
+ dout(10) << "req_tp=" << req_tp << ", cur_tp=" << cur_tp << dendl;
+ return false;
+ }
+
+ return true;
+}
+
+static inline int parse_v4_query_string(const req_info& info, /* in */
+ std::string_view& credential, /* out */
+ std::string_view& signedheaders, /* out */
+ std::string_view& signature, /* out */
+ std::string_view& date, /* out */
+ std::string_view& sessiontoken) /* out */
+{
+ /* auth ships with req params ... */
+
+ /* look for required params */
+ credential = info.args.get("x-amz-credential");
+ if (credential.size() == 0) {
+ return -EPERM;
+ }
+
+ date = info.args.get("x-amz-date");
+ struct tm date_t;
+ if (!parse_iso8601(sview2cstr(date).data(), &date_t, nullptr, false)) {
+ return -EPERM;
+ }
+
+ std::string_view expires = info.args.get("x-amz-expires");
+ if (expires.empty()) {
+ return -EPERM;
+ }
+ /* X-Amz-Expires provides the time period, in seconds, for which
+ the generated presigned URL is valid. The minimum value
+ you can set is 1, and the maximum is 604800 (seven days) */
+ time_t exp = atoll(expires.data());
+ if ((exp < 1) || (exp > 7*24*60*60)) {
+ dout(10) << "NOTICE: exp out of range, exp = " << exp << dendl;
+ return -EPERM;
+ }
+ /* handle expiration in epoch time */
+ uint64_t req_sec = (uint64_t)internal_timegm(&date_t);
+ uint64_t now = ceph_clock_now();
+ if (now >= req_sec + exp) {
+ dout(10) << "NOTICE: now = " << now << ", req_sec = " << req_sec << ", exp = " << exp << dendl;
+ return -EPERM;
+ }
+
+ signedheaders = info.args.get("x-amz-signedheaders");
+ if (signedheaders.size() == 0) {
+ return -EPERM;
+ }
+
+ signature = info.args.get("x-amz-signature");
+ if (signature.size() == 0) {
+ return -EPERM;
+ }
+
+ if (info.args.exists("x-amz-security-token")) {
+ sessiontoken = info.args.get("x-amz-security-token");
+ if (sessiontoken.size() == 0) {
+ return -EPERM;
+ }
+ }
+
+ return 0;
+}
+
+static bool get_next_token(const std::string_view& s,
+ size_t& pos,
+ const char* const delims,
+ std::string_view& token)
+{
+ const size_t start = s.find_first_not_of(delims, pos);
+ if (start == std::string_view::npos) {
+ pos = s.size();
+ return false;
+ }
+
+ size_t end = s.find_first_of(delims, start);
+ if (end != std::string_view::npos)
+ pos = end + 1;
+ else {
+ pos = end = s.size();
+ }
+
+ token = s.substr(start, end - start);
+ return true;
+}
+
+template<std::size_t ExpectedStrNum>
+boost::container::small_vector<std::string_view, ExpectedStrNum>
+get_str_vec(const std::string_view& str, const char* const delims)
+{
+ boost::container::small_vector<std::string_view, ExpectedStrNum> str_vec;
+
+ size_t pos = 0;
+ std::string_view token;
+ while (pos < str.size()) {
+ if (get_next_token(str, pos, delims, token)) {
+ if (token.size() > 0) {
+ str_vec.push_back(token);
+ }
+ }
+ }
+
+ return str_vec;
+}
+
+template<std::size_t ExpectedStrNum>
+boost::container::small_vector<std::string_view, ExpectedStrNum>
+get_str_vec(const std::string_view& str)
+{
+ const char delims[] = ";,= \t";
+ return get_str_vec<ExpectedStrNum>(str, delims);
+}
+
+static inline int parse_v4_auth_header(const req_info& info, /* in */
+ std::string_view& credential, /* out */
+ std::string_view& signedheaders, /* out */
+ std::string_view& signature, /* out */
+ std::string_view& date, /* out */
+ std::string_view& sessiontoken, /* out */
+ const DoutPrefixProvider *dpp)
+{
+ std::string_view input(info.env->get("HTTP_AUTHORIZATION", ""));
+ try {
+ input = input.substr(::strlen(AWS4_HMAC_SHA256_STR) + 1);
+ } catch (std::out_of_range&) {
+ /* We should never ever run into this situation as the presence of
+ * AWS4_HMAC_SHA256_STR had been verified earlier. */
+ ldpp_dout(dpp, 10) << "credentials string is too short" << dendl;
+ return -EINVAL;
+ }
+
+ std::map<std::string_view, std::string_view> kv;
+ for (const auto& s : get_str_vec<4>(input, ",")) {
+ const auto parsed_pair = parse_key_value(s);
+ if (parsed_pair) {
+ kv[parsed_pair->first] = parsed_pair->second;
+ } else {
+ ldpp_dout(dpp, 10) << "NOTICE: failed to parse auth header (s=" << s << ")"
+ << dendl;
+ return -EINVAL;
+ }
+ }
+
+ static const std::array<std::string_view, 3> required_keys = {
+ "Credential",
+ "SignedHeaders",
+ "Signature"
+ };
+
+ /* Ensure that the presigned required keys are really there. */
+ for (const auto& k : required_keys) {
+ if (kv.find(k) == std::end(kv)) {
+ ldpp_dout(dpp, 10) << "NOTICE: auth header missing key: " << k << dendl;
+ return -EINVAL;
+ }
+ }
+
+ credential = kv["Credential"];
+ signedheaders = kv["SignedHeaders"];
+ signature = kv["Signature"];
+
+ /* sig hex str */
+ ldpp_dout(dpp, 10) << "v4 signature format = " << signature << dendl;
+
+ /* ------------------------- handle x-amz-date header */
+
+ /* grab date */
+
+ const char *d = info.env->get("HTTP_X_AMZ_DATE");
+
+ struct tm t;
+ if (unlikely(d == NULL)) {
+ d = info.env->get("HTTP_DATE");
+ }
+ if (!d || !parse_iso8601(d, &t, NULL, false)) {
+ ldpp_dout(dpp, 10) << "error reading date via http_x_amz_date and http_date" << dendl;
+ return -EACCES;
+ }
+ date = d;
+
+ if (!is_time_skew_ok(internal_timegm(&t))) {
+ return -ERR_REQUEST_TIME_SKEWED;
+ }
+
+ auto token = info.env->get_optional("HTTP_X_AMZ_SECURITY_TOKEN");
+ if (token) {
+ sessiontoken = *token;
+ }
+
+ return 0;
+}
+
+bool is_non_s3_op(RGWOpType op_type)
+{
+ if (op_type == RGW_STS_GET_SESSION_TOKEN ||
+ op_type == RGW_STS_ASSUME_ROLE ||
+ op_type == RGW_STS_ASSUME_ROLE_WEB_IDENTITY ||
+ op_type == RGW_OP_CREATE_ROLE ||
+ op_type == RGW_OP_DELETE_ROLE ||
+ op_type == RGW_OP_GET_ROLE ||
+ op_type == RGW_OP_MODIFY_ROLE_TRUST_POLICY ||
+ op_type == RGW_OP_LIST_ROLES ||
+ op_type == RGW_OP_PUT_ROLE_POLICY ||
+ op_type == RGW_OP_GET_ROLE_POLICY ||
+ op_type == RGW_OP_LIST_ROLE_POLICIES ||
+ op_type == RGW_OP_DELETE_ROLE_POLICY ||
+ op_type == RGW_OP_PUT_USER_POLICY ||
+ op_type == RGW_OP_GET_USER_POLICY ||
+ op_type == RGW_OP_LIST_USER_POLICIES ||
+ op_type == RGW_OP_DELETE_USER_POLICY ||
+ op_type == RGW_OP_CREATE_OIDC_PROVIDER ||
+ op_type == RGW_OP_DELETE_OIDC_PROVIDER ||
+ op_type == RGW_OP_GET_OIDC_PROVIDER ||
+ op_type == RGW_OP_LIST_OIDC_PROVIDERS ||
+ op_type == RGW_OP_PUBSUB_TOPIC_CREATE ||
+ op_type == RGW_OP_PUBSUB_TOPICS_LIST ||
+ op_type == RGW_OP_PUBSUB_TOPIC_GET ||
+ op_type == RGW_OP_PUBSUB_TOPIC_DELETE ||
+ op_type == RGW_OP_TAG_ROLE ||
+ op_type == RGW_OP_LIST_ROLE_TAGS ||
+ op_type == RGW_OP_UNTAG_ROLE ||
+ op_type == RGW_OP_UPDATE_ROLE) {
+ return true;
+ }
+ return false;
+}
+
+int parse_v4_credentials(const req_info& info, /* in */
+ std::string_view& access_key_id, /* out */
+ std::string_view& credential_scope, /* out */
+ std::string_view& signedheaders, /* out */
+ std::string_view& signature, /* out */
+ std::string_view& date, /* out */
+ std::string_view& session_token, /* out */
+ const bool using_qs, /* in */
+ const DoutPrefixProvider *dpp)
+{
+ std::string_view credential;
+ int ret;
+ if (using_qs) {
+ ret = parse_v4_query_string(info, credential, signedheaders,
+ signature, date, session_token);
+ } else {
+ ret = parse_v4_auth_header(info, credential, signedheaders,
+ signature, date, session_token, dpp);
+ }
+
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* access_key/YYYYMMDD/region/service/aws4_request */
+ ldpp_dout(dpp, 10) << "v4 credential format = " << credential << dendl;
+
+ if (std::count(credential.begin(), credential.end(), '/') != 4) {
+ return -EINVAL;
+ }
+
+ /* credential must end with 'aws4_request' */
+ if (credential.find("aws4_request") == std::string::npos) {
+ return -EINVAL;
+ }
+
+ /* grab access key id */
+ const size_t pos = credential.find("/");
+ access_key_id = credential.substr(0, pos);
+ ldpp_dout(dpp, 10) << "access key id = " << access_key_id << dendl;
+
+ /* grab credential scope */
+ credential_scope = credential.substr(pos + 1);
+ ldpp_dout(dpp, 10) << "credential scope = " << credential_scope << dendl;
+
+ return 0;
+}
+
+string gen_v4_scope(const ceph::real_time& timestamp,
+ const string& region,
+ const string& service)
+{
+
+ auto sec = real_clock::to_time_t(timestamp);
+
+ struct tm bt;
+ gmtime_r(&sec, &bt);
+
+ auto year = 1900 + bt.tm_year;
+ auto mon = bt.tm_mon + 1;
+ auto day = bt.tm_mday;
+
+ return fmt::format(FMT_STRING("{:d}{:02d}{:02d}/{:s}/{:s}/aws4_request"),
+ year, mon, day, region, service);
+}
+
+std::string get_v4_canonical_qs(const req_info& info, const bool using_qs)
+{
+ const std::string *params = &info.request_params;
+ std::string copy_params;
+ if (params->empty()) {
+ /* Optimize the typical flow. */
+ return std::string();
+ }
+ if (params->find_first_of('+') != std::string::npos) {
+ copy_params = *params;
+ boost::replace_all(copy_params, "+", "%20");
+ params = &copy_params;
+ }
+
+ /* Handle case when query string exists. Step 3 described in: http://docs.
+ * aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */
+ std::map<std::string, std::string> canonical_qs_map;
+ for (const auto& s : get_str_vec<5>(*params, "&")) {
+ std::string_view key, val;
+ const auto parsed_pair = parse_key_value(s);
+ if (parsed_pair) {
+ std::tie(key, val) = *parsed_pair;
+ } else {
+ /* Handling a parameter without any value (even the empty one). That's
+ * it, we've encountered something like "this_param&other_param=val"
+ * which is used by S3 for subresources. */
+ key = s;
+ }
+
+ if (using_qs && boost::iequals(key, "X-Amz-Signature")) {
+ /* Preserving the original behaviour of get_v4_canonical_qs() here. */
+ continue;
+ }
+
+ // while awsv4 specs ask for all slashes to be encoded, s3 itself is relaxed
+ // in its implementation allowing non-url-encoded slashes to be present in
+ // presigned urls for instance
+ canonical_qs_map[aws4_uri_recode(key, true)] = aws4_uri_recode(val, true);
+ }
+
+ /* Thanks to the early exist we have the guarantee that canonical_qs_map has
+ * at least one element. */
+ auto iter = std::begin(canonical_qs_map);
+ std::string canonical_qs;
+ canonical_qs.append(iter->first)
+ .append("=", ::strlen("="))
+ .append(iter->second);
+
+ for (iter++; iter != std::end(canonical_qs_map); iter++) {
+ canonical_qs.append("&", ::strlen("&"))
+ .append(iter->first)
+ .append("=", ::strlen("="))
+ .append(iter->second);
+ }
+
+ return canonical_qs;
+}
+
+static void add_v4_canonical_params_from_map(const map<string, string>& m,
+ std::map<string, string> *result,
+ bool is_non_s3_op)
+{
+ for (auto& entry : m) {
+ const auto& key = entry.first;
+ if (key.empty() || (is_non_s3_op && key == "PayloadHash")) {
+ continue;
+ }
+
+ (*result)[aws4_uri_recode(key, true)] = aws4_uri_recode(entry.second, true);
+ }
+}
+
+std::string gen_v4_canonical_qs(const req_info& info, bool is_non_s3_op)
+{
+ std::map<std::string, std::string> canonical_qs_map;
+
+ add_v4_canonical_params_from_map(info.args.get_params(), &canonical_qs_map, is_non_s3_op);
+ add_v4_canonical_params_from_map(info.args.get_sys_params(), &canonical_qs_map, false);
+
+ if (canonical_qs_map.empty()) {
+ return string();
+ }
+
+ /* Thanks to the early exit we have the guarantee that canonical_qs_map has
+ * at least one element. */
+ auto iter = std::begin(canonical_qs_map);
+ std::string canonical_qs;
+ canonical_qs.append(iter->first)
+ .append("=", ::strlen("="))
+ .append(iter->second);
+
+ for (iter++; iter != std::end(canonical_qs_map); iter++) {
+ canonical_qs.append("&", ::strlen("&"))
+ .append(iter->first)
+ .append("=", ::strlen("="))
+ .append(iter->second);
+ }
+
+ return canonical_qs;
+}
+
+std::string get_v4_canonical_method(const req_state* s)
+{
+ /* If this is a OPTIONS request we need to compute the v4 signature for the
+ * intended HTTP method and not the OPTIONS request itself. */
+ if (s->op_type == RGW_OP_OPTIONS_CORS) {
+ const char *cors_method = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_METHOD");
+
+ if (cors_method) {
+ /* Validate request method passed in access-control-request-method is valid. */
+ auto cors_flags = get_cors_method_flags(cors_method);
+ if (!cors_flags) {
+ ldpp_dout(s, 1) << "invalid access-control-request-method header = "
+ << cors_method << dendl;
+ throw -EINVAL;
+ }
+
+ ldpp_dout(s, 10) << "canonical req method = " << cors_method
+ << ", due to access-control-request-method header" << dendl;
+ return cors_method;
+ } else {
+ ldpp_dout(s, 1) << "invalid http options req missing "
+ << "access-control-request-method header" << dendl;
+ throw -EINVAL;
+ }
+ }
+
+ return s->info.method;
+}
+
+boost::optional<std::string>
+get_v4_canonical_headers(const req_info& info,
+ const std::string_view& signedheaders,
+ const bool using_qs,
+ const bool force_boto2_compat)
+{
+ std::map<std::string_view, std::string> canonical_hdrs_map;
+ for (const auto& token : get_str_vec<5>(signedheaders, ";")) {
+ /* TODO(rzarzynski): we'd like to switch to sstring here but it should
+ * get push_back() and reserve() first. */
+ std::string token_env = "HTTP_";
+ token_env.reserve(token.length() + std::strlen("HTTP_") + 1);
+
+ std::transform(std::begin(token), std::end(token),
+ std::back_inserter(token_env), [](const int c) {
+ return c == '-' ? '_' : c == '_' ? '-' : std::toupper(c);
+ });
+
+ if (token_env == "HTTP_CONTENT_LENGTH") {
+ token_env = "CONTENT_LENGTH";
+ } else if (token_env == "HTTP_CONTENT_TYPE") {
+ token_env = "CONTENT_TYPE";
+ }
+ const char* const t = info.env->get(token_env.c_str());
+ if (!t) {
+ dout(10) << "warning env var not available " << token_env.c_str() << dendl;
+ continue;
+ }
+
+ std::string token_value(t);
+ if (token_env == "HTTP_CONTENT_MD5" &&
+ !std::all_of(std::begin(token_value), std::end(token_value),
+ is_base64_for_content_md5)) {
+ dout(0) << "NOTICE: bad content-md5 provided (not base64)"
+ << ", aborting request" << dendl;
+ return boost::none;
+ }
+
+ if (force_boto2_compat && using_qs && token == "host") {
+ std::string_view port = info.env->get("SERVER_PORT", "");
+ std::string_view secure_port = info.env->get("SERVER_PORT_SECURE", "");
+
+ if (!secure_port.empty()) {
+ if (secure_port != "443")
+ token_value.append(":", std::strlen(":"))
+ .append(secure_port.data(), secure_port.length());
+ } else if (!port.empty()) {
+ if (port != "80")
+ token_value.append(":", std::strlen(":"))
+ .append(port.data(), port.length());
+ }
+ }
+
+ canonical_hdrs_map[token] = rgw_trim_whitespace(token_value);
+ }
+
+ std::string canonical_hdrs;
+ for (const auto& header : canonical_hdrs_map) {
+ const std::string_view& name = header.first;
+ std::string value = header.second;
+ boost::trim_all<std::string>(value);
+
+ canonical_hdrs.append(name.data(), name.length())
+ .append(":", std::strlen(":"))
+ .append(value)
+ .append("\n", std::strlen("\n"));
+ }
+ return canonical_hdrs;
+}
+
+static void handle_header(const string& header, const string& val,
+ std::map<std::string, std::string> *canonical_hdrs_map)
+{
+ /* TODO(rzarzynski): we'd like to switch to sstring here but it should
+ * get push_back() and reserve() first. */
+
+ std::string token;
+ token.reserve(header.length());
+
+ if (header == "HTTP_CONTENT_LENGTH") {
+ token = "content-length";
+ } else if (header == "HTTP_CONTENT_TYPE") {
+ token = "content-type";
+ } else {
+ auto start = std::begin(header);
+ if (boost::algorithm::starts_with(header, "HTTP_")) {
+ start += 5; /* len("HTTP_") */
+ }
+
+ std::transform(start, std::end(header),
+ std::back_inserter(token), [](const int c) {
+ return c == '_' ? '-' : std::tolower(c);
+ });
+ }
+
+ (*canonical_hdrs_map)[token] = rgw_trim_whitespace(val);
+}
+
+std::string gen_v4_canonical_headers(const req_info& info,
+ const map<string, string>& extra_headers,
+ string *signed_hdrs)
+{
+ std::map<std::string, std::string> canonical_hdrs_map;
+ for (auto& entry : info.env->get_map()) {
+ handle_header(entry.first, entry.second, &canonical_hdrs_map);
+ }
+ for (auto& entry : extra_headers) {
+ handle_header(entry.first, entry.second, &canonical_hdrs_map);
+ }
+
+ std::string canonical_hdrs;
+ signed_hdrs->clear();
+ for (const auto& header : canonical_hdrs_map) {
+ const auto& name = header.first;
+ std::string value = header.second;
+ boost::trim_all<std::string>(value);
+
+ if (!signed_hdrs->empty()) {
+ signed_hdrs->append(";");
+ }
+ signed_hdrs->append(name);
+
+ canonical_hdrs.append(name.data(), name.length())
+ .append(":", std::strlen(":"))
+ .append(value)
+ .append("\n", std::strlen("\n"));
+ }
+
+ return canonical_hdrs;
+}
+
+/*
+ * create canonical request for signature version 4
+ *
+ * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+ */
+sha256_digest_t
+get_v4_canon_req_hash(CephContext* cct,
+ const std::string_view& http_verb,
+ const std::string& canonical_uri,
+ const std::string& canonical_qs,
+ const std::string& canonical_hdrs,
+ const std::string_view& signed_hdrs,
+ const std::string_view& request_payload_hash,
+ const DoutPrefixProvider *dpp)
+{
+ ldpp_dout(dpp, 10) << "payload request hash = " << request_payload_hash << dendl;
+
+ const auto canonical_req = string_join_reserve("\n",
+ http_verb,
+ canonical_uri,
+ canonical_qs,
+ canonical_hdrs,
+ signed_hdrs,
+ request_payload_hash);
+
+ const auto canonical_req_hash = calc_hash_sha256(canonical_req);
+
+ using sanitize = rgw::crypt_sanitize::log_content;
+ ldpp_dout(dpp, 10) << "canonical request = " << sanitize{canonical_req} << dendl;
+ ldpp_dout(dpp, 10) << "canonical request hash = "
+ << canonical_req_hash << dendl;
+
+ return canonical_req_hash;
+}
+
+/*
+ * create string to sign for signature version 4
+ *
+ * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+ */
+AWSEngine::VersionAbstractor::string_to_sign_t
+get_v4_string_to_sign(CephContext* const cct,
+ const std::string_view& algorithm,
+ const std::string_view& request_date,
+ const std::string_view& credential_scope,
+ const sha256_digest_t& canonreq_hash,
+ const DoutPrefixProvider *dpp)
+{
+ const auto hexed_cr_hash = canonreq_hash.to_str();
+ const std::string_view hexed_cr_hash_str(hexed_cr_hash);
+
+ const auto string_to_sign = string_join_reserve("\n",
+ algorithm,
+ request_date,
+ credential_scope,
+ hexed_cr_hash_str);
+
+ ldpp_dout(dpp, 10) << "string to sign = "
+ << rgw::crypt_sanitize::log_content{string_to_sign}
+ << dendl;
+
+ return string_to_sign;
+}
+
+
+static inline std::tuple<std::string_view, /* date */
+ std::string_view, /* region */
+ std::string_view> /* service */
+parse_cred_scope(std::string_view credential_scope)
+{
+ /* date cred */
+ size_t pos = credential_scope.find("/");
+ const auto date_cs = credential_scope.substr(0, pos);
+ credential_scope = credential_scope.substr(pos + 1);
+
+ /* region cred */
+ pos = credential_scope.find("/");
+ const auto region_cs = credential_scope.substr(0, pos);
+ credential_scope = credential_scope.substr(pos + 1);
+
+ /* service cred */
+ pos = credential_scope.find("/");
+ const auto service_cs = credential_scope.substr(0, pos);
+
+ return std::make_tuple(date_cs, region_cs, service_cs);
+}
+
+static inline std::vector<unsigned char>
+transform_secret_key(const std::string_view& secret_access_key)
+{
+ /* TODO(rzarzynski): switch to constexpr when C++14 becomes available. */
+ static const std::initializer_list<unsigned char> AWS4 { 'A', 'W', 'S', '4' };
+
+ /* boost::container::small_vector might be used here if someone wants to
+ * optimize out even more dynamic allocations. */
+ std::vector<unsigned char> secret_key_utf8;
+ secret_key_utf8.reserve(AWS4.size() + secret_access_key.size());
+ secret_key_utf8.assign(AWS4);
+
+ for (const auto c : secret_access_key) {
+ std::array<unsigned char, MAX_UTF8_SZ> buf;
+ const size_t n = encode_utf8(c, buf.data());
+ secret_key_utf8.insert(std::end(secret_key_utf8),
+ std::begin(buf), std::begin(buf) + n);
+ }
+
+ return secret_key_utf8;
+}
+
+/*
+ * calculate the SigningKey of AWS auth version 4
+ */
+static sha256_digest_t
+get_v4_signing_key(CephContext* const cct,
+ const std::string_view& credential_scope,
+ const std::string_view& secret_access_key,
+ const DoutPrefixProvider *dpp)
+{
+ std::string_view date, region, service;
+ std::tie(date, region, service) = parse_cred_scope(credential_scope);
+
+ const auto utfed_sec_key = transform_secret_key(secret_access_key);
+ const auto date_k = calc_hmac_sha256(utfed_sec_key, date);
+ const auto region_k = calc_hmac_sha256(date_k, region);
+ const auto service_k = calc_hmac_sha256(region_k, service);
+
+ /* aws4_request */
+ const auto signing_key = calc_hmac_sha256(service_k,
+ std::string_view("aws4_request"));
+
+ ldpp_dout(dpp, 10) << "date_k = " << date_k << dendl;
+ ldpp_dout(dpp, 10) << "region_k = " << region_k << dendl;
+ ldpp_dout(dpp, 10) << "service_k = " << service_k << dendl;
+ ldpp_dout(dpp, 10) << "signing_k = " << signing_key << dendl;
+
+ return signing_key;
+}
+
+/*
+ * calculate the AWS signature version 4
+ *
+ * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+ *
+ * srv_signature_t is an alias over Ceph's basic_sstring. We're using
+ * it to keep everything within the stack boundaries instead of doing
+ * dynamic allocations.
+ */
+AWSEngine::VersionAbstractor::server_signature_t
+get_v4_signature(const std::string_view& credential_scope,
+ CephContext* const cct,
+ const std::string_view& secret_key,
+ const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign,
+ const DoutPrefixProvider *dpp)
+{
+ auto signing_key = get_v4_signing_key(cct, credential_scope, secret_key, dpp);
+
+ /* The server-side generated digest for comparison. */
+ const auto digest = calc_hmac_sha256(signing_key, string_to_sign);
+
+ /* TODO(rzarzynski): I would love to see our sstring having reserve() and
+ * the non-const data() variant like C++17's std::string. */
+ using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
+ srv_signature_t signature(srv_signature_t::initialized_later(),
+ digest.SIZE * 2);
+ buf_to_hex(digest.v, digest.SIZE, signature.begin());
+
+ ldpp_dout(dpp, 10) << "generated signature = " << signature << dendl;
+
+ return signature;
+}
+
+AWSEngine::VersionAbstractor::server_signature_t
+get_v2_signature(CephContext* const cct,
+ const std::string& secret_key,
+ const AWSEngine::VersionAbstractor::string_to_sign_t& string_to_sign)
+{
+ if (secret_key.empty()) {
+ throw -EINVAL;
+ }
+
+ const auto digest = calc_hmac_sha1(secret_key, string_to_sign);
+
+ /* 64 is really enough */;
+ char buf[64];
+ const int ret = ceph_armor(std::begin(buf),
+ std::begin(buf) + 64,
+ reinterpret_cast<const char *>(digest.v),
+ reinterpret_cast<const char *>(digest.v + digest.SIZE));
+ if (ret < 0) {
+ ldout(cct, 10) << "ceph_armor failed" << dendl;
+ throw ret;
+ } else {
+ buf[ret] = '\0';
+ using srv_signature_t = AWSEngine::VersionAbstractor::server_signature_t;
+ return srv_signature_t(buf, ret);
+ }
+}
+
+bool AWSv4ComplMulti::ChunkMeta::is_new_chunk_in_stream(size_t stream_pos) const
+{
+ return stream_pos >= (data_offset_in_stream + data_length);
+}
+
+size_t AWSv4ComplMulti::ChunkMeta::get_data_size(size_t stream_pos) const
+{
+ if (stream_pos > (data_offset_in_stream + data_length)) {
+ /* Data in parsing_buf. */
+ return data_length;
+ } else {
+ return data_offset_in_stream + data_length - stream_pos;
+ }
+}
+
+
+/* AWSv4 completers begin. */
+std::pair<AWSv4ComplMulti::ChunkMeta, size_t /* consumed */>
+AWSv4ComplMulti::ChunkMeta::create_next(CephContext* const cct,
+ ChunkMeta&& old,
+ const char* const metabuf,
+ const size_t metabuf_len)
+{
+ std::string_view metastr(metabuf, metabuf_len);
+
+ const size_t semicolon_pos = metastr.find(";");
+ if (semicolon_pos == std::string_view::npos) {
+ ldout(cct, 20) << "AWSv4ComplMulti cannot find the ';' separator"
+ << dendl;
+ throw rgw::io::Exception(EINVAL, std::system_category());
+ }
+
+ char* data_field_end;
+ /* strtoull ignores the "\r\n" sequence after each non-first chunk. */
+ const size_t data_length = std::strtoull(metabuf, &data_field_end, 16);
+ if (data_length == 0 && data_field_end == metabuf) {
+ ldout(cct, 20) << "AWSv4ComplMulti: cannot parse the data size"
+ << dendl;
+ throw rgw::io::Exception(EINVAL, std::system_category());
+ }
+
+ /* Parse the chunk_signature=... part. */
+ const auto signature_part = metastr.substr(semicolon_pos + 1);
+ const size_t eq_sign_pos = signature_part.find("=");
+ if (eq_sign_pos == std::string_view::npos) {
+ ldout(cct, 20) << "AWSv4ComplMulti: cannot find the '=' separator"
+ << dendl;
+ throw rgw::io::Exception(EINVAL, std::system_category());
+ }
+
+ /* OK, we have at least the beginning of a signature. */
+ const size_t data_sep_pos = signature_part.find("\r\n");
+ if (data_sep_pos == std::string_view::npos) {
+ ldout(cct, 20) << "AWSv4ComplMulti: no new line at signature end"
+ << dendl;
+ throw rgw::io::Exception(EINVAL, std::system_category());
+ }
+
+ const auto signature = \
+ signature_part.substr(eq_sign_pos + 1, data_sep_pos - 1 - eq_sign_pos);
+ if (signature.length() != SIG_SIZE) {
+ ldout(cct, 20) << "AWSv4ComplMulti: signature.length() != 64"
+ << dendl;
+ throw rgw::io::Exception(EINVAL, std::system_category());
+ }
+
+ const size_t data_starts_in_stream = \
+ + semicolon_pos + strlen(";") + data_sep_pos + strlen("\r\n")
+ + old.data_offset_in_stream + old.data_length;
+
+ ldout(cct, 20) << "parsed new chunk; signature=" << signature
+ << ", data_length=" << data_length
+ << ", data_starts_in_stream=" << data_starts_in_stream
+ << dendl;
+
+ return std::make_pair(ChunkMeta(data_starts_in_stream,
+ data_length,
+ signature),
+ semicolon_pos + 83);
+}
+
+std::string
+AWSv4ComplMulti::calc_chunk_signature(const std::string& payload_hash) const
+{
+ const auto string_to_sign = string_join_reserve("\n",
+ AWS4_HMAC_SHA256_PAYLOAD_STR,
+ date,
+ credential_scope,
+ prev_chunk_signature,
+ AWS4_EMPTY_PAYLOAD_HASH,
+ payload_hash);
+
+ ldout(cct, 20) << "AWSv4ComplMulti: string_to_sign=\n" << string_to_sign
+ << dendl;
+
+ /* new chunk signature */
+ const auto sig = calc_hmac_sha256(signing_key, string_to_sign);
+ /* FIXME(rzarzynski): std::string here is really unnecessary. */
+ return sig.to_str();
+}
+
+
+bool AWSv4ComplMulti::is_signature_mismatched()
+{
+ /* The validity of previous chunk can be verified only after getting meta-
+ * data of the next one. */
+ const auto payload_hash = calc_hash_sha256_restart_stream(&sha256_hash);
+ const auto calc_signature = calc_chunk_signature(payload_hash);
+
+ if (chunk_meta.get_signature() != calc_signature) {
+ ldout(cct, 20) << "AWSv4ComplMulti: ERROR: chunk signature mismatch"
+ << dendl;
+ ldout(cct, 20) << "AWSv4ComplMulti: declared signature="
+ << chunk_meta.get_signature() << dendl;
+ ldout(cct, 20) << "AWSv4ComplMulti: calculated signature="
+ << calc_signature << dendl;
+
+ return true;
+ } else {
+ prev_chunk_signature = chunk_meta.get_signature();
+ return false;
+ }
+}
+
+size_t AWSv4ComplMulti::recv_chunk(char* const buf, const size_t buf_max, bool& eof)
+{
+ /* Buffer stores only parsed stream. Raw values reflect the stream
+ * we're getting from a client. */
+ size_t buf_pos = 0;
+
+ if (chunk_meta.is_new_chunk_in_stream(stream_pos)) {
+ /* Verify signature of the previous chunk. We aren't doing that for new
+ * one as the procedure requires calculation of payload hash. This code
+ * won't be triggered for the last, zero-length chunk. Instead, is will
+ * be checked in the complete() method. */
+ if (stream_pos >= ChunkMeta::META_MAX_SIZE && is_signature_mismatched()) {
+ throw rgw::io::Exception(ERR_SIGNATURE_NO_MATCH, std::system_category());
+ }
+
+ /* We don't have metadata for this range. This means a new chunk, so we
+ * need to parse a fresh portion of the stream. Let's start. */
+ size_t to_extract = parsing_buf.capacity() - parsing_buf.size();
+ do {
+ const size_t orig_size = parsing_buf.size();
+ parsing_buf.resize(parsing_buf.size() + to_extract);
+ const size_t received = io_base_t::recv_body(parsing_buf.data() + orig_size,
+ to_extract);
+ parsing_buf.resize(parsing_buf.size() - (to_extract - received));
+ if (received == 0) {
+ eof = true;
+ break;
+ }
+
+ stream_pos += received;
+ to_extract -= received;
+ } while (to_extract > 0);
+
+ size_t consumed;
+ std::tie(chunk_meta, consumed) = \
+ ChunkMeta::create_next(cct, std::move(chunk_meta),
+ parsing_buf.data(), parsing_buf.size());
+
+ /* We can drop the bytes consumed during metadata parsing. The remainder
+ * can be chunk's data plus possibly beginning of next chunks' metadata. */
+ parsing_buf.erase(std::begin(parsing_buf),
+ std::begin(parsing_buf) + consumed);
+ }
+
+ size_t stream_pos_was = stream_pos - parsing_buf.size();
+
+ size_t to_extract = \
+ std::min(chunk_meta.get_data_size(stream_pos_was), buf_max);
+ dout(30) << "AWSv4ComplMulti: stream_pos_was=" << stream_pos_was << ", to_extract=" << to_extract << dendl;
+
+ /* It's quite probable we have a couple of real data bytes stored together
+ * with meta-data in the parsing_buf. We need to extract them and move to
+ * the final buffer. This is a trade-off between frontend's read overhead
+ * and memcpy. */
+ if (to_extract > 0 && parsing_buf.size() > 0) {
+ const auto data_len = std::min(to_extract, parsing_buf.size());
+ const auto data_end_iter = std::begin(parsing_buf) + data_len;
+ dout(30) << "AWSv4ComplMulti: to_extract=" << to_extract << ", data_len=" << data_len << dendl;
+
+ std::copy(std::begin(parsing_buf), data_end_iter, buf);
+ parsing_buf.erase(std::begin(parsing_buf), data_end_iter);
+
+ calc_hash_sha256_update_stream(sha256_hash, buf, data_len);
+
+ to_extract -= data_len;
+ buf_pos += data_len;
+ }
+
+ /* Now we can do the bulk read directly from RestfulClient without any extra
+ * buffering. */
+ while (to_extract > 0) {
+ const size_t received = io_base_t::recv_body(buf + buf_pos, to_extract);
+ dout(30) << "AWSv4ComplMulti: to_extract=" << to_extract << ", received=" << received << dendl;
+
+ if (received == 0) {
+ eof = true;
+ break;
+ }
+
+ calc_hash_sha256_update_stream(sha256_hash, buf + buf_pos, received);
+
+ buf_pos += received;
+ stream_pos += received;
+ to_extract -= received;
+ }
+
+ dout(20) << "AWSv4ComplMulti: filled=" << buf_pos << dendl;
+ return buf_pos;
+}
+
+size_t AWSv4ComplMulti::recv_body(char* const buf, const size_t buf_max)
+{
+ bool eof = false;
+ size_t total = 0;
+
+ while (total < buf_max && !eof) {
+ const size_t received = recv_chunk(buf + total, buf_max - total, eof);
+ total += received;
+ }
+ dout(20) << "AWSv4ComplMulti: received=" << total << dendl;
+ return total;
+}
+
+void AWSv4ComplMulti::modify_request_state(const DoutPrefixProvider* dpp, req_state* const s_rw)
+{
+ const char* const decoded_length = \
+ s_rw->info.env->get("HTTP_X_AMZ_DECODED_CONTENT_LENGTH");
+
+ if (!decoded_length) {
+ throw -EINVAL;
+ } else {
+ s_rw->length = decoded_length;
+ s_rw->content_length = parse_content_length(decoded_length);
+
+ if (s_rw->content_length < 0) {
+ ldpp_dout(dpp, 10) << "negative AWSv4's content length, aborting" << dendl;
+ throw -EINVAL;
+ }
+ }
+
+ /* Install the filter over rgw::io::RestfulClient. */
+ AWS_AUTHv4_IO(s_rw)->add_filter(
+ std::static_pointer_cast<io_base_t>(shared_from_this()));
+}
+
+bool AWSv4ComplMulti::complete()
+{
+ /* Now it's time to verify the signature of the last, zero-length chunk. */
+ if (is_signature_mismatched()) {
+ ldout(cct, 10) << "ERROR: signature of last chunk does not match"
+ << dendl;
+ return false;
+ } else {
+ return true;
+ }
+}
+
+rgw::auth::Completer::cmplptr_t
+AWSv4ComplMulti::create(const req_state* const s,
+ std::string_view date,
+ std::string_view credential_scope,
+ std::string_view seed_signature,
+ const boost::optional<std::string>& secret_key)
+{
+ if (!secret_key) {
+ /* Some external authorizers (like Keystone) aren't fully compliant with
+ * AWSv4. They do not provide the secret_key which is necessary to handle
+ * the streamed upload. */
+ throw -ERR_NOT_IMPLEMENTED;
+ }
+
+ const auto signing_key = \
+ rgw::auth::s3::get_v4_signing_key(s->cct, credential_scope, *secret_key, s);
+
+ return std::make_shared<AWSv4ComplMulti>(s,
+ std::move(date),
+ std::move(credential_scope),
+ std::move(seed_signature),
+ signing_key);
+}
+
+size_t AWSv4ComplSingle::recv_body(char* const buf, const size_t max)
+{
+ const auto received = io_base_t::recv_body(buf, max);
+ calc_hash_sha256_update_stream(sha256_hash, buf, received);
+
+ return received;
+}
+
+void AWSv4ComplSingle::modify_request_state(const DoutPrefixProvider* dpp, req_state* const s_rw)
+{
+ /* Install the filter over rgw::io::RestfulClient. */
+ AWS_AUTHv4_IO(s_rw)->add_filter(
+ std::static_pointer_cast<io_base_t>(shared_from_this()));
+}
+
+bool AWSv4ComplSingle::complete()
+{
+ /* The completer is only for the cases where signed payload has been
+ * requested. It won't be used, for instance, during the query string-based
+ * authentication. */
+ const auto payload_hash = calc_hash_sha256_close_stream(&sha256_hash);
+
+ /* Validate x-amz-sha256 */
+ if (payload_hash.compare(expected_request_payload_hash) == 0) {
+ return true;
+ } else {
+ ldout(cct, 10) << "ERROR: x-amz-content-sha256 does not match"
+ << dendl;
+ ldout(cct, 10) << "ERROR: grab_aws4_sha256_hash()="
+ << payload_hash << dendl;
+ ldout(cct, 10) << "ERROR: expected_request_payload_hash="
+ << expected_request_payload_hash << dendl;
+ return false;
+ }
+}
+
+AWSv4ComplSingle::AWSv4ComplSingle(const req_state* const s)
+ : io_base_t(nullptr),
+ cct(s->cct),
+ expected_request_payload_hash(get_v4_exp_payload_hash(s->info)),
+ sha256_hash(calc_hash_sha256_open_stream()) {
+}
+
+rgw::auth::Completer::cmplptr_t
+AWSv4ComplSingle::create(const req_state* const s,
+ const boost::optional<std::string>&)
+{
+ return std::make_shared<AWSv4ComplSingle>(s);
+}
+
+} // namespace rgw::auth::s3