1#include <userver/s3api/authenticators/signature_v4.hpp>
12#include <fmt/format.h>
13#include <boost/algorithm/string.hpp>
15#include <userver/crypto/hash.hpp>
16#include <userver/http/common_headers.hpp>
17#include <userver/s3api/authenticators/utils.hpp>
18#include <userver/s3api/models/request.hpp>
19#include <userver/utils/datetime_light.hpp>
21USERVER_NAMESPACE_BEGIN
27constexpr std::string_view kAlgorithm =
"AWS4-HMAC-SHA256";
28constexpr std::string_view kAws4Request =
"aws4_request";
29constexpr std::string_view kUnsignedPayload =
"UNSIGNED-PAYLOAD";
31bool IsUnreservedChar(
char c) {
32 if ((c >=
'0' && c <=
'9') || (c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z')) {
35 return c ==
'-' || c ==
'_' || c ==
'.' || c ==
'~';
38void PercentEncodeByteTo(
unsigned char byte, std::string& result) {
39 static constexpr char kHexDigits[] =
"0123456789ABCDEF";
40 result.push_back(
'%');
41 result.push_back(kHexDigits[byte >> 4]);
42 result.push_back(kHexDigits[byte & 0x0F]);
45std::string UriEncode(std::string_view value,
bool encode_slash) {
47 result.reserve(value.size());
49 for (
auto c : value) {
50 if (IsUnreservedChar(c) || (c ==
'/' && !encode_slash)) {
53 PercentEncodeByteTo(
static_cast<
unsigned char>(c), result);
60std::optional<
int> ParseHexDigit(
char c) {
61 if (c >=
'0' && c <=
'9') {
64 if (c >=
'A' && c <=
'F') {
67 if (c >=
'a' && c <=
'f') {
73std::string PercentDecode(std::string_view value) {
75 result.reserve(value.size());
77 for (std::size_t i = 0; i < value.size(); ++i) {
78 if (value[i] ==
'%' && i + 2 < value.size()) {
79 const auto high = ParseHexDigit(value[i + 1]);
80 const auto low = ParseHexDigit(value[i + 2]);
82 result.push_back(
static_cast<
char>((*high * 16) + *low));
87 result.push_back(value[i]);
93std::string TrimAndCollapseSpaces(std::string_view value) {
95 result.reserve(value.size());
97 bool pending_space =
false;
98 for (
auto c : value) {
99 if (std::isspace(
static_cast<
unsigned char>(c))) {
100 pending_space = !result.empty();
105 result.push_back(
' ');
106 pending_space =
false;
115struct RequestTarget {
116 std::string_view path;
117 std::string_view query;
120RequestTarget SplitRequestTarget(
const std::string& req) {
121 const std::string_view target{req};
122 const auto query_pos = target.find(
'?');
124 if (query_pos == std::string_view::npos) {
125 return RequestTarget{
131 return RequestTarget{
132 .path = target.substr(0, query_pos),
133 .query = target.substr(query_pos + 1),
137bool IsVirtualHostAddressing(std::string_view host, std::string_view bucket) {
138 if (bucket.empty()) {
141 if (host.size() <= bucket.size() || host[bucket.size()] !=
'.') {
144 return host.substr(0, bucket.size()) == bucket;
147std::string MakeCanonicalUri(
const Request& request, std::string_view host, std::string_view path) {
148 std::string raw_path;
150 if (!IsVirtualHostAddressing(host, request.bucket)) {
151 raw_path = request.bucket +
"/";
154 raw_path += PercentDecode(path);
156 return "/" + UriEncode(raw_path,
false);
159using QueryParams = std::vector<std::pair<std::string, std::string>>;
161QueryParams ParseQuery(std::string_view query) {
164 while (!query.empty()) {
165 const auto param = query.substr(0, query.find(
'&'));
166 query.remove_prefix(std::min(query.size(), param.size() + 1));
172 const auto eq_pos = param.find(
'=');
173 if (eq_pos == std::string_view::npos) {
174 result.emplace_back(PercentDecode(param), std::string{});
176 result.emplace_back(PercentDecode(param.substr(0, eq_pos)), PercentDecode(param.substr(eq_pos + 1)));
183std::string MakeCanonicalQueryString(QueryParams params) {
184 for (
auto& [name, value] : params) {
185 name = UriEncode(name,
true);
186 value = UriEncode(value,
true);
188 std::ranges::sort(params);
192 for (
const auto& [name, value] : params) {
193 if (!result.empty()) {
194 result.push_back(
'&');
197 result.push_back(
'=');
198 result.append(value);
204struct CanonicalHeaders {
208 std::string signed_headers;
211CanonicalHeaders MakeCanonicalHeaders(
const std::map<std::string, std::string>& headers) {
212 CanonicalHeaders result;
214 for (
const auto& [name, value] : headers) {
215 result.headers += fmt::format(
"{}:{}\n", name, value);
216 if (!result.signed_headers.empty()) {
217 result.signed_headers.push_back(
';');
219 result.signed_headers += name;
225std::string MakeCanonicalRequest(
227 std::string_view host,
228 const CanonicalHeaders& canonical_headers,
229 QueryParams extra_query_params,
230 std::string_view payload_hash
232 const auto target = SplitRequestTarget(request.req);
234 auto query_params = ParseQuery(target.query);
235 std::ranges::move(extra_query_params, std::back_inserter(query_params));
238 "{}\n{}\n{}\n{}\n{}\n{}",
240 MakeCanonicalUri(request, host, target.path),
241 MakeCanonicalQueryString(std::move(query_params)),
242 canonical_headers.headers,
243 canonical_headers.signed_headers,
250 std::string amz_date;
251 std::string date_stamp;
252 std::string credential_scope;
255SigningScope MakeSigningScope(std::string_view region, std::string_view service) {
259 scope.now = std::chrono::system_clock::to_time_t(now);
261 scope.date_stamp = scope.amz_date.substr(0, 8);
262 scope.credential_scope = fmt::format(
"{}/{}/{}/{}", scope.date_stamp, region, service, kAws4Request);
267std::string MakeStringToSign(std::string_view canonical_request,
const SigningScope& scope) {
272 scope.credential_scope,
277std::string MakeSignature(
278 std::string_view string_to_sign,
279 const SigningScope& scope,
280 std::string_view region,
281 std::string_view service,
282 const Secret& secret_key
286 static constexpr auto kBinary = crypto::
hash::OutputEncoding::kBinary;
296std::string GetHostHeaderValue(
const Request& request) {
297 const auto it = request.headers
.find(USERVER_NAMESPACE::http::
headers::kHost
);
298 if (it == request.headers
.end() || it->second.empty()) {
299 throw std::runtime_error(
"AWS Signature V4 requires the 'Host' header, set it before signing the request");
301 return TrimAndCollapseSpaces(it->second);
306std::unordered_map<std::string, std::string>
SignatureV4::Auth(
const Request& request)
const {
309 const auto scope = MakeSigningScope(region_, service_);
310 const auto host = GetHostHeaderValue(request);
313 std::map<std::string, std::string> headers_to_sign;
314 for (
const auto& [name, value] : request.headers) {
315 headers_to_sign[boost::algorithm::to_lower_copy(name)] = TrimAndCollapseSpaces(value);
317 headers_to_sign[
"host"] = host;
318 headers_to_sign[
"x-amz-date"] = scope.amz_date;
319 headers_to_sign[
"x-amz-content-sha256"] = payload_hash;
321 const auto canonical_headers = MakeCanonicalHeaders(headers_to_sign);
322 const auto canonical_request = MakeCanonicalRequest(request, host, canonical_headers, {}, payload_hash);
323 const auto string_to_sign = MakeStringToSign(canonical_request, scope);
324 const auto signature = MakeSignature(string_to_sign, scope, region_, service_, secret_key_);
326 auto authorization = fmt::format(
327 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
330 scope.credential_scope,
331 canonical_headers.signed_headers,
336 {
"Authorization", std::move(authorization)},
337 {
"X-Amz-Date", scope.amz_date},
338 {
"X-Amz-Content-Sha256", std::move(payload_hash)},
345 const auto scope = MakeSigningScope(region_, service_);
346 const auto host = GetHostHeaderValue(request);
348 const auto expires_in = std::max<std::time_t>(expires - scope.now, 1);
350 std::unordered_map<std::string, std::string> sign_params{
351 {
"X-Amz-Algorithm", std::string{kAlgorithm}},
352 {
"X-Amz-Credential", fmt::format(
"{}/{}", access_key_, scope.credential_scope)},
353 {
"X-Amz-Date", scope.amz_date},
354 {
"X-Amz-Expires", std::to_string(expires_in)},
355 {
"X-Amz-SignedHeaders",
"host"},
358 const CanonicalHeaders canonical_headers{
359 .headers = fmt::format(
"host:{}\n", host),
360 .signed_headers =
"host",
363 const auto canonical_request = MakeCanonicalRequest(
367 QueryParams{sign_params.begin(), sign_params.end()},
371 const auto string_to_sign = MakeStringToSign(canonical_request, scope);
373 sign_params.emplace(
"X-Amz-Signature", MakeSignature(string_to_sign, scope, region_, service_, secret_key_));