diff --git a/alternator/auth.cc b/alternator/auth.cc index 41b583cf64..7e18b0ffcb 100644 --- a/alternator/auth.cc +++ b/alternator/auth.cc @@ -10,8 +10,6 @@ #include "log.hh" #include #include -#include -#include "utils/hashers.hh" #include "bytes.hh" #include "alternator/auth.hh" #include @@ -29,99 +27,6 @@ namespace alternator { static logging::logger alogger("alternator-auth"); -static hmac_sha256_digest hmac_sha256(std::string_view key, std::string_view msg) { - hmac_sha256_digest digest; - int ret = gnutls_hmac_fast(GNUTLS_MAC_SHA256, key.data(), key.size(), msg.data(), msg.size(), digest.data()); - if (ret) { - throw std::runtime_error(fmt::format("Computing HMAC failed ({}): {}", ret, gnutls_strerror(ret))); - } - return digest; -} - -static hmac_sha256_digest get_signature_key(std::string_view key, std::string_view date_stamp, std::string_view region_name, std::string_view service_name) { - auto date = hmac_sha256("AWS4" + std::string(key), date_stamp); - auto region = hmac_sha256(std::string_view(date.data(), date.size()), region_name); - auto service = hmac_sha256(std::string_view(region.data(), region.size()), service_name); - auto signing = hmac_sha256(std::string_view(service.data(), service.size()), "aws4_request"); - return signing; -} - -static std::string apply_sha256(std::string_view msg) { - sha256_hasher hasher; - hasher.update(msg.data(), msg.size()); - return to_hex(hasher.finalize()); -} - -static std::string apply_sha256(const std::vector>& msg) { - sha256_hasher hasher; - for (const temporary_buffer& buf : msg) { - hasher.update(buf.get(), buf.size()); - } - return to_hex(hasher.finalize()); -} - -static std::string format_time_point(db_clock::time_point tp) { - time_t time_point_repr = db_clock::to_time_t(tp); - std::string time_point_str; - time_point_str.resize(17); - ::tm time_buf; - // strftime prints the terminating null character as well - std::strftime(time_point_str.data(), time_point_str.size(), "%Y%m%dT%H%M%SZ", ::gmtime_r(&time_point_repr, &time_buf)); - time_point_str.resize(16); - return time_point_str; -} - -void check_expiry(std::string_view signature_date) { - //FIXME: The default 15min can be changed with X-Amz-Expires header - we should honor it - std::string expiration_str = format_time_point(db_clock::now() - 15min); - std::string validity_str = format_time_point(db_clock::now() + 15min); - if (signature_date < expiration_str) { - throw api_error::invalid_signature( - fmt::format("Signature expired: {} is now earlier than {} (current time - 15 min.)", - signature_date, expiration_str)); - } - if (signature_date > validity_str) { - throw api_error::invalid_signature( - fmt::format("Signature not yet current: {} is still later than {} (current time + 15 min.)", - signature_date, validity_str)); - } -} - -std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method, - std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map& signed_headers_map, - const std::vector>& body_content, std::string_view region, std::string_view service, std::string_view query_string) { - auto amz_date_it = signed_headers_map.find("x-amz-date"); - if (amz_date_it == signed_headers_map.end()) { - throw api_error::invalid_signature("X-Amz-Date header is mandatory for signature verification"); - } - std::string_view amz_date = amz_date_it->second; - check_expiry(amz_date); - std::string_view datestamp = amz_date.substr(0, 8); - if (datestamp != orig_datestamp) { - throw api_error::invalid_signature( - format("X-Amz-Date date does not match the provided datestamp. Expected {}, got {}", - orig_datestamp, datestamp)); - } - std::string_view canonical_uri = "/"; - - std::stringstream canonical_headers; - for (const auto& header : signed_headers_map) { - canonical_headers << fmt::format("{}:{}", header.first, header.second) << '\n'; - } - - std::string payload_hash = apply_sha256(body_content); - std::string canonical_request = fmt::format("{}\n{}\n{}\n{}\n{}\n{}", method, canonical_uri, query_string, canonical_headers.str(), signed_headers_str, payload_hash); - - std::string_view algorithm = "AWS4-HMAC-SHA256"; - std::string credential_scope = fmt::format("{}/{}/{}/aws4_request", datestamp, region, service); - std::string string_to_sign = fmt::format("{}\n{}\n{}\n{}", algorithm, amz_date, credential_scope, apply_sha256(canonical_request)); - - hmac_sha256_digest signing_key = get_signature_key(secret_access_key, datestamp, region, service); - hmac_sha256_digest signature = hmac_sha256(std::string_view(signing_key.data(), signing_key.size()), string_to_sign); - - return to_hex(bytes_view(reinterpret_cast(signature.data()), signature.size())); -} - future get_key_from_roles(service::storage_proxy& proxy, std::string username) { schema_ptr schema = proxy.data_dictionary().find_schema("system_auth", "roles"); partition_key pk = partition_key::from_single_value(*schema, utf8_type->decompose(username)); diff --git a/alternator/auth.hh b/alternator/auth.hh index 2ab0d98605..a8edb71ea3 100644 --- a/alternator/auth.hh +++ b/alternator/auth.hh @@ -20,14 +20,8 @@ class storage_proxy; namespace alternator { -using hmac_sha256_digest = std::array; - using key_cache = utils::loading_cache; -std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method, - std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map& signed_headers_map, - const std::vector>& body_content, std::string_view region, std::string_view service, std::string_view query_string); - future get_key_from_roles(service::storage_proxy& proxy, std::string username); } diff --git a/alternator/server.cc b/alternator/server.cc index 6855e6da40..7515f9896e 100644 --- a/alternator/server.cc +++ b/alternator/server.cc @@ -24,6 +24,7 @@ #include "gms/gossiper.hh" #include "utils/overloaded_functor.hh" #include "utils/fb_utilities.hh" +#include "utils/aws_sigv4.hh" static logging::logger slogger("alternator-server"); @@ -319,8 +320,13 @@ future server::verify_signature(const request& req, const chunked_c region = std::move(region), service = std::move(service), user_signature = std::move(user_signature)] (key_cache::value_ptr key_ptr) { - std::string signature = get_signature(user, *key_ptr, std::string_view(host), req._method, + std::string signature; + try { + signature = utils::aws::get_signature(user, *key_ptr, std::string_view(host), req._method, datestamp, signed_headers_str, signed_headers_map, content, region, service, ""); + } catch (const std::exception& e) { + throw api_error::invalid_signature(e.what()); + } if (signature != std::string_view(user_signature)) { _key_cache.remove(user); diff --git a/configure.py b/configure.py index 8e8ffbe39c..9d8cf5c4bc 100755 --- a/configure.py +++ b/configure.py @@ -1002,6 +1002,7 @@ scylla_core = (['message/messaging_service.cc', 'tombstone_gc.cc', 'utils/disk-error-handler.cc', 'utils/hashers.cc', + 'utils/aws_sigv4.cc', 'duration.cc', 'vint-serialization.cc', 'utils/arch/powerpc/crc32-vpmsum/crc32_wrapper.cc', @@ -1902,6 +1903,8 @@ with open(buildfile, 'w') as f: local_libs += ' ' + maybe_static(args.staticboost, '-lboost_unit_test_framework') if binary not in tests_not_using_seastar_test_framework: local_libs += ' ' + "$seastar_testing_libs_{}".format(mode) + else: + local_libs += ' ' + '-lgnutls' # Our code's debugging information is huge, and multiplied # by many tests yields ridiculous amounts of disk space. # So we strip the tests by default; The user can very diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 31e0cc76f5..23dbe34859 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -42,7 +42,8 @@ target_sources(utils to_string.cc updateable_value.cc utf8.cc - uuid.cc) + uuid.cc + aws_sigv4.cc) target_include_directories(utils PUBLIC ${CMAKE_SOURCE_DIR} diff --git a/utils/aws_sigv4.cc b/utils/aws_sigv4.cc new file mode 100644 index 0000000000..ae495d30d9 --- /dev/null +++ b/utils/aws_sigv4.cc @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023-present ScyllaDB + */ + +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include +#include "utils/aws_sigv4.hh" +#include "utils/hashers.hh" +#include "db_clock.hh" + +using namespace std::chrono_literals; + +namespace utils { +namespace aws { + +static hmac_sha256_digest hmac_sha256(std::string_view key, std::string_view msg) { + hmac_sha256_digest digest; + int ret = gnutls_hmac_fast(GNUTLS_MAC_SHA256, key.data(), key.size(), msg.data(), msg.size(), digest.data()); + if (ret) { + throw std::runtime_error(fmt::format("Computing HMAC failed ({}): {}", ret, gnutls_strerror(ret))); + } + return digest; +} + +static hmac_sha256_digest get_signature_key(std::string_view key, std::string_view date_stamp, std::string_view region_name, std::string_view service_name) { + auto date = hmac_sha256("AWS4" + std::string(key), date_stamp); + auto region = hmac_sha256(std::string_view(date.data(), date.size()), region_name); + auto service = hmac_sha256(std::string_view(region.data(), region.size()), service_name); + auto signing = hmac_sha256(std::string_view(service.data(), service.size()), "aws4_request"); + return signing; +} + +static std::string apply_sha256(std::string_view msg) { + sha256_hasher hasher; + hasher.update(msg.data(), msg.size()); + return to_hex(hasher.finalize()); +} + +static std::string apply_sha256(const std::vector>& msg) { + sha256_hasher hasher; + for (const temporary_buffer& buf : msg) { + hasher.update(buf.get(), buf.size()); + } + return to_hex(hasher.finalize()); +} + +static std::string format_time_point(db_clock::time_point tp) { + time_t time_point_repr = db_clock::to_time_t(tp); + std::string time_point_str; + time_point_str.resize(17); + ::tm time_buf; + // strftime prints the terminating null character as well + std::strftime(time_point_str.data(), time_point_str.size(), "%Y%m%dT%H%M%SZ", ::gmtime_r(&time_point_repr, &time_buf)); + time_point_str.resize(16); + return time_point_str; +} + +void check_expiry(std::string_view signature_date) { + //FIXME: The default 15min can be changed with X-Amz-Expires header - we should honor it + std::string expiration_str = format_time_point(db_clock::now() - 15min); + std::string validity_str = format_time_point(db_clock::now() + 15min); + if (signature_date < expiration_str) { + throw std::runtime_error( + fmt::format("Signature expired: {} is now earlier than {} (current time - 15 min.)", + signature_date, expiration_str)); + } + if (signature_date > validity_str) { + throw std::runtime_error( + fmt::format("Signature not yet current: {} is still later than {} (current time + 15 min.)", + signature_date, validity_str)); + } +} + +std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method, + std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map& signed_headers_map, + const std::vector>& body_content, std::string_view region, std::string_view service, std::string_view query_string) { + auto amz_date_it = signed_headers_map.find("x-amz-date"); + if (amz_date_it == signed_headers_map.end()) { + throw std::runtime_error("X-Amz-Date header is mandatory for signature verification"); + } + std::string_view amz_date = amz_date_it->second; + check_expiry(amz_date); + std::string_view datestamp = amz_date.substr(0, 8); + if (datestamp != orig_datestamp) { + throw std::runtime_error( + format("X-Amz-Date date does not match the provided datestamp. Expected {}, got {}", + orig_datestamp, datestamp)); + } + std::string_view canonical_uri = "/"; + + std::stringstream canonical_headers; + for (const auto& header : signed_headers_map) { + canonical_headers << fmt::format("{}:{}", header.first, header.second) << '\n'; + } + + std::string payload_hash = apply_sha256(body_content); + std::string canonical_request = fmt::format("{}\n{}\n{}\n{}\n{}\n{}", method, canonical_uri, query_string, canonical_headers.str(), signed_headers_str, payload_hash); + + std::string_view algorithm = "AWS4-HMAC-SHA256"; + std::string credential_scope = fmt::format("{}/{}/{}/aws4_request", datestamp, region, service); + std::string string_to_sign = fmt::format("{}\n{}\n{}\n{}", algorithm, amz_date, credential_scope, apply_sha256(canonical_request)); + + hmac_sha256_digest signing_key = get_signature_key(secret_access_key, datestamp, region, service); + hmac_sha256_digest signature = hmac_sha256(std::string_view(signing_key.data(), signing_key.size()), string_to_sign); + + return to_hex(bytes_view(reinterpret_cast(signature.data()), signature.size())); +} + +} // aws namespace +} // utils namespace diff --git a/utils/aws_sigv4.hh b/utils/aws_sigv4.hh new file mode 100644 index 0000000000..9117463d59 --- /dev/null +++ b/utils/aws_sigv4.hh @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023-present ScyllaDB + */ + +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include +#include "utils/hashers.hh" + +// The declared below get_signature() method makes the Signature string for AWS +// authenticated requests as described in [1]. It can be used in two ways. +// +// First, if a request is about to be sent, the method can be used to create the +// signature value that'll later be included into Authorization header, Signature +// part. It's up to the caller to provide request with relevant headers and the +// signed_headers_map list. +// +// Second, for a received request this method can be used to calculate the signature +// that can later be compared with the request's Authorization header, Signature +// part for correctness. +// +// [1] https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html + +namespace utils { + +using hmac_sha256_digest = std::array; + +namespace aws { + +std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method, + std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map& signed_headers_map, + const std::vector>& body_content, std::string_view region, std::string_view service, std::string_view query_string); + +} // aws namespace +} // utils namespace