userver: /data/code/userver/libraries/s3api/src/s3api/authenticators/signature_v4.cpp Source File
Loading...
Searching...
No Matches
signature_v4.cpp
1#include <userver/s3api/authenticators/signature_v4.hpp>
2
3#include <algorithm>
4#include <cctype>
5#include <optional>
6#include <stdexcept>
7#include <string>
8#include <string_view>
9#include <utility>
10#include <vector>
11
12#include <fmt/format.h>
13
14#include <userver/clients/http/request.hpp>
15#include <userver/crypto/aws.hpp>
16#include <userver/http/common_headers.hpp>
17#include <userver/s3api/models/request.hpp>
18
19USERVER_NAMESPACE_BEGIN
20
21namespace s3api::authenticators {
22
23namespace {
24
25std::string TrimAndCollapseSpaces(std::string_view value) {
26 std::string result;
27 result.reserve(value.size());
28
29 bool pending_space = false;
30 for (auto c : value) {
31 if (std::isspace(static_cast<unsigned char>(c))) {
32 pending_space = !result.empty();
33 continue;
34 }
35
36 if (pending_space) {
37 result.push_back(' ');
38 pending_space = false;
39 }
40
41 result.push_back(c);
42 }
43
44 return result;
45}
46
47bool IsUnreservedChar(char c) {
48 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
49 return true;
50 }
51 return c == '-' || c == '_' || c == '.' || c == '~';
52}
53
54void PercentEncodeByteTo(unsigned char byte, std::string& result) {
55 static constexpr char kHexDigits[] = "0123456789ABCDEF";
56 result.push_back('%');
57 result.push_back(kHexDigits[byte >> 4]);
58 result.push_back(kHexDigits[byte & 0x0F]);
59}
60
61std::optional<int> ParseHexDigit(char c) {
62 if (c >= '0' && c <= '9') {
63 return c - '0';
64 }
65 if (c >= 'A' && c <= 'F') {
66 return c - 'A' + 10;
67 }
68 if (c >= 'a' && c <= 'f') {
69 return c - 'a' + 10;
70 }
71 return std::nullopt;
72}
73
74std::string UriEncode(std::string_view value, bool encode_slash) {
75 std::string result;
76 result.reserve(value.size());
77
78 for (auto c : value) {
79 if (IsUnreservedChar(c) || (c == '/' && !encode_slash)) {
80 result.push_back(c);
81 } else {
82 PercentEncodeByteTo(static_cast<unsigned char>(c), result);
83 }
84 }
85
86 return result;
87}
88
89std::string UriDecode(std::string_view value) {
90 std::string result;
91 result.reserve(value.size());
92
93 for (std::size_t i = 0; i < value.size(); ++i) {
94 if (value[i] == '%' && i + 2 < value.size()) {
95 const auto high = ParseHexDigit(value[i + 1]);
96 const auto low = ParseHexDigit(value[i + 2]);
97 if (high && low) {
98 result.push_back(static_cast<char>((*high * 16) + *low));
99 i += 2;
100 continue;
101 }
102 }
103 result.push_back(value[i]);
104 }
105
106 return result;
107}
108
109struct RequestTarget {
110 std::string_view path;
111 std::string_view query;
112};
113
114RequestTarget SplitRequestTarget(const std::string& req) {
115 const std::string_view target{req};
116 const auto query_pos = target.find('?');
117
118 if (query_pos == std::string_view::npos) {
119 return RequestTarget{
120 .path = target,
121 .query = {},
122 };
123 }
124
125 return RequestTarget{
126 .path = target.substr(0, query_pos),
127 .query = target.substr(query_pos + 1),
128 };
129}
130
131bool IsVirtualHostAddressing(std::string_view host, std::string_view bucket) {
132 if (bucket.empty()) {
133 return true;
134 }
135 if (host.size() <= bucket.size() || host[bucket.size()] != '.') {
136 return false;
137 }
138 return host.substr(0, bucket.size()) == bucket;
139}
140
141std::string MakeCanonicalUri(const Request& request, std::string_view host, std::string_view path) {
142 std::string raw_path;
143
144 if (!IsVirtualHostAddressing(host, request.bucket)) {
145 raw_path = request.bucket + "/";
146 }
147
148 raw_path += UriDecode(path);
149
150 return "/" + UriEncode(raw_path, /*encode_slash=*/false);
151}
152
153using QueryParams = std::vector<std::pair<std::string, std::string>>;
154
155QueryParams ParseQuery(std::string_view query) {
156 QueryParams result;
157
158 while (!query.empty()) {
159 const auto param = query.substr(0, query.find('&'));
160 query.remove_prefix(std::min(query.size(), param.size() + 1));
161
162 if (param.empty()) {
163 continue;
164 }
165
166 const auto eq_pos = param.find('=');
167 if (eq_pos == std::string_view::npos) {
168 result.emplace_back(UriDecode(param), std::string{});
169 } else {
170 result.emplace_back(UriDecode(param.substr(0, eq_pos)), UriDecode(param.substr(eq_pos + 1)));
171 }
172 }
173
174 return result;
175}
176
177std::string MakeCanonicalQueryString(QueryParams params) {
178 for (auto& [name, value] : params) {
179 name = UriEncode(name, /*encode_slash=*/true);
180 value = UriEncode(value, /*encode_slash=*/true);
181 }
182 std::ranges::sort(params);
183
184 std::string result;
185
186 for (const auto& [name, value] : params) {
187 if (!result.empty()) {
188 result.push_back('&');
189 }
190 result.append(name);
191 result.push_back('=');
192 result.append(value);
193 }
194
195 return result;
196}
197
198std::string GetHostHeaderValue(const Request& request) {
199 const auto it = request.headers.find(USERVER_NAMESPACE::http::headers::kHost);
200 if (it == request.headers.end() || it->second.empty()) {
201 throw std::runtime_error("AWS Signature V4 requires the 'Host' header, set it before signing the request");
202 }
203 return TrimAndCollapseSpaces(it->second);
204}
205
206} // namespace
207
208std::unordered_map<std::string, std::string> SignatureV4::Auth(const Request& request) const {
209 // https://docs.aws.amazon.com/AmazonS3/latest/developerguide/sig-v4-header-based-auth.html
210
211 const auto host = GetHostHeaderValue(request);
212 const auto target = SplitRequestTarget(request.req);
213 const auto canonical_uri = MakeCanonicalUri(request, host, target.path);
214 const auto canonical_query = MakeCanonicalQueryString(ParseQuery(target.query));
215
216 auto headers = request.headers;
218 headers,
219 {
220 .http_method = ToStringView(request.method),
221 .canonical_uri = canonical_uri,
222 .canonical_query = canonical_query,
223 .payload = request.body,
224 .access_key = access_key_,
225 .secret_key = secret_key_.GetUnderlying(),
226 .region = region_,
227 .service = service_,
228 }
229 );
230
231 return {
232 {"Authorization", headers[USERVER_NAMESPACE::http::headers::kAuthorization]},
233 {"X-Amz-Date", headers[crypto::aws::kAmzDate]},
234 {"X-Amz-Content-Sha256", headers[crypto::aws::kAmzContentSha256]},
235 };
236}
237
238std::unordered_map<std::string, std::string> SignatureV4::Sign(const Request& request, std::time_t expires) const {
239 // https://docs.aws.amazon.com/AmazonS3/latest/developerguide/sigv4-query-string-auth.html
240
241 const auto scope = crypto::aws::MakeV4TimeScope(region_, service_);
242 const auto host = GetHostHeaderValue(request);
243 const auto target = SplitRequestTarget(request.req);
244
245 const auto expires_in = std::max<std::time_t>(expires - scope.now, 1);
246
247 std::unordered_map<std::string, std::string> sign_params{
248 {"X-Amz-Algorithm", std::string{crypto::aws::kAws4HmacSha256}},
249 {"X-Amz-Credential", fmt::format("{}/{}", access_key_, scope.credential_scope)},
250 {"X-Amz-Date", scope.amz_date},
251 {"X-Amz-Expires", std::to_string(expires_in)},
252 {"X-Amz-SignedHeaders", "host"},
253 };
254
255 QueryParams query_params = ParseQuery(target.query);
256 query_params.insert(query_params.end(), sign_params.begin(), sign_params.end());
257
258 const auto canonical_request = crypto::aws::MakeCanonicalRequest(
259 ToStringView(request.method),
260 MakeCanonicalUri(request, host, target.path),
261 MakeCanonicalQueryString(std::move(query_params)),
262 fmt::format("host:{}\n", host),
263 "host",
264 crypto::aws::kUnsignedPayload
265 );
266
267 const auto string_to_sign = crypto::aws::MakeV4StringToSign(canonical_request, scope);
268 sign_params.emplace(
269 "X-Amz-Signature",
270 crypto::aws::MakeV4Signature(string_to_sign, scope, region_, service_, secret_key_.GetUnderlying())
271 );
272
273 return sign_params;
274}
275
276} // namespace s3api::authenticators
277
278USERVER_NAMESPACE_END