#include "rgw_auth_s3.h"
#include "common/ceph_crypto_cms.h"
-#include "common/armor.h"
+#include "common/ceph_crypto.h"
#include "common/Cond.h"
#define dout_subsys ceph_subsys_rgw
return std::make_pair(std::move(token_envelope), 0);
}
+std::string EC2Engine::sign(const DoutPrefixProvider* dpp,
+ const std::string& secret,
+ const std::string& string_to_sign) const
+{
+ using ceph::crypto::HMACSHA256;
+
+ /*
+ The extra data needed for the signing is included in the clear in the string_to_sign
+ so we really need to extract it.
+ */
+
+ std::vector<std::string> lines;
+ boost::split(lines, string_to_sign, boost::is_any_of("\r\n"));
+
+ if (lines.size() < 1) {
+ ldpp_dout(dpp, 0) << "String to sign is not in any recognisable format" << dendl;
+ throw -ERR_SIGNATURE_NO_MATCH;
+ }
+
+ if (lines[0] != "AWS4-HMAC-SHA256") {
+ ldpp_dout(dpp, 0) << "Signature format " << lines[0] << "cannot be handled internally" << dendl;
+ throw -ERR_SIGNATURE_NO_MATCH;
+ }
+
+ if (lines.size() < 4) {
+ ldpp_dout(dpp, 0) << "String to sign does not contain all necessary data for AWS4-HMAC-SHA256 signing mode" << dendl;
+ throw -ERR_SIGNATURE_NO_MATCH;
+ }
+ std::string& credential_scope = lines[2];
+
+ std::vector<std::string> scope_data;
+ boost::split(scope_data, credential_scope, boost::is_any_of("/"));
+
+ if (scope_data.size() < 4) {
+ ldpp_dout(dpp, 0) << "String to sign does not contain all necessary credential scope data for AWS4-HMAC-SHA256 signing mode" << dendl;
+ throw -ERR_SIGNATURE_NO_MATCH;
+ }
+
+ std::string& timestamp = scope_data[0];
+ std::string& region_name = scope_data[1];
+ std::string& service_name = scope_data[2];
+ std::string& command_name = scope_data[3];
+
+ if (command_name != "aws4_request") {
+ ldpp_dout(dpp, 0) << "credential_scope is not for aws4_request when using AWS4-HMAC-SHA256 signing mode" << dendl;
+ throw -ERR_SIGNATURE_NO_MATCH;
+ }
+
+ /* With these data we can now construct the signing key */
+ std::string key = std::string("AWS4") + secret;
+
+ unsigned char date_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+ HMACSHA256 date_hash((const unsigned char *)key.c_str(), key.size());
+ date_hash.Update((const unsigned char *)timestamp.c_str(), timestamp.size());
+ date_hash.Final(date_digest);
+
+ unsigned char region_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+ HMACSHA256 region_hash(date_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+ region_hash.Update((const unsigned char *)region_name.c_str(), region_name.size());
+ region_hash.Final(region_digest);
+
+ unsigned char service_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+ HMACSHA256 service_hash(region_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+ service_hash.Update((const unsigned char *)service_name.c_str(), service_name.size());
+ service_hash.Final(service_digest);
+
+ unsigned char command_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+ HMACSHA256 command_hash(service_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+ command_hash.Update((const unsigned char *)"aws4_request", 12);
+ command_hash.Final(command_digest);
+
+ /* And sign the data with it */
+ unsigned char signature[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+ HMACSHA256 hash(command_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+ hash.Update((const unsigned char *)string_to_sign.c_str(), string_to_sign.size());
+ hash.Final(signature);
+
+ std::stringstream ss;
+ for (int i=0; i < CEPH_CRYPTO_HMACSHA256_DIGESTSIZE; i++) {
+ ss << std::setfill('0') << std::setw(2) << std::hex << (int)signature[i];
+ }
+
+ return ss.str();
+}
+
+std::pair<boost::optional<std::string>, int> EC2Engine::get_secret_from_keystone(const DoutPrefixProvider* dpp,
+ const std::string& user_id,
+ const boost::string_view& access_key_id) const
+{
+ /* Fetch from /users/{USER_ID}/credentials/OS-EC2/{ACCESS_KEY_ID} */
+ /* Should return json with response key "credential" which contains entry "secret"*/
+
+ /* prepare keystone url */
+ std::string keystone_url = config.get_endpoint_url();
+ if (keystone_url.empty()) {
+ return make_pair(boost::none, -EINVAL);
+ }
+
+ const auto api_version = config.get_api_version();
+ if (config.get_api_version() == rgw::keystone::ApiVersion::VER_3) {
+ keystone_url.append("v3/");
+ } else {
+ keystone_url.append("v2.0/");
+ }
+ keystone_url.append("users/");
+ keystone_url.append(user_id);
+ keystone_url.append("/credentials/OS-EC2/");
+ keystone_url.append(access_key_id.to_string());
+
+ /* get authentication token for Keystone. */
+ std::string admin_token;
+ int ret = rgw::keystone::Service::get_admin_token(cct, token_cache, config,
+ admin_token);
+ if (ret < 0) {
+ ldpp_dout(dpp, 2) << "s3 keystone: cannot get token for keystone access"
+ << dendl;
+ return make_pair(boost::none, ret);
+ }
+
+ using RGWGetAccessSecret
+ = rgw::keystone::Service::RGWKeystoneHTTPTransceiver;
+
+ /* The container for plain response obtained from Keystone.*/
+ ceph::bufferlist token_body_bl;
+ RGWGetAccessSecret secret(cct, "GET", keystone_url, &token_body_bl);
+
+ /* set required headers for keystone request */
+ secret.append_header("X-Auth-Token", admin_token);
+
+ /* check if we want to verify keystone's ssl certs */
+ secret.set_verify_ssl(cct->_conf->rgw_keystone_verify_ssl);
+
+ /* send request */
+ ret = secret.process();
+ if (ret < 0) {
+ ldpp_dout(dpp, 2) << "s3 keystone: secret fetching error: "
+ << token_body_bl.c_str() << dendl;
+ return make_pair(boost::none, ret);
+ }
+
+ /* if the supplied signature is wrong, we will get 401 from Keystone */
+ if (secret.get_http_status() ==
+ decltype(secret)::HTTP_STATUS_NOTFOUND) {
+ return make_pair(boost::none, -EINVAL);
+ }
+
+ /* now parse response */
+
+ JSONParser parser;
+ if (! parser.parse(token_body_bl.c_str(), token_body_bl.length())) {
+ ldpp_dout(dpp, 0) << "Keystone credential parse error: malformed json" << dendl;
+ return make_pair(boost::none, -EINVAL);
+ }
+
+ JSONObjIter credential_iter = parser.find_first("credential");
+ std::string secret_string;
+
+ try {
+ if (!credential_iter.end()) {
+ JSONDecoder::decode_json("secret", secret_string, *credential_iter, true);
+ } else {
+ ldpp_dout(dpp, 0) << "Keystone credential not present in return from server" << dendl;
+ return make_pair(boost::none, -EINVAL);
+ }
+ } catch (JSONDecoder::err& err) {
+ ldpp_dout(dpp, 0) << "Keystone credential parse error: " << err.message << dendl;
+ return make_pair(boost::none, -EINVAL);
+ }
+
+ return make_pair(secret_string, 0);
+}
+
+/*
+ * Try to get a token for S3 authentication, using a secret cache if available
+ */
+std::pair<boost::optional<rgw::keystone::TokenEnvelope>, int>
+EC2Engine::get_access_token(const DoutPrefixProvider* dpp,
+ const boost::string_view& access_key_id,
+ const std::string& string_to_sign,
+ const boost::string_view& signature) const
+{
+ boost::optional<rgw::keystone::TokenEnvelope> token;
+ int failure_reason;
+
+ /* Get a token from the cache if one has already been stored */
+ boost::optional<boost::tuple<rgw::keystone::TokenEnvelope, std::string>>
+ t = secret_cache.find(access_key_id.to_string());
+
+ /* Check that credentials can correctly be used to sign data */
+ if (t) {
+ std::string sig(signature);
+ std::string calculated_signature = sign(dpp, t->get<1>(), string_to_sign);
+ if (sig == calculated_signature) {
+ return std::make_pair(t->get<0>(), 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;
+ }
+
+ /* No cached token, token expired, or secret invalid: fall back to keystone */
+ std::tie(token, failure_reason) = get_from_keystone(dpp, access_key_id, string_to_sign, signature);
+
+ if (token) {
+ /* Fetch secret from keystone for the access_key_id */
+ boost::optional<std::string> secret;
+ std::tie(secret, failure_reason) = get_secret_from_keystone(dpp, token->get_user_id(), access_key_id);
+
+ if (secret) {
+ /* Add token, secret pair to cache, and set timeout */
+ secret_cache.add(access_key_id.to_string(), *token, *secret);
+ }
+ }
+
+ return std::make_pair(token, failure_reason);
+}
+
EC2Engine::acl_strategy_t
EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t&) const
{
boost::optional<token_envelope_t> t;
int failure_reason;
std::tie(t, failure_reason) = \
- get_from_keystone(dpp, access_key_id, string_to_sign, signature);
+ get_access_token(dpp, access_key_id, string_to_sign, signature);
if (! t) {
return result_t::deny(failure_reason);
}
}
}
+bool SecretCache::find(const std::string& token_id,
+ SecretCache::token_envelope_t& token,
+ std::string &secret)
+{
+ Mutex::Locker l(lock);
+
+ map<std::string, secret_entry>::iterator iter = secrets.find(token_id);
+ if (iter == secrets.end()) {
+ return false;
+ }
+
+ secret_entry& entry = iter->second;
+ secrets_lru.erase(entry.lru_iter);
+
+ const uint64_t now = ceph_clock_now().sec();
+ if (entry.token.expired() || now > entry.expires) {
+ secrets.erase(iter);
+ return false;
+ }
+ token = entry.token;
+ secret = entry.secret;
+
+ secrets_lru.push_front(token_id);
+ entry.lru_iter = secrets_lru.begin();
+
+ return true;
+}
+
+void SecretCache::add(const std::string& token_id,
+ const SecretCache::token_envelope_t& token,
+ const std::string& secret)
+{
+ Mutex::Locker l(lock);
+
+ map<string, secret_entry>::iterator iter = secrets.find(token_id);
+ if (iter != secrets.end()) {
+ secret_entry& e = iter->second;
+ secrets_lru.erase(e.lru_iter);
+ }
+
+ const uint64_t now = ceph_clock_now().sec();
+ secrets_lru.push_front(token_id);
+ secret_entry& entry = secrets[token_id];
+ entry.token = token;
+ entry.secret = secret;
+ entry.expires = now + s3_token_expiry_length;
+ entry.lru_iter = secrets_lru.begin();
+
+ while (secrets_lru.size() > max) {
+ list<string>::reverse_iterator riter = secrets_lru.rbegin();
+ iter = secrets.find(*riter);
+ assert(iter != secrets.end());
+ secrets.erase(iter);
+ secrets_lru.pop_back();
+ }
+}
+
}; /* namespace keystone */
}; /* namespace auth */
}; /* namespace rgw */
}
}; /* class TokenEngine */
+class SecretCache {
+ using token_envelope_t = rgw::keystone::TokenEnvelope;
+
+ struct secret_entry {
+ token_envelope_t token;
+ std::string secret;
+ uint64_t expires;
+ list<std::string>::iterator lru_iter;
+ };
+
+ const boost::intrusive_ptr<CephContext> cct;
+
+ std::map<std::string, secret_entry> secrets;
+ std::list<std::string> secrets_lru;
+
+ Mutex lock;
+
+ const size_t max;
+
+ const uint64_t s3_token_expiry_length;
+
+ SecretCache()
+ : cct(g_ceph_context),
+ lock("rgw::auth::keystone::SecretCache"),
+ max(cct->_conf->rgw_keystone_token_cache_size),
+ s3_token_expiry_length(300) {
+ }
+
+ ~SecretCache() {}
+
+public:
+ SecretCache(const SecretCache&) = delete;
+ void operator=(const SecretCache&) = delete;
+
+ static SecretCache& get_instance() {
+ /* In C++11 this is thread safe. */
+ static SecretCache instance;
+ return instance;
+ }
+
+ bool find(const std::string& token_id, token_envelope_t& token, std::string& secret);
+ boost::optional<boost::tuple<token_envelope_t, std::string>> find(const std::string& token_id) {
+ token_envelope_t token_envlp;
+ std::string secret;
+ if (find(token_id, token_envlp, secret)) {
+ return boost::make_tuple(token_envlp, secret);
+ }
+ return boost::none;
+ }
+ void add(const std::string& token_id, const token_envelope_t& token, const std::string& secret);
+}; /* class SecretCache */
class EC2Engine : public rgw::auth::s3::AWSEngine {
using acl_strategy_t = rgw::auth::RemoteApplier::acl_strategy_t;
const rgw::auth::RemoteApplier::Factory* const apl_factory;
rgw::keystone::Config& config;
rgw::keystone::TokenCache& token_cache;
+ rgw::auth::keystone::SecretCache& secret_cache;
/* Helper methods. */
acl_strategy_t get_acl_strategy(const token_envelope_t& token) const;
const std::vector<std::string>& admin_roles
) const noexcept;
std::pair<boost::optional<token_envelope_t>, int>
- get_from_keystone(const DoutPrefixProvider* dpp, const boost::string_view& access_key_id,
+ get_from_keystone(const DoutPrefixProvider* dpp,
+ const boost::string_view& access_key_id,
const std::string& string_to_sign,
const boost::string_view& signature) const;
+ std::pair<boost::optional<token_envelope_t>, int>
+ get_access_token(const DoutPrefixProvider* dpp,
+ const boost::string_view& access_key_id,
+ const std::string& string_to_sign,
+ const boost::string_view& signature) const;
result_t authenticate(const DoutPrefixProvider* dpp,
const boost::string_view& access_key_id,
const boost::string_view& signature,
const signature_factory_t&,
const completer_factory_t& completer_factory,
const req_state* s) const override;
+ std::string sign(const DoutPrefixProvider* dpp, const std::string& secret, const std::string& string_to_sign) const;
+ std::pair<boost::optional<std::string>, int> get_secret_from_keystone(const DoutPrefixProvider* dpp,
+ const std::string& user_id,
+ const boost::string_view& access_key_id) const;
public:
EC2Engine(CephContext* const cct,
const rgw::auth::s3::AWSEngine::VersionAbstractor* const ver_abstractor,
/* The token cache is used ONLY for the retrieving admin token.
* Due to the architecture of AWS Auth S3 credentials cannot be
* cached at all. */
- rgw::keystone::TokenCache& token_cache)
+ rgw::keystone::TokenCache& token_cache,
+ rgw::auth::keystone::SecretCache& secret_cache)
: AWSEngine(cct, *ver_abstractor),
apl_factory(apl_factory),
config(config),
- token_cache(token_cache) {
+ token_cache(token_cache),
+ secret_cache(secret_cache) {
}
using AWSEngine::authenticate;