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