From d0f26c3e07dcdc16d10cd9cc4820db959b216784 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Tue, 6 Feb 2024 07:50:55 +0000 Subject: [PATCH] rgw/auth: ignoring signatures for HTTP OPTIONS calls Before [1] we always sent all HTTP OPTIONS requests to the S3AnonymousEngine and ignored any provided AWSv4 credentials sent in the request. That PR changed so that if we got credentials in the request we instead sent it through the authentication code in order to solve HTTP OPTIONS requests on tenanted users to start working (because we need to resolve the tenant, also called bucket tenant in the code, and we can't only rely on the bucket name since it will not be found). We solved this by modifying the canonical HTTP method used when calculating the AWSv4 signature by instead using the access-control-request-method header which worked good. This change did not take into account that when you generated a presigned URL for a put_object request you can also pass in extra parameters like a canned ACL [2] to the Params variable in for example boto3's generated_presigned_url(). Doing that will cause the client to add the x-amz-acl header to x-amz-signedheaders and also use that in their signature calculation. When doing a HTTP OPTIONS calls for CORS on that presigned URL the browser will never send a x-amz-acl header with the correct data since that is something that the actual PUT request should include later, so that HTTP OPTIONS call should pass even though the signature can never be calculated correctly server-side like verified against AWS S3 in tracker [3]. This patch as a result skips the signature calculation when doing EC2 auth using the LocalEngine but we still need to pass the request there in order to lookup the user to support buckets in a tenant. For the Keystone EC2 auth we're pretty out of luck in the sense that Keystone's API itself requires us to send the AWSv4 signature in the request with the access_key in order to obtain a token, and we cannot leave the signature out, we also cannot spoof the signature from rgw -> keystone since we don't have access to the secret_key if it's not in our cache. For that approach we simply pass on to get_access_token() that if it's an HTTP OPTIONS and we find the access_key in the cache we pull that and ignore verifying signature and pass it on for validation. This means that the cache must be warm if using Keystone auth and adding extra params to a presigned URL. This partly makes some of the commits in [1] redundant for EC2 LocalEngine auth but we still need it for tenanted bucket support. [1] https://github.com/ceph/ceph/pull/52673 [2] https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl [3] https://tracker.ceph.com/issues/64308 Fixes: https://tracker.ceph.com/issues/64308 Signed-off-by: Tobias Urdin (cherry picked from commit fe15b52edb5d228d2ed56679c62cf48493ae2d54) Conflicts: no optional_yield argument in quincy src/rgw/rgw_auth_keystone.cc src/rgw/rgw_auth_keystone.h --- src/rgw/rgw_auth_keystone.cc | 25 ++++++++++++++++++------- src/rgw/rgw_auth_keystone.h | 3 ++- src/rgw/rgw_rest_s3.cc | 7 +++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/rgw/rgw_auth_keystone.cc b/src/rgw/rgw_auth_keystone.cc index e6f4857cdef46..519f307e98747 100644 --- a/src/rgw/rgw_auth_keystone.cc +++ b/src/rgw/rgw_auth_keystone.cc @@ -571,7 +571,8 @@ auto EC2Engine::get_access_token(const DoutPrefixProvider* dpp, const std::string_view& access_key_id, const std::string& string_to_sign, const std::string_view& signature, - const signature_factory_t& signature_factory) const + const signature_factory_t& signature_factory, + bool ignore_signature) const -> access_token_result { using server_signature_t = VersionAbstractor::server_signature_t; @@ -585,12 +586,19 @@ auto EC2Engine::get_access_token(const DoutPrefixProvider* dpp, /* Check that credentials can correctly be used to sign data */ if (t) { - std::string sig(signature); - server_signature_t server_signature = signature_factory(cct, t->get<1>(), string_to_sign); - if (sig.compare(server_signature) == 0) { + /* We should ignore checking signature in cache if caller tells us to which + * means we're handling a HTTP OPTIONS call. */ + if (ignore_signature) { + ldpp_dout(dpp, 20) << "ignore_signature set and found in cache" << dendl; return {t->get<0>(), t->get<1>(), 0}; } else { - ldpp_dout(dpp, 0) << "Secret string does not correctly sign payload, cache miss" << dendl; + std::string sig(signature); + server_signature_t server_signature = signature_factory(cct, t->get<1>(), string_to_sign); + if (sig.compare(server_signature) == 0) { + return {t->get<0>(), t->get<1>(), 0}; + } else { + ldpp_dout(dpp, 0) << "Secret string does not correctly sign payload, cache miss" << dendl; + } } } else { ldpp_dout(dpp, 0) << "No stored secret string, cache miss" << dendl; @@ -662,7 +670,6 @@ rgw::auth::Engine::result_t EC2Engine::authenticate( const string_to_sign_t& string_to_sign, const signature_factory_t& signature_factory, const completer_factory_t& completer_factory, - /* Passthorugh only! */ const req_state* s, optional_yield y) const { @@ -681,8 +688,12 @@ rgw::auth::Engine::result_t EC2Engine::authenticate( std::vector admin; } accepted_roles(cct); + /* When we handle a HTTP OPTIONS call we must ignore the signature */ + bool ignore_signature = (s->op_type == RGW_OP_OPTIONS_CORS); + auto [t, secret_key, failure_reason] = - get_access_token(dpp, access_key_id, string_to_sign, signature, signature_factory); + get_access_token(dpp, access_key_id, string_to_sign, + signature, signature_factory, ignore_signature); if (! t) { if (failure_reason == -ERR_SIGNATURE_NO_MATCH) { // we looked up a secret but it didn't generate the same signature as diff --git a/src/rgw/rgw_auth_keystone.h b/src/rgw/rgw_auth_keystone.h index de1a108747c18..111f44f29dc12 100644 --- a/src/rgw/rgw_auth_keystone.h +++ b/src/rgw/rgw_auth_keystone.h @@ -161,7 +161,8 @@ class EC2Engine : public rgw::auth::s3::AWSEngine { const std::string_view& access_key_id, const std::string& string_to_sign, const std::string_view& signature, - const signature_factory_t& signature_factory) const; + const signature_factory_t& signature_factory, + bool ignore_signature) const; result_t authenticate(const DoutPrefixProvider* dpp, const std::string_view& access_key_id, const std::string_view& signature, diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc index 99590bdaaa598..4f91da033ff3a 100644 --- a/src/rgw/rgw_rest_s3.cc +++ b/src/rgw/rgw_rest_s3.cc @@ -6180,6 +6180,13 @@ rgw::auth::s3::LocalEngine::authenticate( } const RGWAccessKey& k = iter->second; + /* Ignore signature for HTTP OPTIONS */ + if (s->op_type == RGW_OP_OPTIONS_CORS) { + auto apl = apl_factory->create_apl_local(cct, s, user->get_info(), + k.subuser, std::nullopt, access_key_id); + return result_t::grant(std::move(apl), completer_factory(k.key)); + } + const VersionAbstractor::server_signature_t server_signature = \ signature_factory(cct, k.key, string_to_sign); auto compare = signature.compare(server_signature); -- 2.39.5