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