├── .gitignore ├── .gitmodules ├── VERSION ├── crypto_helper.h ├── config ├── .travis.yml ├── .circleci └── config.yml ├── LICENSE ├── Makefile ├── crypto_helper_openssl.c ├── generate_signing_key ├── reference-impl-py ├── reference_v2.py └── reference_v4.py ├── README.md ├── ngx_http_aws_auth.c ├── test_suite.c └── aws_functions.h /.gitignore: -------------------------------------------------------------------------------- 1 | /.cmocka_build 2 | /test_suite 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/cmocka"] 2 | path = vendor/cmocka 3 | url = https://gitlab.com/cmocka/cmocka.git 4 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 2.0.0 4 | Move to AWS V4 signatures 5 | 6 | ## Version 1.1.1 7 | AWS date header is computed unconditionally. See #11 8 | 9 | ## Version 1.1.0 10 | Extend functionality beyond simple GET. See #5 11 | 12 | ## Version 1.0.0 13 | Start maintaining version numbers 14 | -------------------------------------------------------------------------------- /crypto_helper.h: -------------------------------------------------------------------------------- 1 | #ifndef __NGX_AWS_AUTH__CRYPTO_HELPER__ 2 | #define __NGX_AWS_AUTH__CRYPTO_HELPER__ 3 | 4 | 5 | #include 6 | #include 7 | 8 | 9 | ngx_str_t* ngx_aws_auth__hash_sha256(ngx_pool_t *pool, const ngx_str_t *blob); 10 | ngx_str_t* ngx_aws_auth__sign_sha256_hex(ngx_pool_t *pool, const ngx_str_t *blob, const ngx_str_t *signing_key); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_aws_auth 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP 5 | ngx_module_name=ngx_http_aws_auth_module 6 | ngx_module_incs= 7 | ngx_module_deps= 8 | ngx_module_srcs="$ngx_addon_dir/ngx_http_aws_auth.c $ngx_addon_dir/crypto_helper_openssl.c" 9 | ngx_module_libs="$CORE_LIBS -lssl -lcrypto" 10 | 11 | . auto/module 12 | else 13 | HTTP_MODULES="$HTTP_MODULES ngx_http_aws_auth_module" 14 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_aws_auth.c $ngx_addon_dir/crypto_helper_openssl.c" 15 | CORE_LIBS="$CORE_LIBS -lssl -lcrypto" 16 | fi 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | os: 4 | - linux 5 | dist: bionic 6 | 7 | addons: 8 | apt: 9 | packages: 10 | - libpcre3 11 | - libpcre3-dev 12 | - zlib1g-dev 13 | - libssl-dev 14 | 15 | env: 16 | jobs: 17 | - NGINX_VERSION=1.18.0 # stable version 18 | - NGINX_VERSION=1.19.4 # mainline 19 | - NGINX_VERSION=1.16.1 # legacy 1 20 | - NGINX_VERSION=1.14.2 # legacy 2 21 | global: 22 | - LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib 23 | 24 | jobs: 25 | include: 26 | - dist: focal 27 | - dist: focal 28 | compiler: clang 29 | - dist: focal 30 | group: edge 31 | arch: arm64-graviton2 32 | compiler: clang 33 | virt: lxd 34 | allow_failures: 35 | - compiler: clang 36 | 37 | script: 38 | make prepare-travis-env nginx test 39 | 40 | # vim: ts=2 sw=2 41 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | build-executor: 5 | docker: 6 | - image: ubuntu:20.04 7 | resource_class: small 8 | jobs: 9 | build: 10 | executor: build-executor 11 | steps: 12 | - run: 13 | name: Install dependencies 14 | command: | 15 | apt-get update 16 | apt-get install -y libpcre3 libpcre3-dev zlib1g-dev libssl-dev build-essential wget git sudo 17 | - run: 18 | name: Install cmake 19 | command: | 20 | echo 'tzdata tzdata/Areas select Etc' | debconf-set-selections 21 | echo 'tzdata tzdata/Zones/Etc select UTC' | debconf-set-selections 22 | DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata 23 | apt-get install -y cmake 24 | - checkout 25 | 26 | - run: 27 | name: Build and test 28 | command: | 29 | export NGINX_VERSION=1.18.0 30 | export LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib 31 | make prepare-travis-env nginx test 32 | 33 | workflows: 34 | version: 2 35 | build: 36 | jobs: 37 | - build 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Arvind Jayaprakash 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | CFLAGS=-g -I${NGX_PATH}/src/os/unix -I${NGX_PATH}/src/core -I${NGX_PATH}/src/http -I${NGX_PATH}/src/http/modules -I${NGX_PATH}/src/event -I${NGX_PATH}/objs/ -I. 3 | 4 | 5 | all: 6 | 7 | %.o: %.c 8 | $(CC) -c -o $@ $< $(CFLAGS) 9 | 10 | .PHONY: all clean test nginx prepare-travis-env 11 | 12 | 13 | NGX_PATH := $(shell echo `pwd`/nginx) 14 | 15 | prepare-travis-env: 16 | wget --no-verbose https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz 17 | tar -xzf nginx-${NGINX_VERSION}.tar.gz 18 | ln -s nginx-${NGINX_VERSION} ${NGX_PATH} 19 | cd ${NGX_PATH} && ./configure --with-http_ssl_module --with-cc=$(CC) --add-module=/root/project 20 | 21 | nginx: 22 | cd ${NGX_PATH} && rm -rf ${NGX_PATH}/objs/src/core/nginx.o && make 23 | 24 | vendor/cmocka: 25 | cd /root/project && git submodule init && git submodule update 26 | 27 | .cmocka_build: 28 | cd /root/project && git submodule init && git submodule update && mkdir .cmocka_build && cd .cmocka_build \ 29 | && cmake -DCMAKE_C_COMPILER=$(CC) -DCMAKE_MAKE_PROGRAM=make /root/project/vendor/cmocka \ 30 | && make && sudo make install 31 | 32 | test: .cmocka_build | nginx 33 | strip -N main -o ${NGX_PATH}/objs/src/core/nginx_without_main.o ${NGX_PATH}/objs/src/core/nginx.o \ 34 | && mv ${NGX_PATH}/objs/src/core/nginx_without_main.o ${NGX_PATH}/objs/src/core/nginx.o \ 35 | && $(CC) test_suite.c $(CFLAGS) -o test_suite -lcmocka `find ${NGX_PATH}/objs -name \*.o` -ldl -lpthread -lcrypt -lssl -lpcre -lcrypto -lz \ 36 | && ./test_suite 37 | 38 | clean: 39 | rm -f *.o test_suite 40 | 41 | # vim: ft=make ts=8 sw=8 noet 42 | -------------------------------------------------------------------------------- /crypto_helper_openssl.c: -------------------------------------------------------------------------------- 1 | /* openssl based implementation of crypto functions 2 | * 3 | * Contributors can provide alternate implementations in future and make 4 | * changes to the makefile to compile/link against these alternate crypto 5 | * libraries. The same approach can also be used to support multiple 6 | * versions of openssl in cases where openssl makes API incompatibile 7 | * releases. 8 | */ 9 | 10 | #include "crypto_helper.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | static const EVP_MD* evp_md = NULL; 19 | 20 | ngx_str_t* ngx_aws_auth__sign_sha256_hex(ngx_pool_t *pool, const ngx_str_t *blob, 21 | const ngx_str_t *signing_key) { 22 | 23 | unsigned int md_len; 24 | unsigned char md[EVP_MAX_MD_SIZE]; 25 | ngx_str_t *const retval = ngx_palloc(pool, sizeof(ngx_str_t)); 26 | 27 | if (evp_md==NULL) { 28 | evp_md = EVP_sha256(); 29 | } 30 | 31 | HMAC(evp_md, signing_key->data, signing_key->len, blob->data, blob->len, md, &md_len); 32 | retval->data = ngx_palloc(pool, md_len * 2 + 1); 33 | retval->len = md_len * 2; 34 | ngx_hex_dump(retval->data, md, md_len); 35 | return retval; 36 | } 37 | 38 | ngx_str_t* ngx_aws_auth__hash_sha256(ngx_pool_t *pool, const ngx_str_t *blob) { 39 | unsigned char hash[EVP_MAX_MD_SIZE]; 40 | unsigned int hash_len; 41 | ngx_str_t *const retval = ngx_palloc(pool, sizeof(ngx_str_t)); 42 | 43 | EVP_MD_CTX *mdctx; 44 | mdctx = EVP_MD_CTX_new(); 45 | 46 | if (mdctx == NULL) { 47 | // Handle error 48 | return NULL; 49 | } 50 | 51 | if((mdctx = EVP_MD_CTX_create()) == NULL) 52 | return NULL; 53 | 54 | if(1 != EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) 55 | return NULL; 56 | 57 | if(1 != EVP_DigestUpdate(mdctx, blob->data, blob->len)) 58 | return NULL; 59 | 60 | if(1 != EVP_DigestFinal_ex(mdctx, hash, &hash_len)) 61 | return NULL; 62 | 63 | EVP_MD_CTX_free(mdctx); 64 | 65 | retval->data = ngx_palloc(pool, hash_len * 2 + 1); 66 | retval->len = hash_len * 2; 67 | ngx_hex_dump(retval->data, hash, hash_len); 68 | return retval; 69 | } 70 | -------------------------------------------------------------------------------- /generate_signing_key: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import argparse 5 | import base64 6 | import hashlib 7 | import hmac 8 | import sys 9 | from datetime import datetime 10 | 11 | def sign(key, val): 12 | return hmac.new(key, val.encode('utf-8'), hashlib.sha256).digest() 13 | 14 | def getSignatureKey(key, dateStamp, regionName, serviceName): 15 | kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp) 16 | kRegion = sign(kDate, regionName) 17 | kService = sign(kRegion, serviceName) 18 | kSigning = sign(kService, "aws4_request") 19 | return kSigning 20 | 21 | def cmdline_parser(): 22 | parser = argparse.ArgumentParser(description="Generate AWS S3 signing key in it's base64 encoded form") 23 | parser.add_argument("-k", "--secret-key", required=True, help='The secret key generated using AWS IAM. Do not confuse this with the access key id') 24 | parser.add_argument("-r", "--region", required=True, help='The AWS region where this key would be used. Example: us-east-1') 25 | parser.add_argument("-s", "--service", help='The AWS service for which this key would be used. Example: s3') 26 | parser.add_argument("-d", "--date", help='The date on which this key is generated in yyyymmdd format') 27 | parser.add_argument("--no-base64", action='store_true', help='Disable output as a base64 encoded string. This NOT recommended') 28 | parser.add_argument("-v", "--verbose", action='store_true', help='Produce verbose output on stderr') 29 | return parser.parse_args() 30 | 31 | if __name__ == "__main__": 32 | args = cmdline_parser() 33 | verbose = args.verbose 34 | 35 | ymd = args.date 36 | if ymd is None: 37 | now = datetime.utcnow().date() 38 | ymd = '%04d%02d%02d' % (now.year, now.month, now.day) 39 | if verbose: 40 | print('The auto-selected date is %s' % ymd, file=sys.stderr) 41 | 42 | service = args.service 43 | if service is None: 44 | service = 's3' 45 | if verbose: 46 | print('The auto-selected service is %s' % service, file=sys.stderr) 47 | 48 | region = args.region 49 | signature = getSignatureKey(args.secret_key, ymd, region, service) 50 | 51 | if args.no_base64: 52 | signature = signature.decode('latin_1') 53 | else: 54 | signature = base64.b64encode(signature).decode('ascii') 55 | 56 | print(signature) 57 | print('%s/%s/%s/aws4_request' % (ymd, region, service)) 58 | -------------------------------------------------------------------------------- /reference-impl-py/reference_v2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from datetime import datetime 3 | from hashlib import sha1 4 | import hmac 5 | import sys 6 | 7 | try: 8 | from urllib.request import Request, urlopen, HTTPError # Python 3 9 | except: 10 | from urllib2 import Request, urlopen, HTTPError # Python 2 11 | 12 | ''' 13 | Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; 14 | 15 | Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ); 16 | 17 | StringToSign = HTTP-Verb + "\n" + 18 | Content-MD5 + "\n" + 19 | Content-Type + "\n" + 20 | Date + "\n" + 21 | CanonicalizedAmzHeaders + 22 | CanonicalizedResource; 23 | 24 | CanonicalizedResource = [ "/" + Bucket ] + 25 | + 26 | [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 27 | 28 | CanonicalizedAmzHeaders = 29 | ''' 30 | 31 | 32 | def canon_resource(vhost_mode, bucket, url): 33 | val = "/%s" % bucket if vhost_mode else "" 34 | val = val+url 35 | 36 | return val 37 | 38 | 39 | def str_to_sign_v2(method, vhost_mode, bucket, url): 40 | cr = canon_resource(vhost_mode, bucket, url) 41 | ctype = "" 42 | cmd5 = "" 43 | dt = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') 44 | azh = "" 45 | retval = "%s\n%s\n%s\n%s\n%s%s" % (method, 46 | cmd5, ctype, dt, azh, cr) 47 | headers = {} 48 | headers['Date'] = dt 49 | if vhost_mode: 50 | headers['Host'] = "%s.s3.amazonaws.com" % bucket 51 | return {'s2s': retval, 'headers': headers } 52 | 53 | 54 | def v2sign(key, method, vhost_mode, bucket, url): 55 | raw = str_to_sign_v2(method, vhost_mode, bucket, url) 56 | print "String to sign is\n----------------------\n%s\n---------------------\n" % raw['s2s'] 57 | retval = hmac.new(key, raw['s2s'], sha1) 58 | return {'sign': retval.digest().encode("base64").rstrip("\n"), 59 | 'headers': raw['headers']} 60 | 61 | def az_h(ak, key, method, vhost_mode, bucket, url): 62 | sig = v2sign(key, method, vhost_mode, bucket, url) 63 | ahv = "AWS %s:%s" % (ak, sig['sign']) 64 | sig['headers']['Authorization'] = ahv 65 | return sig['headers'] 66 | 67 | 68 | def get_data(ak, key, method, vhost_mode, bucket, url): 69 | if vhost_mode: 70 | rurl = "http://%s.s3.amazonaws.com%s" % (bucket, url) 71 | else: 72 | rurl = "http://s3.amazonaws.com%s" % (url) 73 | q = Request(rurl) 74 | headers = az_h(ak, key, method, vhost_mode, bucket, url) 75 | print 'About to make a request' 76 | print url 77 | print headers 78 | for k,v in headers.iteritems(): 79 | q.add_header(k, v) 80 | try: 81 | return urlopen(q).read() 82 | except HTTPError as e: 83 | print 'Got exception', e 84 | 85 | 86 | if __name__ == "__main__": 87 | ak = sys.argv[1] 88 | k = sys.argv[2] 89 | print get_data(ak, k, "GET", True, "hw.anomalizer", "/lock.txt") 90 | print get_data(ak, k, "GET", False, "hw.anomalizer", "/hw.anomalizer/nq.c") 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS proxy module 2 | 3 | [![Build Status](https://travis-ci.com/anomalizer/ngx_aws_auth.svg?branch=master)](https://travis-ci.com/anomalizer/ngx_aws_auth) 4 | [![Gitter chat](https://badges.gitter.im/anomalizer/ngx_aws_auth.png)](https://gitter.im/ngx_aws_auth/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) 5 | 6 | This nginx module can proxy requests to authenticated S3 backends using Amazon's 7 | V4 authentication API. The first version of this module was written for the V2 8 | authentication protocol and can be found in the *AuthV2* branch. 9 | 10 | ## License 11 | This project uses the same license as ngnix does i.e. the 2 clause BSD / simplified BSD / FreeBSD license 12 | 13 | ## Usage example 14 | 15 | Implements proxying of authenticated requests to S3. 16 | 17 | ```nginx 18 | server { 19 | listen 8000; 20 | 21 | aws_access_key your_aws_access_key; # Example AKIDEXAMPLE 22 | aws_key_scope scope_of_generated_signing_key; #Example 20150830/us-east-1/service/aws4_request 23 | aws_signing_key signing_key_generated_using_script; #Example L4vRLWAO92X5L3Sqk5QydUSdB0nC9+1wfqLMOKLbRp4= 24 | aws_s3_bucket your_s3_bucket; 25 | 26 | location / { 27 | aws_sign; 28 | proxy_pass http://your_s3_bucket.s3.amazonaws.com; 29 | } 30 | 31 | # This is an example that does not use the server root for the proxy root 32 | location /myfiles { 33 | 34 | rewrite /myfiles/(.*) /$1 break; 35 | proxy_pass http://your_s3_bucket.s3.amazonaws.com/$1; 36 | 37 | aws_access_key your_aws_access_key; 38 | aws_key_scope scope_of_generated_signing_key; 39 | aws_signing_key signing_key_generated_using_script; 40 | } 41 | 42 | # This is an example that use specific s3 endpoint, default endpoint is s3.amazonaws.com 43 | location /s3_beijing { 44 | 45 | rewrite /s3_beijing/(.*) /$1 break; 46 | proxy_pass http://your_s3_bucket.s3.cn-north-1.amazonaws.com.cn/$1; 47 | 48 | aws_sign; 49 | aws_endpoint "s3.cn-north-1.amazonaws.com.cn"; 50 | aws_access_key your_aws_access_key; 51 | aws_key_scope scope_of_generated_signing_key; 52 | aws_signing_key signing_key_generated_using_script; 53 | } 54 | } 55 | ``` 56 | 57 | ## Security considerations 58 | The V4 protocol does not need access to the actual secret keys that one obtains 59 | from the IAM service. The correct way to use the IAM key is to actually generate 60 | a scoped signing key and use this signing key to access S3. This nginx module 61 | requires the signing key and not the actual secret key. It is an insecure practise 62 | to let the secret key reside on your nginx server. 63 | 64 | Note that signing keys have a validity of just one week. Hence, they need to 65 | be refreshed constantly. Please useyour favourite configuration management 66 | system such as saltstack, puppet, chef, etc. etc. to distribute the signing 67 | keys to your nginx clusters. Do not forget to HUP the server after placing the new 68 | signing key as nginx reads the configuration only at startup time. 69 | 70 | A standalone python script has been provided to generate the signing key 71 | ``` 72 | ./generate_signing_key -h 73 | usage: generate_signing_key [-h] -k SECRET_KEY -r REGION [-s SERVICE] 74 | [-d DATE] [--no-base64] [-v] 75 | 76 | Generate AWS S3 signing key in it's base64 encoded form 77 | 78 | optional arguments: 79 | -h, --help show this help message and exit 80 | -k SECRET_KEY, --secret-key SECRET_KEY 81 | The secret key generated using AWS IAM. Do not confuse 82 | this with the access key id 83 | -r REGION, --region REGION 84 | The AWS region where this key would be used. Example: 85 | us-east-1 86 | -s SERVICE, --service SERVICE 87 | The AWS service for which this key would be used. 88 | Example: s3 89 | -d DATE, --date DATE The date on which this key is generated in yyyymmdd 90 | format 91 | --no-base64 Disable output as a base64 encoded string. This NOT 92 | recommended 93 | -v, --verbose Produce verbose output on stderr 94 | 95 | 96 | ./generate_signing_key -k wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY -r us-east-1 97 | L4vRLWAO92X5L3Sqk5QydUSdB0nC9+1wfqLMOKLbRp4= 98 | 20160902/us-east-1/s3/aws4_request 99 | 100 | ``` 101 | ## Supported environments 102 | This plugin is tested against a variety of nginx versions, compilers, OS versions and hardware architectures. Take a look at the .travis.yml file or the latest travis build status to see the versions that the plugin has been tested against 103 | 104 | 105 | ## Known limitations 106 | The 2.x version of the module currently only has support for GET and HEAD calls. This is because 107 | signing request body is complex and has not yet been implemented. 108 | 109 | 110 | 111 | ## Credits 112 | Original idea based on http://nginx.org/pipermail/nginx/2010-February/018583.html and suggestion of moving to variables rather than patching the proxy module. 113 | 114 | Subsequent contributions can be found in the commit logs of the project. 115 | -------------------------------------------------------------------------------- /reference-impl-py/reference_v4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from datetime import datetime 3 | from hashlib import sha1, sha256 4 | import hmac 5 | import base64 6 | import sys 7 | from xml.dom.minidom import parseString 8 | import unittest 9 | 10 | try: 11 | from urllib.request import Request, urlopen, HTTPError # Python 3 12 | except: 13 | from urllib2 import Request, urlopen, HTTPError # Python 2 14 | 15 | ''' 16 | 17 | CanonicalRequest 18 | ================ 19 | \n 20 | \n 21 | \n 22 | \n 23 | \n 24 | 25 | 26 | 27 | 28 | String to Sign 29 | ============== 30 | "AWS4-HMAC-SHA256" + "\n" + 31 | timeStampISO8601Format + "\n" + 32 | + "\n" + 33 | Hex(SHA256Hash()) 34 | 35 | 36 | SigningKey 37 | ========== 38 | DateKey = HMAC-SHA256("AWS4"+"", "") 39 | DateRegionKey = HMAC-SHA256(, "") 40 | DateRegionServiceKey = HMAC-SHA256(, "") 41 | SigningKey = HMAC-SHA256(, "aws4_request") 42 | 43 | 44 | 45 | HMAC-SHA256(SigningKey, StringToSign) 46 | 47 | 48 | ''' 49 | 50 | def long_date(yyyymmdd): 51 | return yyyymmdd+'T000000Z' 52 | 53 | 54 | def canon_querystring(qs_map): 55 | return {'cqs':'', 'qsmap':{}} # TODO: impl 56 | 57 | 58 | def make_headers(req_time, bucket, aws_headers, content_hash): 59 | headers = [] 60 | headers.append(['x-amz-content-sha256', content_hash]) 61 | headers.append(['x-amz-date', req_time]) 62 | headers.append(['Host', '%s.s3.amazonaws.com' % (bucket)]) 63 | 64 | hmap = {} 65 | for x in headers: 66 | x[0] = x[0].lower() 67 | x[1] = x[1].strip() 68 | hmap[x[0]] = x[1] 69 | 70 | headers.sort(key =lambda x:x[0]) 71 | 72 | signed_headers = ';'.join([x[0] for x in headers]) 73 | canon_headers = '' 74 | for h in [':'.join(x) for x in headers]: 75 | canon_headers = '%s%s\n' % (canon_headers, h) 76 | 77 | return {'hmap': hmap, 'sh': signed_headers, 'ch': canon_headers } 78 | 79 | 80 | def canon_request(req_time, bucket, url, qs_map, aws_headers): 81 | qs = canon_querystring(qs_map) 82 | payload_hash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' #hardcoded 83 | header_info = make_headers(req_time, bucket, None, payload_hash) 84 | cr = "\n".join(('GET', url, qs['cqs'], header_info['ch'], header_info['sh'], payload_hash)) # hardcoded method 85 | print cr 86 | 87 | return {'cr_str': cr, 'headers': header_info['hmap'], 'qs': qs, 'sh': header_info['sh']} 88 | 89 | 90 | def sign_body(body=None): 91 | return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' # TODO: fix hardcoding 92 | 93 | 94 | def get_scope(dt, region): 95 | return '%s/%s/s3/aws4_request' % (dt, region) 96 | 97 | def str_to_sign_v4(req_time, scope, bucket, url, qs_map, aws_headers): 98 | cr_info = canon_request(req_time, bucket, url, qs_map, aws_headers) 99 | h265 = sha256() 100 | h265.update(cr_info['cr_str']) 101 | hd = h265.hexdigest() 102 | s2s = "\n".join(('AWS4-HMAC-SHA256', cr_info['headers']['x-amz-date'], scope, hd)) 103 | print s2s 104 | return {'s2s': s2s, 'headers': cr_info['headers'], 'qs':cr_info['qs'], 'scope': scope, 'sh': cr_info['sh']} 105 | 106 | def sign(req_time, access_id, key, scope, bucket, url, qs_map, aws_headers): 107 | s2s = str_to_sign_v4(req_time, scope, bucket, url, qs_map, aws_headers) 108 | retval = hmac.new(key, s2s['s2s'], sha256) 109 | sig = retval.hexdigest() 110 | auth_header = 'AWS4-HMAC-SHA256 Credential=%s/%s,SignedHeaders=%s,Signature=%s' % ( 111 | access_id, s2s['scope'], s2s['sh'], sig) 112 | s2s['headers']['Authorization'] = auth_header 113 | return {'headers': s2s['headers'], 'qs':s2s['qs'], 'sig': sig} 114 | 115 | 116 | def get_data(req_time, access_id, key, scope, bucket, url, qs_map, aws_headers): 117 | s = sign(req_time, access_id, key, scope, bucket, url, qs_map, aws_headers) 118 | rurl = "http://%s.s3.amazonaws.com%s" % (bucket, url) 119 | # print rurl 120 | # print s 121 | q = Request(rurl) 122 | for k,v in s['headers'].iteritems(): 123 | q.add_header(k, v) 124 | try: 125 | return urlopen(q).read() 126 | except HTTPError as e: 127 | exml = "".join(e.readlines()) 128 | xml = parseString(exml) 129 | print 'Got exception\n-------------------------\n\n', xml.toprettyxml() 130 | 131 | ''' 132 | if __name__ == '__main__': 133 | aid = sys.argv[1] 134 | b64_key = sys.argv[2] 135 | scope = sys.argv[3] 136 | request_time = datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') if len(sys.argv) == 4 else sys.argv[4] 137 | print "Request time is %s" % request_time 138 | get_data(request_time, aid, base64.b64decode(b64_key), scope, 'hw.anomalizer', '/lock.txt', {}, {}) 139 | ''' 140 | 141 | class TestStringMethods(unittest.TestCase): 142 | def test_simple_get(self): 143 | now = '20150830T123600Z' 144 | key = base64.b64decode('k4EntTNoEN22pdavRF/KyeNx+e1BjtOGsCKu2CkBvnU=') 145 | aid = 'AKIDEXAMPLE' 146 | scope = '20150830/us-east-1/service/aws4_request' 147 | s = sign(now, aid, key, scope, 'example', '/', {}, {}) 148 | self.assertEqual(s['sig'], '5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31') 149 | 150 | 151 | if __name__ == '__main__': 152 | unittest.main() 153 | -------------------------------------------------------------------------------- /ngx_http_aws_auth.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "aws_functions.h" 5 | 6 | #define AWS_S3_VARIABLE "s3_auth_token" 7 | #define AWS_DATE_VARIABLE "aws_date" 8 | 9 | static void* ngx_http_aws_auth_create_loc_conf(ngx_conf_t *cf); 10 | static char* ngx_http_aws_auth_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); 11 | static ngx_int_t ngx_aws_auth_req_init(ngx_conf_t *cf); 12 | static char * ngx_http_aws_endpoint(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 13 | static char* ngx_http_aws_sign(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 14 | 15 | typedef struct { 16 | ngx_str_t access_key; 17 | ngx_str_t key_scope; 18 | ngx_str_t signing_key; 19 | ngx_str_t signing_key_decoded; 20 | ngx_str_t endpoint; 21 | ngx_str_t bucket_name; 22 | ngx_uint_t enabled; 23 | } ngx_http_aws_auth_conf_t; 24 | 25 | 26 | static ngx_command_t ngx_http_aws_auth_commands[] = { 27 | { ngx_string("aws_access_key"), 28 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 29 | ngx_conf_set_str_slot, 30 | NGX_HTTP_LOC_CONF_OFFSET, 31 | offsetof(ngx_http_aws_auth_conf_t, access_key), 32 | NULL }, 33 | 34 | { ngx_string("aws_key_scope"), 35 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 36 | ngx_conf_set_str_slot, 37 | NGX_HTTP_LOC_CONF_OFFSET, 38 | offsetof(ngx_http_aws_auth_conf_t, key_scope), 39 | NULL }, 40 | 41 | { ngx_string("aws_signing_key"), 42 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 43 | ngx_conf_set_str_slot, 44 | NGX_HTTP_LOC_CONF_OFFSET, 45 | offsetof(ngx_http_aws_auth_conf_t, signing_key), 46 | NULL }, 47 | 48 | { ngx_string("aws_endpoint"), 49 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 50 | ngx_http_aws_endpoint, 51 | NGX_HTTP_LOC_CONF_OFFSET, 52 | offsetof(ngx_http_aws_auth_conf_t, endpoint), 53 | NULL }, 54 | 55 | { ngx_string("aws_s3_bucket"), 56 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 57 | ngx_conf_set_str_slot, 58 | NGX_HTTP_LOC_CONF_OFFSET, 59 | offsetof(ngx_http_aws_auth_conf_t, bucket_name), 60 | NULL }, 61 | 62 | { ngx_string("aws_sign"), 63 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, 64 | ngx_http_aws_sign, 65 | 0, 66 | 0, 67 | NULL }, 68 | 69 | ngx_null_command 70 | }; 71 | 72 | static ngx_http_module_t ngx_http_aws_auth_module_ctx = { 73 | NULL, /* preconfiguration */ 74 | ngx_aws_auth_req_init, /* postconfiguration */ 75 | 76 | NULL, /* create main configuration */ 77 | NULL, /* init main configuration */ 78 | 79 | NULL, /* create server configuration */ 80 | NULL, /* merge server configuration */ 81 | 82 | ngx_http_aws_auth_create_loc_conf, /* create location configuration */ 83 | ngx_http_aws_auth_merge_loc_conf /* merge location configuration */ 84 | }; 85 | 86 | 87 | ngx_module_t ngx_http_aws_auth_module = { 88 | NGX_MODULE_V1, 89 | &ngx_http_aws_auth_module_ctx, /* module context */ 90 | ngx_http_aws_auth_commands, /* module directives */ 91 | NGX_HTTP_MODULE, /* module type */ 92 | NULL, /* init master */ 93 | NULL, /* init module */ 94 | NULL, /* init process */ 95 | NULL, /* init thread */ 96 | NULL, /* exit thread */ 97 | NULL, /* exit process */ 98 | NULL, /* exit master */ 99 | NGX_MODULE_V1_PADDING 100 | }; 101 | 102 | static void * 103 | ngx_http_aws_auth_create_loc_conf(ngx_conf_t *cf) 104 | { 105 | ngx_http_aws_auth_conf_t *conf; 106 | 107 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_aws_auth_conf_t)); 108 | conf->enabled = 0; 109 | ngx_str_set(&conf->endpoint, "s3.amazonaws.com"); 110 | if (conf == NULL) { 111 | return NGX_CONF_ERROR; 112 | } 113 | 114 | return conf; 115 | } 116 | 117 | static char * 118 | ngx_http_aws_auth_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 119 | { 120 | ngx_http_aws_auth_conf_t *prev = parent; 121 | ngx_http_aws_auth_conf_t *conf = child; 122 | 123 | ngx_conf_merge_str_value(conf->access_key, prev->access_key, ""); 124 | ngx_conf_merge_str_value(conf->key_scope, prev->key_scope, ""); 125 | ngx_conf_merge_str_value(conf->signing_key, prev->signing_key, ""); 126 | ngx_conf_merge_str_value(conf->endpoint, prev->endpoint, "s3.amazonaws.com"); 127 | ngx_conf_merge_str_value(conf->bucket_name, prev->bucket_name, ""); 128 | 129 | if(conf->signing_key_decoded.data == NULL) 130 | { 131 | conf->signing_key_decoded.data = ngx_pcalloc(cf->pool, 100); 132 | if(conf->signing_key_decoded.data == NULL) 133 | { 134 | return NGX_CONF_ERROR; 135 | } 136 | } 137 | 138 | if(conf->signing_key.len > 64) { 139 | return NGX_CONF_ERROR; 140 | } else { 141 | ngx_decode_base64(&conf->signing_key_decoded, &conf->signing_key); 142 | } 143 | 144 | return NGX_CONF_OK; 145 | } 146 | 147 | static ngx_int_t 148 | ngx_http_aws_proxy_sign(ngx_http_request_t *r) 149 | { 150 | ngx_http_aws_auth_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_aws_auth_module); 151 | if(!conf->enabled) { 152 | /* return directly if module is not enabled */ 153 | return NGX_DECLINED; 154 | } 155 | ngx_table_elt_t *h; 156 | header_pair_t *hv; 157 | 158 | if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) { 159 | /* We do not wish to support anything with a body as signing for a body is unimplemented */ 160 | return NGX_HTTP_NOT_ALLOWED; 161 | } 162 | 163 | const ngx_array_t* headers_out = ngx_aws_auth__sign(r->pool, r, 164 | &conf->access_key, &conf->signing_key_decoded, &conf->key_scope, &conf->bucket_name, &conf->endpoint); 165 | 166 | ngx_uint_t i; 167 | for(i = 0; i < headers_out->nelts; i++) 168 | { 169 | hv = (header_pair_t*)((u_char *) headers_out->elts + headers_out->size * i); 170 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 171 | "header name %s, value %s", hv->key.data, hv->value.data); 172 | 173 | if(ngx_strncmp(hv->key.data, HOST_HEADER.data, hv->key.len) == 0) { 174 | /* host header is controlled by proxy pass directive and hence 175 | cannot be set by our module */ 176 | continue; 177 | } 178 | 179 | h = ngx_list_push(&r->headers_in.headers); 180 | if (h == NULL) { 181 | return NGX_ERROR; 182 | } 183 | 184 | h->hash = 1; 185 | h->key = hv->key; 186 | h->lowcase_key = hv->key.data; /* We ensure that header names are already lowercased */ 187 | h->value = hv->value; 188 | } 189 | return NGX_OK; 190 | } 191 | 192 | static char * 193 | ngx_http_aws_endpoint(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 194 | { 195 | char *p = conf; 196 | 197 | ngx_str_t *field, *value; 198 | 199 | field = (ngx_str_t *)(p + cmd->offset); 200 | 201 | value = cf->args->elts; 202 | 203 | *field = value[1]; 204 | 205 | return NGX_CONF_OK; 206 | } 207 | 208 | static char * 209 | ngx_http_aws_sign(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 210 | { 211 | /* 212 | ngx_http_core_loc_conf_t *clcf; 213 | 214 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 215 | clcf->handler = ngx_http_aws_proxy_sign; 216 | */ 217 | ngx_http_aws_auth_conf_t *mconf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_aws_auth_module); 218 | mconf->enabled = 1; 219 | 220 | return NGX_CONF_OK; 221 | } 222 | 223 | static ngx_int_t 224 | ngx_aws_auth_req_init(ngx_conf_t *cf) 225 | { 226 | ngx_http_handler_pt *h; 227 | ngx_http_core_main_conf_t *cmcf; 228 | 229 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 230 | 231 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 232 | if (h == NULL) { 233 | return NGX_ERROR; 234 | } 235 | 236 | *h = ngx_http_aws_proxy_sign; 237 | 238 | return NGX_OK; 239 | } 240 | /* 241 | * vim: ts=4 sw=4 et 242 | */ 243 | 244 | -------------------------------------------------------------------------------- /test_suite.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "vendor/cmocka/include/cmocka.h" 6 | 7 | #include "aws_functions.h" 8 | 9 | ngx_pool_t *pool; 10 | 11 | static void assert_ngx_string_equal(ngx_str_t a, ngx_str_t b) { 12 | int len = a.len < b.len ? a.len : b.len; 13 | assert_memory_equal(a.data, b.data, len); 14 | } 15 | 16 | static void null_test_success(void **state) { 17 | (void) state; /* unused */ 18 | } 19 | 20 | static void host_header_ctor(void **state) { 21 | ngx_str_t bucket; 22 | const ngx_str_t* host; 23 | 24 | (void) state; /* unused */ 25 | 26 | bucket.data = "test-es-three"; 27 | bucket.len = strlen(bucket.data); 28 | host = ngx_aws_auth__host_from_bucket(pool, &bucket); 29 | assert_string_equal("test-es-three.s3.amazonaws.com", host->data); 30 | 31 | bucket.data = "complex.sub.domain.test"; 32 | bucket.len = strlen(bucket.data); 33 | host = ngx_aws_auth__host_from_bucket(pool, &bucket); 34 | assert_string_equal("complex.sub.domain.test.s3.amazonaws.com", host->data); 35 | } 36 | 37 | static void x_amz_date(void **state) { 38 | time_t t; 39 | const ngx_str_t* date; 40 | 41 | (void) state; /* unused */ 42 | 43 | t = 1; 44 | date = ngx_aws_auth__compute_request_time(pool, &t); 45 | assert_int_equal(date->len, 16); 46 | assert_string_equal("19700101T000001Z", date->data); 47 | 48 | t = 1456036272; 49 | date = ngx_aws_auth__compute_request_time(pool, &t); 50 | assert_int_equal(date->len, 16); 51 | assert_string_equal("20160221T063112Z", date->data); 52 | } 53 | 54 | 55 | static void hmac_sha256(void **state) { 56 | ngx_str_t key; 57 | ngx_str_t text; 58 | ngx_str_t* hash; 59 | (void) state; /* unused */ 60 | 61 | key.data = "abc"; key.len=3; 62 | text.data = "asdf"; text.len=4; 63 | hash = ngx_aws_auth__sign_sha256_hex(pool, &text, &key); 64 | assert_int_equal(64, hash->len); 65 | assert_string_equal("07e434c45d15994e620bf8e43da6f652d331989be1783cdfcc989ddb0a2358e2", hash->data); 66 | 67 | key.data = "\011\001\057asf"; key.len=6; 68 | text.data = "lorem ipsum"; text.len=11; 69 | hash = ngx_aws_auth__sign_sha256_hex(pool, &text, &key); 70 | assert_int_equal(64, hash->len); 71 | assert_string_equal("827ce31c45e77292af25fef980c3e7afde23abcde622ecd8e82e1be6dd94fad3", hash->data); 72 | } 73 | 74 | 75 | static void sha256(void **state) { 76 | ngx_str_t text; 77 | ngx_str_t* hash; 78 | (void) state; /* unused */ 79 | 80 | text.data = "asdf"; text.len=4; 81 | hash = ngx_aws_auth__hash_sha256(pool, &text); 82 | assert_int_equal(64, hash->len); 83 | assert_string_equal("f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b", hash->data); 84 | 85 | text.len=0; 86 | hash = ngx_aws_auth__hash_sha256(pool, &text); 87 | assert_int_equal(64, hash->len); 88 | assert_string_equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash->data); 89 | } 90 | 91 | static void canon_header_string(void **state) { 92 | (void) state; /* unused */ 93 | 94 | ngx_str_t bucket, date, hash, endpoint; 95 | struct AwsCanonicalHeaderDetails retval; 96 | 97 | bucket.data = "bugait"; bucket.len = 6; 98 | date.data = "20160221T063112Z"; date.len = 16; 99 | hash.data = "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"; hash.len = 64; 100 | endpoint.data = "s3.amazonaws.com"; endpoint.len = 16; 101 | 102 | retval = ngx_aws_auth__canonize_headers(pool, NULL, &bucket, &date, &hash, &endpoint); 103 | assert_string_equal(retval.canon_header_str->data, 104 | "host:bugait.s3.amazonaws.com\nx-amz-content-sha256:f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b\nx-amz-date:20160221T063112Z\n"); 105 | } 106 | 107 | static void signed_headers(void **state) { 108 | (void) state; /* unused */ 109 | 110 | ngx_str_t bucket, date, hash, endpoint; 111 | struct AwsCanonicalHeaderDetails retval; 112 | 113 | bucket.data = "bugait"; bucket.len = 6; 114 | date.data = "20160221T063112Z"; date.len = 16; 115 | hash.data = "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"; hash.len = 64; 116 | endpoint.data = "s3.amazonaws.com"; endpoint.len = 16; 117 | 118 | retval = ngx_aws_auth__canonize_headers(pool, NULL, &bucket, &date, &hash, &endpoint); 119 | assert_string_equal(retval.signed_header_names->data, "host;x-amz-content-sha256;x-amz-date"); 120 | } 121 | 122 | static void canonical_qs_empty(void **state) { 123 | (void) state; /* unused */ 124 | ngx_http_request_t request; 125 | request.args = EMPTY_STRING; 126 | request.connection = NULL; 127 | 128 | const ngx_str_t *canon_qs = ngx_aws_auth__canonize_query_string(pool, &request); 129 | assert_ngx_string_equal(*canon_qs, EMPTY_STRING); 130 | } 131 | 132 | static void canonical_qs_single_arg(void **state) { 133 | (void) state; /* unused */ 134 | ngx_http_request_t request; 135 | ngx_str_t args = ngx_string("arg1=val1"); 136 | request.args = args; 137 | request.connection = NULL; 138 | 139 | const ngx_str_t *canon_qs = ngx_aws_auth__canonize_query_string(pool, &request); 140 | assert_ngx_string_equal(*canon_qs, args); 141 | } 142 | 143 | static void canonical_qs_two_arg_reverse(void **state) { 144 | (void) state; /* unused */ 145 | ngx_http_request_t request; 146 | ngx_str_t args = ngx_string("brg1=val2&arg1=val1"); 147 | ngx_str_t cargs = ngx_string("arg1=val1&brg1=val"); 148 | request.args = args; 149 | request.connection = NULL; 150 | 151 | const ngx_str_t *canon_qs = ngx_aws_auth__canonize_query_string(pool, &request); 152 | assert_ngx_string_equal(*canon_qs, cargs); 153 | } 154 | 155 | static void canonical_qs_subrequest(void **state) { 156 | (void) state; /* unused */ 157 | ngx_http_request_t request; 158 | ngx_str_t args = ngx_string("acl"); 159 | ngx_str_t cargs = ngx_string("acl="); 160 | request.args = args; 161 | request.connection = NULL; 162 | 163 | const ngx_str_t *canon_qs = ngx_aws_auth__canonize_query_string(pool, &request); 164 | assert_ngx_string_equal(*canon_qs, cargs); 165 | } 166 | 167 | static void canonical_url_sans_qs(void **state) { 168 | (void) state; /* unused */ 169 | 170 | ngx_http_request_t request; 171 | ngx_str_t url = ngx_string("foo.php"); 172 | request.uri = url; 173 | request.uri_start = request.uri.data; 174 | request.args_start = url.data + url.len; 175 | request.args = EMPTY_STRING; 176 | request.connection = NULL; 177 | 178 | const ngx_str_t *canon_url = ngx_aws_auth__canon_url(pool, &request); 179 | assert_int_equal(canon_url->len, url.len); 180 | assert_ngx_string_equal(*canon_url, url); 181 | } 182 | 183 | static void canonical_url_with_qs(void **state) { 184 | (void) state; /* unused */ 185 | 186 | ngx_http_request_t request; 187 | ngx_str_t url = ngx_string("foo.php?arg1=var1"); 188 | ngx_str_t curl = ngx_string("foo.php"); 189 | 190 | ngx_str_t args; 191 | args.data = url.data + 8; 192 | args.len = 9; 193 | 194 | request.uri = url; 195 | request.uri_start = request.uri.data; 196 | request.args_start = url.data + 8; 197 | request.args = args; 198 | request.connection = NULL; 199 | 200 | const ngx_str_t *canon_url = ngx_aws_auth__canon_url(pool, &request); 201 | assert_int_equal(canon_url->len, curl.len); 202 | assert_ngx_string_equal(*canon_url, curl); 203 | } 204 | 205 | static void canonical_url_with_special_chars(void **state) { 206 | (void) state; /* unused */ 207 | 208 | ngx_str_t url = ngx_string("f&o@o/b ar.php"); 209 | ngx_str_t expected_canon_url = ngx_string("f%26o%40o/b%20ar.php"); 210 | 211 | ngx_http_request_t request; 212 | request.uri = url; 213 | request.uri_start = request.uri.data; 214 | request.args_start = url.data + url.len; 215 | request.args = EMPTY_STRING; 216 | request.connection = NULL; 217 | 218 | const ngx_str_t *canon_url = ngx_aws_auth__canon_url(pool, &request); 219 | assert_int_equal(canon_url->len, expected_canon_url.len); 220 | assert_ngx_string_equal(*canon_url, expected_canon_url); 221 | } 222 | 223 | static void canonical_request_sans_qs(void **state) { 224 | (void) state; /* unused */ 225 | const ngx_str_t bucket = ngx_string("example"); 226 | const ngx_str_t aws_date = ngx_string("20160221T063112Z"); 227 | const ngx_str_t url = ngx_string("/"); 228 | const ngx_str_t method = ngx_string("GET"); 229 | const ngx_str_t endpoint = ngx_string("s3.amazonaws.com"); 230 | 231 | struct AwsCanonicalRequestDetails result; 232 | ngx_http_request_t request; 233 | 234 | request.uri = url; 235 | request.method_name = method; 236 | request.args = EMPTY_STRING; 237 | request.connection = NULL; 238 | 239 | result = ngx_aws_auth__make_canonical_request(pool, &request, &bucket, &aws_date, &endpoint); 240 | assert_string_equal(result.canon_request->data, "GET\n\ 241 | /\n\ 242 | \n\ 243 | host:example.s3.amazonaws.com\n\ 244 | x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ 245 | x-amz-date:20160221T063112Z\n\ 246 | \n\ 247 | host;x-amz-content-sha256;x-amz-date\n\ 248 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 249 | } 250 | 251 | static void basic_get_signature(void **state) { 252 | (void) state; /* unused */ 253 | 254 | const ngx_str_t url = ngx_string("/"); 255 | const ngx_str_t method = ngx_string("GET"); 256 | const ngx_str_t key_scope = ngx_string("20150830/us-east-1/service/aws4_request"); 257 | const ngx_str_t bucket = ngx_string("example"); 258 | const ngx_str_t endpoint = ngx_string("s3.amazonaws.com"); 259 | 260 | ngx_str_t signing_key, signing_key_b64e = ngx_string("k4EntTNoEN22pdavRF/KyeNx+e1BjtOGsCKu2CkBvnU="); 261 | ngx_http_request_t request; 262 | 263 | request.start_sec = 1440938160; /* 20150830T123600Z */ 264 | request.uri = url; 265 | request.method_name = method; 266 | request.args = EMPTY_STRING; 267 | request.connection = NULL; 268 | 269 | signing_key.len = 64; 270 | signing_key.data = ngx_palloc(pool, signing_key.len ); 271 | ngx_decode_base64(&signing_key, &signing_key_b64e); 272 | 273 | struct AwsSignedRequestDetails result = ngx_aws_auth__compute_signature(pool, &request, 274 | &signing_key, &key_scope, &bucket, &endpoint); 275 | assert_string_equal(result.signature->data, "4ed4ec875ff02e55c7903339f4f24f8780b986a9cc9eff03f324d31da6a57690"); 276 | } 277 | 278 | int main() { 279 | const struct CMUnitTest tests[] = { 280 | cmocka_unit_test(null_test_success), 281 | cmocka_unit_test(x_amz_date), 282 | cmocka_unit_test(host_header_ctor), 283 | cmocka_unit_test(hmac_sha256), 284 | cmocka_unit_test(sha256), 285 | cmocka_unit_test(canon_header_string), 286 | cmocka_unit_test(canonical_qs_empty), 287 | cmocka_unit_test(canonical_qs_single_arg), 288 | cmocka_unit_test(canonical_qs_two_arg_reverse), 289 | cmocka_unit_test(canonical_qs_subrequest), 290 | cmocka_unit_test(canonical_url_sans_qs), 291 | cmocka_unit_test(canonical_url_with_qs), 292 | cmocka_unit_test(canonical_url_with_special_chars), 293 | cmocka_unit_test(signed_headers), 294 | cmocka_unit_test(canonical_request_sans_qs), 295 | cmocka_unit_test(basic_get_signature), 296 | }; 297 | 298 | pool = ngx_create_pool(1000000, NULL); 299 | 300 | return cmocka_run_group_tests(tests, NULL, NULL); 301 | } 302 | -------------------------------------------------------------------------------- /aws_functions.h: -------------------------------------------------------------------------------- 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 27 | #include 28 | #include 29 | #include 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 | 70 | static inline char* __CHAR_PTR_U(u_char* ptr) {return (char*)ptr;} 71 | static inline const char* __CONST_CHAR_PTR_U(const u_char* ptr) {return (const char*)ptr;} 72 | 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 | 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 | 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 | 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 | 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 + s3_endpoint->len + 2; 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 | 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 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 | 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 | 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 | 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 | 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 | 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 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 | --------------------------------------------------------------------------------