1 /* AWS V4 Signature implementation
2 *
3 * This file contains the modularized source code for accepting a given HTTP
4 * request as ngx_http_request_t and modifiying it to introduce the
5 * Authorization header in compliance with the AWS V4 spec. The IAM access
6 * key and the signing key (not to be confused with the secret key) along
7 * with it's scope are taken as inputs.
8 *
9 * The actual nginx module binding code is not present in this file. This file
10 * is meant to serve as an "AWS Signing SDK for nginx".
11 *
12 * Maintainer/contributor rules
13 *
14 * (1) All functions here need to be static and inline.
15 * (2) Every function must have it's own set of unit tests.
16 * (3) The code must be written in a thread-safe manner. This is usually not
17 * a problem with standard nginx functions. However, care must be taken
18 * when using very old C functions such as strtok, gmtime, etc. etc.
19 * Always use the _r variants of such functions
20 * (4) All heap allocation must be done using ngx_pool_t instead of malloc
21 */
22
23 #ifndef __NGX_AWS_FUNCTIONS_INTERNAL__H__
24 #define __NGX_AWS_FUNCTIONS_INTERNAL__H__
25
26 #include <time.h>
27 #include <ngx_times.h>
28 #include <ngx_core.h>
29 #include <ngx_http.h>
30
31 #include "crypto_helper.h"
32
33 #define AMZ_DATE_MAX_LEN 20
34 #define STRING_TO_SIGN_LENGTH 3000
35
36 typedef ngx_keyval_t header_pair_t;
37
38 struct AwsCanonicalRequestDetails {
39 ngx_str_t *canon_request;
40 ngx_str_t *signed_header_names;
41 ngx_array_t *header_list; // list of header_pair_t
42 };
43
44 struct AwsCanonicalHeaderDetails {
45 ngx_str_t *canon_header_str;
46 ngx_str_t *signed_header_names;
47 ngx_array_t *header_list; // list of header_pair_t
48 };
49
50 struct AwsSignedRequestDetails {
51 const ngx_str_t *signature;
52 const ngx_str_t *signed_header_names;
53 ngx_array_t *header_list; // list of header_pair_t
54 };
55
56 // mainly useful to avoid having to full instantiate request structures for
57 // tests...
58 #define safe_ngx_log_error(req, ...) \
59 if (req->connection) { \
60 ngx_log_error(NGX_LOG_ERR, req->connection->log, 0, __VA_ARGS__); \
61 }
62
63 static const ngx_str_t EMPTY_STRING_SHA256 = ngx_string("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
64 static const ngx_str_t EMPTY_STRING = ngx_null_string;
65 static const ngx_str_t AMZ_HASH_HEADER = ngx_string("x-amz-content-sha256");
66 static const ngx_str_t AMZ_DATE_HEADER = ngx_string("x-amz-date");
67 static const ngx_str_t HOST_HEADER = ngx_string("host");
68 static const ngx_str_t AUTHZ_HEADER = ngx_string("authorization");
69
__CHAR_PTR_U(u_char * ptr)70 static inline char* __CHAR_PTR_U(u_char* ptr) {return (char*)ptr;}
__CONST_CHAR_PTR_U(const u_char * ptr)71 static inline const char* __CONST_CHAR_PTR_U(const u_char* ptr) {return (const char*)ptr;}
72
ngx_aws_auth__compute_request_time(ngx_pool_t * pool,const time_t * timep)73 static inline const ngx_str_t* ngx_aws_auth__compute_request_time(ngx_pool_t *pool, const time_t *timep) {
74 ngx_str_t *const retval = ngx_palloc(pool, sizeof(ngx_str_t));
75 retval->data = ngx_palloc(pool, AMZ_DATE_MAX_LEN);
76 struct tm *tm_p = ngx_palloc(pool, sizeof(struct tm));
77 gmtime_r(timep, tm_p);
78 retval->len = strftime(__CHAR_PTR_U(retval->data), AMZ_DATE_MAX_LEN - 1, "%Y%m%dT%H%M%SZ", tm_p);
79 return retval;
80 }
81
ngx_aws_auth__cmp_hnames(const void * one,const void * two)82 static inline int ngx_aws_auth__cmp_hnames(const void *one, const void *two) {
83 header_pair_t *first, *second;
84 int ret;
85 first = (header_pair_t *) one;
86 second = (header_pair_t *) two;
87 ret = ngx_strncmp(first->key.data, second->key.data, ngx_min(first->key.len, second->key.len));
88 if (ret != 0){
89 return ret;
90 } else {
91 return (first->key.len - second->key.len);
92 }
93 }
94
ngx_aws_auth__canonize_query_string(ngx_pool_t * pool,const ngx_http_request_t * req)95 static inline const ngx_str_t* ngx_aws_auth__canonize_query_string(ngx_pool_t *pool,
96 const ngx_http_request_t *req) {
97 u_char *p, *ampersand, *equal, *last;
98 size_t i, len;
99 ngx_str_t *retval = ngx_palloc(pool, sizeof(ngx_str_t));
100
101 header_pair_t *qs_arg;
102 ngx_array_t *query_string_args = ngx_array_create(pool, 0, sizeof(header_pair_t));
103
104 if (req->args.len == 0) {
105 return &EMPTY_STRING;
106 }
107
108 p = req->args.data;
109 last = p + req->args.len;
110
111 for ( /* void */ ; p < last; p++) {
112 qs_arg = ngx_array_push(query_string_args);
113
114 ampersand = ngx_strlchr(p, last, '&');
115 if (ampersand == NULL) {
116 ampersand = last;
117 }
118
119 equal = ngx_strlchr(p, last, '=');
120 if ((equal == NULL) || (equal > ampersand)) {
121 equal = ampersand;
122 }
123
124 len = equal - p;
125 qs_arg->key.data = ngx_palloc(pool, len*3);
126 qs_arg->key.len = (u_char *)ngx_escape_uri(qs_arg->key.data, p, len, NGX_ESCAPE_ARGS) - qs_arg->key.data;
127
128
129 len = ampersand - equal;
130 if(len > 0 ) {
131 qs_arg->value.data = ngx_palloc(pool, len*3);
132 qs_arg->value.len = (u_char *)ngx_escape_uri(qs_arg->value.data, equal+1, len-1, NGX_ESCAPE_ARGS) - qs_arg->value.data;
133 } else {
134 qs_arg->value = EMPTY_STRING;
135 }
136
137 p = ampersand;
138 }
139
140 ngx_qsort(query_string_args->elts, (size_t) query_string_args->nelts,
141 sizeof(header_pair_t), ngx_aws_auth__cmp_hnames);
142
143 retval->data = ngx_palloc(pool, req->args.len*3 + query_string_args->nelts*2);
144 retval->len = 0;
145
146 for(i = 0; i < query_string_args->nelts; i++) {
147 qs_arg = &((header_pair_t*)query_string_args->elts)[i];
148
149 ngx_memcpy(retval->data + retval->len, qs_arg->key.data, qs_arg->key.len);
150 retval->len += qs_arg->key.len;
151
152 *(retval->data + retval->len) = '=';
153 retval->len++;
154
155 ngx_memcpy(retval->data + retval->len, qs_arg->value.data, qs_arg->value.len);
156 retval->len += qs_arg->value.len;
157
158 *(retval->data + retval->len) = '&';
159 retval->len++;
160 }
161 retval->len--;
162
163 safe_ngx_log_error(req, "canonical qs constructed is %V", retval);
164
165 return retval;
166 }
167
168
ngx_aws_auth__host_from_bucket(ngx_pool_t * pool,const ngx_str_t * s3_bucket)169 static inline const ngx_str_t* ngx_aws_auth__host_from_bucket(ngx_pool_t *pool,
170 const ngx_str_t *s3_bucket) {
171 static const char HOST_PATTERN[] = ".s3.amazonaws.com";
172 ngx_str_t *host;
173
174 host = ngx_palloc(pool, sizeof(ngx_str_t));
175 host->len = s3_bucket->len + sizeof(HOST_PATTERN) + 1;
176 host->data = ngx_palloc(pool, host->len);
177 host->len = ngx_snprintf(host->data, host->len, "%V%s", s3_bucket, HOST_PATTERN) - host->data;
178
179 return host;
180 }
181
ngx_aws_auth__canonize_headers(ngx_pool_t * pool,const ngx_http_request_t * req,const ngx_str_t * s3_bucket,const ngx_str_t * amz_date,const ngx_str_t * content_hash,const ngx_str_t * s3_endpoint)182 static inline struct AwsCanonicalHeaderDetails ngx_aws_auth__canonize_headers(ngx_pool_t *pool,
183 const ngx_http_request_t *req,
184 const ngx_str_t *s3_bucket, const ngx_str_t *amz_date,
185 const ngx_str_t *content_hash,
186 const ngx_str_t *s3_endpoint) {
187 size_t header_names_size = 1, header_nameval_size = 1;
188 size_t i, used;
189 u_char *buf_progress;
190 struct AwsCanonicalHeaderDetails retval;
191
192 ngx_array_t *settable_header_array = ngx_array_create(pool, 3, sizeof(header_pair_t));
193 header_pair_t *header_ptr;
194
195 header_ptr = ngx_array_push(settable_header_array);
196 header_ptr->key = AMZ_HASH_HEADER;
197 header_ptr->value = *content_hash;
198
199 header_ptr = ngx_array_push(settable_header_array);
200 header_ptr->key = AMZ_DATE_HEADER;
201 header_ptr->value = *amz_date;
202
203 header_ptr = ngx_array_push(settable_header_array);
204 header_ptr->key = HOST_HEADER;
205 header_ptr->value.len = s3_bucket->len + 60;
206 header_ptr->value.data = ngx_palloc(pool, header_ptr->value.len);
207 header_ptr->value.len = ngx_snprintf(header_ptr->value.data, header_ptr->value.len, "%V.%V", s3_bucket, s3_endpoint) - header_ptr->value.data;
208
209 ngx_qsort(settable_header_array->elts, (size_t) settable_header_array->nelts,
210 sizeof(header_pair_t), ngx_aws_auth__cmp_hnames);
211 retval.header_list = settable_header_array;
212
213 for(i = 0; i < settable_header_array->nelts; i++) {
214 header_names_size += ((header_pair_t*)settable_header_array->elts)[i].key.len + 1;
215 header_nameval_size += ((header_pair_t*)settable_header_array->elts)[i].key.len + 1;
216 header_nameval_size += ((header_pair_t*)settable_header_array->elts)[i].value.len + 2;
217 }
218
219 /* make canonical headers string */
220 retval.canon_header_str = ngx_palloc(pool, sizeof(ngx_str_t));
221 retval.canon_header_str->data = ngx_palloc(pool, header_nameval_size);
222
223 for(i = 0, used = 0, buf_progress = retval.canon_header_str->data;
224 i < settable_header_array->nelts;
225 i++, used = buf_progress - retval.canon_header_str->data) {
226 buf_progress = ngx_snprintf(buf_progress, header_nameval_size - used, "%V:%V\n",
227 & ((header_pair_t*)settable_header_array->elts)[i].key,
228 & ((header_pair_t*)settable_header_array->elts)[i].value);
229 }
230 retval.canon_header_str->len = used;
231
232 /* make signed headers */
233 retval.signed_header_names = ngx_palloc(pool, sizeof(ngx_str_t));
234 retval.signed_header_names->data = ngx_palloc(pool, header_names_size);
235
236 for(i = 0, used = 0, buf_progress = retval.signed_header_names->data;
237 i < settable_header_array->nelts;
238 i++, used = buf_progress - retval.signed_header_names->data) {
239 buf_progress = ngx_snprintf(buf_progress, header_names_size - used, "%V;",
240 & ((header_pair_t*)settable_header_array->elts)[i].key);
241 }
242 used--;
243 retval.signed_header_names->len = used;
244 retval.signed_header_names->data[used] = 0;
245
246 return retval;
247 }
248
ngx_aws_auth__request_body_hash(ngx_pool_t * pool,const ngx_http_request_t * req)249 static inline const ngx_str_t* ngx_aws_auth__request_body_hash(ngx_pool_t *pool,
250 const ngx_http_request_t *req) {
251 /* TODO: support cases involving non-empty body */
252 return &EMPTY_STRING_SHA256;
253 }
254
255 // AWS wants a peculiar kind of URI-encoding: they want RFC 3986, except that
256 // slashes shouldn't be encoded...
257 // this function is a light wrapper around ngx_escape_uri that does exactly that
258 // modifies the source in place if it needs to be escaped
259 // see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
ngx_aws_auth__escape_uri(ngx_pool_t * pool,ngx_str_t * src)260 static inline void ngx_aws_auth__escape_uri(ngx_pool_t *pool, ngx_str_t* src) {
261 u_char *escaped_data;
262 u_int escaped_data_len, escaped_data_with_slashes_len, i, j;
263 uintptr_t escaped_count, slashes_count = 0;
264
265 // first, we need to know how many characters need to be escaped
266 escaped_count = ngx_escape_uri(NULL, src->data, src->len, NGX_ESCAPE_URI_COMPONENT);
267 // except slashes should not be escaped...
268 if (escaped_count > 0) {
269 for (i = 0; i < src->len; i++) {
270 if (src->data[i] == '/') {
271 slashes_count++;
272 }
273 }
274 }
275
276 if (escaped_count == slashes_count) {
277 // nothing to do! nothing but slashes escaped (if even that)
278 return;
279 }
280
281 // each escaped character is replaced by 3 characters
282 escaped_data_len = src->len + escaped_count * 2;
283 escaped_data = ngx_palloc(pool, escaped_data_len);
284 ngx_escape_uri(escaped_data, src->data, src->len, NGX_ESCAPE_URI_COMPONENT);
285
286 // now we need to go back and re-replace each occurrence of %2F with a slash
287 escaped_data_with_slashes_len = src->len + (escaped_count - slashes_count) * 2;
288 if (slashes_count > 0) {
289 for (i = 0, j = 0; i < escaped_data_with_slashes_len; i++) {
290 if (j < escaped_data_len - 2 && strncmp((char*) (escaped_data + j), "%2F", 3) == 0) {
291 escaped_data[i] = '/';
292 j += 3;
293 } else {
294 escaped_data[i] = escaped_data[j];
295 j++;
296 }
297 }
298
299 src->len = escaped_data_with_slashes_len;
300 } else {
301 // no slashes
302 src->len = escaped_data_len;
303 }
304
305 src->data = escaped_data;
306 }
307
ngx_aws_auth__canon_url(ngx_pool_t * pool,const ngx_http_request_t * req)308 static inline const ngx_str_t* ngx_aws_auth__canon_url(ngx_pool_t *pool, const ngx_http_request_t *req) {
309 ngx_str_t *retval;
310 const u_char *req_uri_data;
311 u_int req_uri_len;
312
313 if(req->args.len == 0) {
314 req_uri_data = req->uri.data;
315 req_uri_len = req->uri.len;
316 } else {
317 req_uri_data = req->uri_start;
318 req_uri_len = req->args_start - req->uri_start - 1;
319 }
320
321 // we need to copy that data to not modify the request for other modules
322 retval = ngx_palloc(pool, sizeof(ngx_str_t));
323 retval->data = ngx_palloc(pool, req_uri_len);
324 ngx_memcpy(retval->data, req_uri_data, req_uri_len);
325 retval->len = req_uri_len;
326
327 safe_ngx_log_error(req, "canonical url extracted before URI encoding is %V", retval);
328
329 // then URI-encode it per RFC 3986
330 ngx_aws_auth__escape_uri(pool, retval);
331 safe_ngx_log_error(req, "canonical url extracted after URI encoding is %V", retval);
332
333 return retval;
334 }
335
ngx_aws_auth__make_canonical_request(ngx_pool_t * pool,const ngx_http_request_t * req,const ngx_str_t * s3_bucket_name,const ngx_str_t * amz_date,const ngx_str_t * s3_endpoint)336 static inline struct AwsCanonicalRequestDetails ngx_aws_auth__make_canonical_request(ngx_pool_t *pool,
337 const ngx_http_request_t *req,
338 const ngx_str_t *s3_bucket_name, const ngx_str_t *amz_date, const ngx_str_t *s3_endpoint) {
339 struct AwsCanonicalRequestDetails retval;
340
341 // canonize query string
342 const ngx_str_t *canon_qs = ngx_aws_auth__canonize_query_string(pool, req);
343
344 // compute request body hash
345 const ngx_str_t *request_body_hash = ngx_aws_auth__request_body_hash(pool, req);
346
347 const struct AwsCanonicalHeaderDetails canon_headers =
348 ngx_aws_auth__canonize_headers(pool, req, s3_bucket_name, amz_date, request_body_hash, s3_endpoint);
349 retval.signed_header_names = canon_headers.signed_header_names;
350
351 const ngx_str_t *http_method = &(req->method_name);
352 const ngx_str_t *url = ngx_aws_auth__canon_url(pool, req);
353
354 retval.canon_request = ngx_palloc(pool, sizeof(ngx_str_t));
355 retval.canon_request->len = 10000;
356 retval.canon_request->data = ngx_palloc(pool, retval.canon_request->len);
357
358 retval.canon_request->len = ngx_snprintf(retval.canon_request->data, retval.canon_request->len, "%V\n%V\n%V\n%V\n%V\n%V",
359 http_method, url, canon_qs, canon_headers.canon_header_str,
360 canon_headers.signed_header_names, request_body_hash) - retval.canon_request->data;
361 retval.header_list = canon_headers.header_list;
362
363 safe_ngx_log_error(req, "canonical req is %V", retval.canon_request);
364
365 return retval;
366 }
367
ngx_aws_auth__string_to_sign(ngx_pool_t * pool,const ngx_str_t * key_scope,const ngx_str_t * date,const ngx_str_t * canon_request_hash)368 static inline const ngx_str_t* ngx_aws_auth__string_to_sign(ngx_pool_t *pool,
369 const ngx_str_t *key_scope, const ngx_str_t *date, const ngx_str_t *canon_request_hash) {
370 ngx_str_t *retval = ngx_palloc(pool, sizeof(ngx_str_t));
371
372 retval->len = STRING_TO_SIGN_LENGTH;
373 retval->data = ngx_palloc(pool, retval->len);
374 retval->len = ngx_snprintf(retval->data, retval->len, "AWS4-HMAC-SHA256\n%V\n%V\n%V",
375 date, key_scope, canon_request_hash) - retval->data ;
376
377 return retval;
378 }
379
ngx_aws_auth__make_auth_token(ngx_pool_t * pool,const ngx_str_t * signature,const ngx_str_t * signed_header_names,const ngx_str_t * access_key_id,const ngx_str_t * key_scope)380 static inline const ngx_str_t* ngx_aws_auth__make_auth_token(ngx_pool_t *pool,
381 const ngx_str_t *signature, const ngx_str_t *signed_header_names,
382 const ngx_str_t *access_key_id, const ngx_str_t *key_scope) {
383
384 const char FMT_STRING[] = "AWS4-HMAC-SHA256 Credential=%V/%V,SignedHeaders=%V,Signature=%V";
385 ngx_str_t *authz;
386
387 authz = ngx_palloc(pool, sizeof(ngx_str_t));
388 authz->len = access_key_id->len + key_scope->len + signed_header_names->len
389 + signature->len + sizeof(FMT_STRING);
390 authz->data = ngx_palloc(pool, authz->len);
391 authz->len = ngx_snprintf(authz->data, authz->len, FMT_STRING,
392 access_key_id, key_scope, signed_header_names, signature) - authz->data;
393 return authz;
394 }
395
ngx_aws_auth__compute_signature(ngx_pool_t * pool,ngx_http_request_t * req,const ngx_str_t * signing_key,const ngx_str_t * key_scope,const ngx_str_t * s3_bucket_name,const ngx_str_t * s3_endpoint)396 static inline struct AwsSignedRequestDetails ngx_aws_auth__compute_signature(ngx_pool_t *pool, ngx_http_request_t *req,
397 const ngx_str_t *signing_key,
398 const ngx_str_t *key_scope,
399 const ngx_str_t *s3_bucket_name,
400 const ngx_str_t *s3_endpoint) {
401 struct AwsSignedRequestDetails retval;
402
403 const ngx_str_t *date = ngx_aws_auth__compute_request_time(pool, &req->start_sec);
404 const struct AwsCanonicalRequestDetails canon_request =
405 ngx_aws_auth__make_canonical_request(pool, req, s3_bucket_name, date, s3_endpoint);
406 const ngx_str_t *canon_request_hash = ngx_aws_auth__hash_sha256(pool, canon_request.canon_request);
407
408 // get string to sign
409 const ngx_str_t *string_to_sign = ngx_aws_auth__string_to_sign(pool, key_scope, date, canon_request_hash);
410
411 // generate signature
412 const ngx_str_t *signature = ngx_aws_auth__sign_sha256_hex(pool, string_to_sign, signing_key);
413
414 retval.signature = signature;
415 retval.signed_header_names = canon_request.signed_header_names;
416 retval.header_list = canon_request.header_list;
417 return retval;
418 }
419
420
421 // list of header_pair_t
ngx_aws_auth__sign(ngx_pool_t * pool,ngx_http_request_t * req,const ngx_str_t * access_key_id,const ngx_str_t * signing_key,const ngx_str_t * key_scope,const ngx_str_t * s3_bucket_name,const ngx_str_t * s3_endpoint)422 static inline const ngx_array_t* ngx_aws_auth__sign(ngx_pool_t *pool, ngx_http_request_t *req,
423 const ngx_str_t *access_key_id,
424 const ngx_str_t *signing_key,
425 const ngx_str_t *key_scope,
426 const ngx_str_t *s3_bucket_name,
427 const ngx_str_t *s3_endpoint) {
428 const struct AwsSignedRequestDetails signature_details = ngx_aws_auth__compute_signature(pool, req, signing_key, key_scope, s3_bucket_name, s3_endpoint);
429
430
431 const ngx_str_t *auth_header_value = ngx_aws_auth__make_auth_token(pool, signature_details.signature,
432 signature_details.signed_header_names, access_key_id, key_scope);
433
434 header_pair_t *header_ptr;
435 header_ptr = ngx_array_push(signature_details.header_list);
436 header_ptr->key = AUTHZ_HEADER;
437 header_ptr->value = *auth_header_value;
438
439 return signature_details.header_list;
440 }
441
442 #endif
443