├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── awsauth.go ├── awsauth_test.go ├── common.go ├── common_test.go ├── s3.go ├── s3_test.go ├── sign2.go ├── sign2_test.go ├── sign3.go ├── sign3_test.go ├── sign4.go └── sign4_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | In general, the code posted to the [SmartyStreets github organization](https://github.com/smartystreets) is created to solve specific problems at SmartyStreets that are ancillary to our core products in the address verification industry and may or may not be useful to other organizations or developers. Our reason for posting said code isn't necessarily to solicit feedback or contributions from the community but more as a showcase of some of the approaches to solving problems we have adopted. 4 | 5 | Having stated that, we do consider issues raised by other githubbers as well as contributions submitted via pull requests. When submitting such a pull request, please follow these guidelines: 6 | 7 | - _Look before you leap:_ If the changes you plan to make are significant, it's in everyone's best interest for you to discuss them with a SmartyStreets team member prior to opening a pull request. 8 | - _License and ownership:_ If modifying the `LICENSE.md` file, limit your changes to fixing typographical mistakes. Do NOT modify the actual terms in the license or the copyright by **SmartyStreets, LLC**. Code submitted to SmartyStreets projects becomes property of SmartyStreets and must be compatible with the associated license. 9 | - _Testing:_ If the code you are submitting resides in packages/modules covered by automated tests, be sure to add passing tests that cover your changes and assert expected behavior and state. Submit the additional test cases as part of your change set. 10 | - _Style:_ Match your approach to **naming** and **formatting** with the surrounding code. Basically, the code you submit shouldn't stand out. 11 | - "Naming" refers to such constructs as variables, methods, functions, classes, structs, interfaces, packages, modules, directories, files, etc... 12 | - "Formatting" refers to such constructs as whitespace, horizontal line length, vertical function length, vertical file length, indentation, curly braces, etc... 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 SmartyStreets 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | NOTE: Various optional and subordinate components carry their own licensing 22 | requirements and restrictions. Use of those components is subject to the terms 23 | and conditions outlined the respective license of each component. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-aws-auth 2 | =========== 3 | 4 | [![GoDoc](https://godoc.org/github.com/smartystreets/go-aws-auth?status.svg)](http://godoc.org/github.com/smartystreets/go-aws-auth) 5 | 6 | Go-AWS-Auth is a comprehensive, lightweight library for signing requests to Amazon Web Services. 7 | 8 | It's easy to use: simply build your HTTP request and call `awsauth.Sign(req)` before sending your request over the wire. 9 | 10 | 11 | 12 | ### Supported signing mechanisms 13 | 14 | - [Signed Signature Version 2](http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html) 15 | - [Signed Signature Version 3](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html) 16 | - [Signed Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) 17 | - [Custom S3 Authentication Scheme](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) 18 | - [Security Token Service](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html) 19 | - [S3 Query String Authentication](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth) 20 | - [IAM Role](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials) 21 | 22 | For more info about AWS authentication, see the [comprehensive docs](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html) at AWS. 23 | 24 | 25 | ### Install 26 | 27 | Go get it: 28 | 29 | $ go get github.com/smartystreets/go-aws-auth 30 | 31 | Then import it: 32 | 33 | import "github.com/smartystreets/go-aws-auth" 34 | 35 | 36 | ### Using your AWS Credentials 37 | 38 | The library looks for credentials in this order: 39 | 40 | 1. **Hard-code:** You can manually pass in an instance of `awsauth.Credentials` to any call to a signing function as a second argument: 41 | 42 | ```go 43 | awsauth.Sign(req, awsauth.Credentials{ 44 | AccessKeyID: "Access Key ID", 45 | SecretAccessKey: "Secret Access Key", 46 | SecurityToken: "Security Token", // STS (optional) 47 | }) 48 | ``` 49 | 50 | 51 | 2. **Environment variables:** Set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables with your credentials. The library will automatically detect and use them. Optionally, you may also set the `AWS_SECURITY_TOKEN` environment variable if you are using temporary credentials from [STS](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html). 52 | 53 | 3. **IAM Role:** If running on EC2 and the credentials are neither hard-coded nor in the environment, go-aws-auth will detect the first IAM role assigned to the current EC2 instance and use those credentials. 54 | 55 | (Be especially careful hard-coding credentials into your application if the code is committed to source control.) 56 | 57 | 58 | 59 | ### Signing requests 60 | 61 | Just make the request, have it signed, and perform the request as you normally would. 62 | 63 | ```go 64 | url := "https://iam.amazonaws.com/?Action=ListRoles&Version=2010-05-08" 65 | client := new(http.Client) 66 | 67 | req, err := http.NewRequest("GET", url, nil) 68 | 69 | awsauth.Sign(req) // Automatically chooses the best signing mechanism for the service 70 | 71 | resp, err := client.Do(req) 72 | ``` 73 | 74 | You can use `Sign` to have the library choose the best signing algorithm depending on the service, or you can specify it manually if you know what you need: 75 | 76 | - `Sign2` 77 | - `Sign3` 78 | - `Sign4` 79 | - `SignS3` (deprecated for Sign4) 80 | - `SignS3Url` (for pre-signed S3 URLs; GETs only) 81 | 82 | 83 | 84 | ### Contributing 85 | 86 | Please feel free to contribute! Bug fixes are more than welcome any time, as long as tests assert correct behavior. If you'd like to change an existing implementation or see a new feature, open an issue first so we can discuss it. Thanks to all contributors! 87 | -------------------------------------------------------------------------------- /awsauth.go: -------------------------------------------------------------------------------- 1 | // Package awsauth implements AWS request signing using Signed Signature Version 2, 2 | // Signed Signature Version 3, and Signed Signature Version 4. Supports S3 and STS. 3 | package awsauth 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // Credentials stores the information necessary to authorize with AWS and it 12 | // is from this information that requests are signed. 13 | type Credentials struct { 14 | AccessKeyID string 15 | SecretAccessKey string 16 | SecurityToken string `json:"Token"` 17 | Expiration time.Time 18 | } 19 | 20 | // Sign signs a request bound for AWS. It automatically chooses the best 21 | // authentication scheme based on the service the request is going to. 22 | func Sign(request *http.Request, credentials ...Credentials) *http.Request { 23 | service, _ := serviceAndRegion(request.URL.Host) 24 | signVersion := awsSignVersion[service] 25 | 26 | switch signVersion { 27 | case 2: 28 | return Sign2(request, credentials...) 29 | case 3: 30 | return Sign3(request, credentials...) 31 | case 4: 32 | return Sign4(request, credentials...) 33 | case -1: 34 | return SignS3(request, credentials...) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // Sign4 signs a request with Signed Signature Version 4. 41 | func Sign4(request *http.Request, credentials ...Credentials) *http.Request { 42 | keys := chooseKeys(credentials) 43 | 44 | // Add the X-Amz-Security-Token header when using STS 45 | if keys.SecurityToken != "" { 46 | request.Header.Set("X-Amz-Security-Token", keys.SecurityToken) 47 | } 48 | 49 | prepareRequestV4(request) 50 | meta := new(metadata) 51 | 52 | // Task 1 53 | hashedCanonReq := hashedCanonicalRequestV4(request, meta) 54 | 55 | // Task 2 56 | stringToSign := stringToSignV4(request, hashedCanonReq, meta) 57 | 58 | // Task 3 59 | signingKey := signingKeyV4(keys.SecretAccessKey, meta.date, meta.region, meta.service) 60 | signature := signatureV4(signingKey, stringToSign) 61 | 62 | request.Header.Set("Authorization", buildAuthHeaderV4(signature, meta, keys)) 63 | 64 | return request 65 | } 66 | 67 | // Sign3 signs a request with Signed Signature Version 3. 68 | // If the service you're accessing supports Version 4, use that instead. 69 | func Sign3(request *http.Request, credentials ...Credentials) *http.Request { 70 | keys := chooseKeys(credentials) 71 | 72 | // Add the X-Amz-Security-Token header when using STS 73 | if keys.SecurityToken != "" { 74 | request.Header.Set("X-Amz-Security-Token", keys.SecurityToken) 75 | } 76 | 77 | prepareRequestV3(request) 78 | 79 | // Task 1 80 | stringToSign := stringToSignV3(request) 81 | 82 | // Task 2 83 | signature := signatureV3(stringToSign, keys) 84 | 85 | // Task 3 86 | request.Header.Set("X-Amzn-Authorization", buildAuthHeaderV3(signature, keys)) 87 | 88 | return request 89 | } 90 | 91 | // Sign2 signs a request with Signed Signature Version 2. 92 | // If the service you're accessing supports Version 4, use that instead. 93 | func Sign2(request *http.Request, credentials ...Credentials) *http.Request { 94 | keys := chooseKeys(credentials) 95 | 96 | // Add the SecurityToken parameter when using STS 97 | // This must be added before the signature is calculated 98 | if keys.SecurityToken != "" { 99 | values := url.Values{} 100 | values.Set("SecurityToken", keys.SecurityToken) 101 | augmentRequestQuery(request, values) 102 | } 103 | 104 | prepareRequestV2(request, keys) 105 | 106 | stringToSign := stringToSignV2(request) 107 | signature := signatureV2(stringToSign, keys) 108 | 109 | values := url.Values{} 110 | values.Set("Signature", signature) 111 | 112 | augmentRequestQuery(request, values) 113 | 114 | return request 115 | } 116 | 117 | // SignS3 signs a request bound for Amazon S3 using their custom 118 | // HTTP authentication scheme. 119 | func SignS3(request *http.Request, credentials ...Credentials) *http.Request { 120 | keys := chooseKeys(credentials) 121 | 122 | // Add the X-Amz-Security-Token header when using STS 123 | if keys.SecurityToken != "" { 124 | request.Header.Set("X-Amz-Security-Token", keys.SecurityToken) 125 | } 126 | 127 | prepareRequestS3(request) 128 | 129 | stringToSign := stringToSignS3(request) 130 | signature := signatureS3(stringToSign, keys) 131 | 132 | authHeader := "AWS " + keys.AccessKeyID + ":" + signature 133 | request.Header.Set("Authorization", authHeader) 134 | 135 | return request 136 | } 137 | 138 | // SignS3Url signs a GET request for a resource on Amazon S3 by appending 139 | // query string parameters containing credentials and signature. You must 140 | // specify an expiration date for these signed requests. After that date, 141 | // a request signed with this method will be rejected by S3. 142 | func SignS3Url(request *http.Request, expire time.Time, credentials ...Credentials) *http.Request { 143 | keys := chooseKeys(credentials) 144 | 145 | stringToSign := stringToSignS3Url("GET", expire, request.URL.Path) 146 | signature := signatureS3(stringToSign, keys) 147 | 148 | query := request.URL.Query() 149 | query.Set("AWSAccessKeyId", keys.AccessKeyID) 150 | query.Set("Signature", signature) 151 | query.Set("Expires", timeToUnixEpochString(expire)) 152 | request.URL.RawQuery = query.Encode() 153 | 154 | return request 155 | } 156 | 157 | // expired checks to see if the temporary credentials from an IAM role are 158 | // within 4 minutes of expiration (The IAM documentation says that new keys 159 | // will be provisioned 5 minutes before the old keys expire). Credentials 160 | // that do not have an Expiration cannot expire. 161 | func (this *Credentials) expired() bool { 162 | if this.Expiration.IsZero() { 163 | // Credentials with no expiration can't expire 164 | return false 165 | } 166 | expireTime := this.Expiration.Add(-4 * time.Minute) 167 | // if t - 4 mins is before now, true 168 | if expireTime.Before(time.Now()) { 169 | return true 170 | } else { 171 | return false 172 | } 173 | } 174 | 175 | type metadata struct { 176 | algorithm string 177 | credentialScope string 178 | signedHeaders string 179 | date string 180 | region string 181 | service string 182 | } 183 | 184 | const ( 185 | envAccessKey = "AWS_ACCESS_KEY" 186 | envAccessKeyID = "AWS_ACCESS_KEY_ID" 187 | envSecretKey = "AWS_SECRET_KEY" 188 | envSecretAccessKey = "AWS_SECRET_ACCESS_KEY" 189 | envSecurityToken = "AWS_SECURITY_TOKEN" 190 | ) 191 | 192 | var ( 193 | awsSignVersion = map[string]int{ 194 | "autoscaling": 4, 195 | "ce": 4, 196 | "cloudfront": 4, 197 | "cloudformation": 4, 198 | "cloudsearch": 4, 199 | "monitoring": 4, 200 | "dynamodb": 4, 201 | "ec2": 4, 202 | "elasticmapreduce": 4, 203 | "elastictranscoder": 4, 204 | "elasticache": 4, 205 | "es": 4, 206 | "glacier": 4, 207 | "kinesis": 4, 208 | "redshift": 4, 209 | "rds": 4, 210 | "sdb": 2, 211 | "sns": 4, 212 | "sqs": 4, 213 | "s3": 4, 214 | "elasticbeanstalk": 4, 215 | "importexport": 4, 216 | "iam": 4, 217 | "route53": 4, 218 | "elasticloadbalancing": 4, 219 | "email": 4, 220 | } 221 | ) 222 | -------------------------------------------------------------------------------- /awsauth_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/smartystreets/assertions" 13 | "github.com/smartystreets/assertions/should" 14 | "github.com/smartystreets/gunit" 15 | ) 16 | 17 | func TestIntegrationFixture(t *testing.T) { 18 | if !credentialsSet() { 19 | t.Skip("Required credentials absent from environment.") 20 | } 21 | 22 | gunit.RunSequential(new(IntegrationFixture), t) 23 | } 24 | 25 | type IntegrationFixture struct { 26 | *gunit.Fixture 27 | } 28 | 29 | func (this *IntegrationFixture) assertOK(response *http.Response) { 30 | if !this.So(response.StatusCode, should.Equal, http.StatusOK) { 31 | message, _ := ioutil.ReadAll(response.Body) 32 | this.Error(string(message)) 33 | } 34 | } 35 | 36 | func (this *IntegrationFixture) LongTestSign4_IAM_OutOfOrderQueryString() { 37 | request := newRequest("GET", "https://iam.amazonaws.com/?Version=2010-05-08&Action=ListRoles", nil) 38 | response := sign4AndDo(request) 39 | this.assertOK(response) 40 | } 41 | 42 | func (this *IntegrationFixture) LongTestSign4_S3() { 43 | request, _ := http.NewRequest("GET", "https://s3.amazonaws.com", nil) 44 | response := sign4AndDo(request) 45 | this.assertOK(response) 46 | } 47 | 48 | func (this *IntegrationFixture) LongTestSign2_EC2() { 49 | request := newRequest("GET", "https://ec2.amazonaws.com/?Version=2013-10-15&Action=DescribeInstances", nil) 50 | response := sign2AndDo(request) 51 | this.assertOK(response) 52 | } 53 | func (this *IntegrationFixture) LongTestSign4_SQS() { 54 | request := newRequest("POST", "https://sqs.us-west-2.amazonaws.com", url.Values{"Action": []string{"ListQueues"}}) 55 | response := sign4AndDo(request) 56 | this.assertOK(response) 57 | } 58 | 59 | func (this *IntegrationFixture) LongTestSign3_SES() { 60 | request := newRequest("GET", "https://email.us-east-1.amazonaws.com/?Action=GetSendStatistics", nil) 61 | response := sign3AndDo(request) 62 | this.assertOK(response) 63 | } 64 | 65 | func (this *IntegrationFixture) LongTestSign3_Route53() { 66 | request := newRequest("GET", "https://route53.amazonaws.com/2013-04-01/hostedzone?maxitems=1", nil) 67 | response := sign3AndDo(request) 68 | this.assertOK(response) 69 | } 70 | 71 | func (this *IntegrationFixture) LongTestSign2_SimpleDB() { 72 | request := newRequest("GET", "https://sdb.amazonaws.com/?Action=ListDomains&Version=2009-04-15", nil) 73 | response := sign2AndDo(request) 74 | this.assertOK(response) 75 | } 76 | 77 | func (this *IntegrationFixture) LongTestSignS3Url() { 78 | s3res := os.Getenv("S3Resource") 79 | if s3res == "" { 80 | return 81 | } 82 | request, _ := http.NewRequest("GET", s3res, nil) 83 | response := signS3UrlAndDo(request) 84 | this.assertOK(response) 85 | } 86 | 87 | func TestSign_Version2(t *testing.T) { 88 | requests := []*http.Request{ 89 | newRequest("GET", "https://ec2.amazonaws.com", url.Values{}), 90 | newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}), 91 | } 92 | for _, request := range requests { 93 | signed := Sign(request) 94 | assertions.New(t).So(signed.URL.Query().Get("SignatureVersion"), should.Equal, "2") 95 | } 96 | } 97 | func TestSign_Version3(t *testing.T) { 98 | requests := []*http.Request{ 99 | newRequest("GET", "https://route53.amazonaws.com", url.Values{}), 100 | newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}), 101 | } 102 | for _, request := range requests { 103 | signed := Sign(request) 104 | assertions.New(t).So(signed.Header.Get("X-Amzn-Authorization"), should.NotBeBlank) 105 | } 106 | } 107 | 108 | func TestSign_Version4(t *testing.T) { 109 | requests := []*http.Request{ 110 | newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}), 111 | newRequest("GET", "https://iam.amazonaws.com", url.Values{}), 112 | newRequest("GET", "https://s3.amazonaws.com", url.Values{}), 113 | } 114 | for _, request := range requests { 115 | signed := Sign(request) 116 | assertions.New(t).So(signed.Header.Get("Authorization"), should.ContainSubstring, ", Signature=") 117 | } 118 | } 119 | 120 | func TestSign_ExistingCredentials_Version2(t *testing.T) { 121 | requests := []*http.Request{ 122 | newRequest("GET", "https://ec2.amazonaws.com", url.Values{}), 123 | newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}), 124 | } 125 | for _, request := range requests { 126 | signed := Sign(request, newKeys()) 127 | assertions.New(t).So(signed.URL.Query().Get("SignatureVersion"), should.Equal, "2") 128 | } 129 | } 130 | 131 | func TestSign_ExistingCredentials_Version3(t *testing.T) { 132 | requests := []*http.Request{ 133 | newRequest("GET", "https://route53.amazonaws.com", url.Values{}), 134 | newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}), 135 | } 136 | for _, request := range requests { 137 | signed := Sign(request, newKeys()) 138 | assertions.New(t).So(signed.Header.Get("X-Amzn-Authorization"), should.NotBeBlank) 139 | } 140 | } 141 | 142 | func TestSign_ExistingCredentials_Version4(t *testing.T) { 143 | requests := []*http.Request{ 144 | newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}), 145 | newRequest("GET", "https://iam.amazonaws.com", url.Values{}), 146 | newRequest("GET", "https://s3.amazonaws.com", url.Values{}), 147 | } 148 | for _, request := range requests { 149 | signed := Sign(request, newKeys()) 150 | assertions.New(t).So(signed.Header.Get("Authorization"), should.ContainSubstring, ", Signature=") 151 | } 152 | } 153 | 154 | func TestExpiration(t *testing.T) { 155 | assert := assertions.New(t) 156 | var credentials = &Credentials{} 157 | 158 | // Credentials without an expiration can't expire 159 | assert.So(credentials.expired(), should.BeFalse) 160 | 161 | // Credentials that expire in 5 minutes aren't expired 162 | credentials.Expiration = time.Now().Add(5 * time.Minute) 163 | assert.So(credentials.expired(), should.BeFalse) 164 | 165 | // Credentials that expire in 1 minute are expired 166 | credentials.Expiration = time.Now().Add(1 * time.Minute) 167 | assert.So(credentials.expired(), should.BeTrue) 168 | 169 | // Credentials that expired 2 hours ago are expired 170 | credentials.Expiration = time.Now().Add(-2 * time.Hour) 171 | assert.So(credentials.expired(), should.BeTrue) 172 | } 173 | 174 | func credentialsSet() bool { 175 | var keys Credentials 176 | keys = newKeys() 177 | return keys.AccessKeyID != "" 178 | } 179 | 180 | func newRequest(method string, url string, v url.Values) *http.Request { 181 | request, _ := http.NewRequest(method, url, strings.NewReader(v.Encode())) 182 | return request 183 | } 184 | 185 | func sign2AndDo(request *http.Request) *http.Response { 186 | Sign2(request) 187 | response, _ := client.Do(request) 188 | return response 189 | } 190 | 191 | func sign3AndDo(request *http.Request) *http.Response { 192 | Sign3(request) 193 | response, _ := client.Do(request) 194 | return response 195 | } 196 | 197 | func sign4AndDo(request *http.Request) *http.Response { 198 | Sign4(request) 199 | response, _ := client.Do(request) 200 | return response 201 | } 202 | 203 | func signS3AndDo(request *http.Request) *http.Response { 204 | SignS3(request) 205 | response, _ := client.Do(request) 206 | return response 207 | } 208 | 209 | func signS3UrlAndDo(request *http.Request) *http.Response { 210 | SignS3Url(request, time.Now().AddDate(0, 0, 1)) 211 | response, _ := client.Do(request) 212 | return response 213 | } 214 | 215 | var client = &http.Client{} 216 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/hmac" 7 | "crypto/md5" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "encoding/json" 12 | "fmt" 13 | "io/ioutil" 14 | "net" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | type location struct { 23 | ec2 bool 24 | checked bool 25 | } 26 | 27 | var loc *location 28 | 29 | // serviceAndRegion parsers a hostname to find out which ones it is. 30 | // http://docs.aws.amazon.com/general/latest/gr/rande.html 31 | func serviceAndRegion(host string) (service string, region string) { 32 | // These are the defaults if the hostname doesn't suggest something else 33 | region = "us-east-1" 34 | service = "s3" 35 | 36 | parts := strings.Split(host, ".") 37 | if len(parts) == 4 { 38 | // Either service.region.amazonaws.com or virtual-host.region.amazonaws.com 39 | if parts[1] == "s3" { 40 | service = "s3" 41 | } else if strings.HasPrefix(parts[1], "s3-") { 42 | region = parts[1][3:] 43 | service = "s3" 44 | } else { 45 | service = parts[0] 46 | region = parts[1] 47 | } 48 | } else if len(parts) == 5 { 49 | service = parts[2] 50 | region = parts[1] 51 | } else { 52 | // Either service.amazonaws.com or s3-region.amazonaws.com 53 | if strings.HasPrefix(parts[0], "s3-") { 54 | region = parts[0][3:] 55 | } else { 56 | service = parts[0] 57 | } 58 | } 59 | 60 | if region == "external-1" { 61 | region = "us-east-1" 62 | } 63 | 64 | return 65 | } 66 | 67 | // newKeys produces a set of credentials based on the environment 68 | func newKeys() (newCredentials Credentials) { 69 | // First use credentials from environment variables 70 | newCredentials.AccessKeyID = os.Getenv(envAccessKeyID) 71 | if newCredentials.AccessKeyID == "" { 72 | newCredentials.AccessKeyID = os.Getenv(envAccessKey) 73 | } 74 | 75 | newCredentials.SecretAccessKey = os.Getenv(envSecretAccessKey) 76 | if newCredentials.SecretAccessKey == "" { 77 | newCredentials.SecretAccessKey = os.Getenv(envSecretKey) 78 | } 79 | 80 | newCredentials.SecurityToken = os.Getenv(envSecurityToken) 81 | 82 | // If there is no Access Key and you are on EC2, get the key from the role 83 | if (newCredentials.AccessKeyID == "" || newCredentials.SecretAccessKey == "") && onEC2() { 84 | newCredentials = *getIAMRoleCredentials() 85 | } 86 | 87 | // If the key is expiring, get a new key 88 | if newCredentials.expired() && onEC2() { 89 | newCredentials = *getIAMRoleCredentials() 90 | } 91 | 92 | return newCredentials 93 | } 94 | 95 | // checkKeys gets credentials depending on if any were passed in as an argument 96 | // or it makes new ones based on the environment. 97 | func chooseKeys(cred []Credentials) Credentials { 98 | if len(cred) == 0 { 99 | return newKeys() 100 | } else { 101 | return cred[0] 102 | } 103 | } 104 | 105 | // onEC2 checks to see if the program is running on an EC2 instance. 106 | // It does this by looking for the EC2 metadata service. 107 | // This caches that information in a struct so that it doesn't waste time. 108 | func onEC2() bool { 109 | if loc == nil { 110 | loc = &location{} 111 | } 112 | if !(loc.checked) { 113 | c, err := net.DialTimeout("tcp", "169.254.169.254:80", time.Millisecond*100) 114 | 115 | if err != nil { 116 | loc.ec2 = false 117 | } else { 118 | c.Close() 119 | loc.ec2 = true 120 | } 121 | loc.checked = true 122 | } 123 | 124 | return loc.ec2 125 | } 126 | 127 | // getIAMRoleList gets a list of the roles that are available to this instance 128 | func getIAMRoleList() []string { 129 | 130 | var roles []string 131 | url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 132 | 133 | client := &http.Client{} 134 | 135 | request, err := http.NewRequest("GET", url, nil) 136 | 137 | if err != nil { 138 | return roles 139 | } 140 | 141 | response, err := client.Do(request) 142 | 143 | if err != nil { 144 | return roles 145 | } 146 | defer response.Body.Close() 147 | 148 | scanner := bufio.NewScanner(response.Body) 149 | for scanner.Scan() { 150 | roles = append(roles, scanner.Text()) 151 | } 152 | return roles 153 | } 154 | 155 | func getIAMRoleCredentials() *Credentials { 156 | 157 | roles := getIAMRoleList() 158 | 159 | if len(roles) < 1 { 160 | return &Credentials{} 161 | } 162 | 163 | // Use the first role in the list 164 | role := roles[0] 165 | 166 | url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 167 | 168 | // Create the full URL of the role 169 | var buffer bytes.Buffer 170 | buffer.WriteString(url) 171 | buffer.WriteString(role) 172 | roleURL := buffer.String() 173 | 174 | // Get the role 175 | roleRequest, err := http.NewRequest("GET", roleURL, nil) 176 | 177 | if err != nil { 178 | return &Credentials{} 179 | } 180 | 181 | client := &http.Client{} 182 | roleResponse, err := client.Do(roleRequest) 183 | 184 | if err != nil { 185 | return &Credentials{} 186 | } 187 | defer roleResponse.Body.Close() 188 | 189 | roleBuffer := new(bytes.Buffer) 190 | roleBuffer.ReadFrom(roleResponse.Body) 191 | 192 | credentials := Credentials{} 193 | 194 | err = json.Unmarshal(roleBuffer.Bytes(), &credentials) 195 | 196 | if err != nil { 197 | return &Credentials{} 198 | } 199 | 200 | return &credentials 201 | 202 | } 203 | 204 | func augmentRequestQuery(request *http.Request, values url.Values) *http.Request { 205 | for key, array := range request.URL.Query() { 206 | for _, value := range array { 207 | values.Set(key, value) 208 | } 209 | } 210 | 211 | request.URL.RawQuery = values.Encode() 212 | 213 | return request 214 | } 215 | 216 | func hmacSHA256(key []byte, content string) []byte { 217 | mac := hmac.New(sha256.New, key) 218 | mac.Write([]byte(content)) 219 | return mac.Sum(nil) 220 | } 221 | 222 | func hmacSHA1(key []byte, content string) []byte { 223 | mac := hmac.New(sha1.New, key) 224 | mac.Write([]byte(content)) 225 | return mac.Sum(nil) 226 | } 227 | 228 | func hashSHA256(content []byte) string { 229 | h := sha256.New() 230 | h.Write(content) 231 | return fmt.Sprintf("%x", h.Sum(nil)) 232 | } 233 | 234 | func hashMD5(content []byte) string { 235 | h := md5.New() 236 | h.Write(content) 237 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 238 | } 239 | 240 | func readAndReplaceBody(request *http.Request) []byte { 241 | if request.Body == nil { 242 | return []byte{} 243 | } 244 | payload, _ := ioutil.ReadAll(request.Body) 245 | request.Body = ioutil.NopCloser(bytes.NewReader(payload)) 246 | return payload 247 | } 248 | 249 | func concat(delim string, str ...string) string { 250 | return strings.Join(str, delim) 251 | } 252 | 253 | var now = func() time.Time { 254 | return time.Now().UTC() 255 | } 256 | 257 | func normuri(uri string) string { 258 | parts := strings.Split(uri, "/") 259 | for i := range parts { 260 | parts[i] = encodePathFrag(parts[i]) 261 | } 262 | return strings.Join(parts, "/") 263 | } 264 | 265 | func encodePathFrag(s string) string { 266 | hexCount := 0 267 | for i := 0; i < len(s); i++ { 268 | c := s[i] 269 | if shouldEscape(c) { 270 | hexCount++ 271 | } 272 | } 273 | t := make([]byte, len(s)+2*hexCount) 274 | j := 0 275 | for i := 0; i < len(s); i++ { 276 | c := s[i] 277 | if shouldEscape(c) { 278 | t[j] = '%' 279 | t[j+1] = "0123456789ABCDEF"[c>>4] 280 | t[j+2] = "0123456789ABCDEF"[c&15] 281 | j += 3 282 | } else { 283 | t[j] = c 284 | j++ 285 | } 286 | } 287 | return string(t) 288 | } 289 | 290 | func shouldEscape(c byte) bool { 291 | if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { 292 | return false 293 | } 294 | if '0' <= c && c <= '9' { 295 | return false 296 | } 297 | if c == '-' || c == '_' || c == '.' || c == '~' { 298 | return false 299 | } 300 | return true 301 | } 302 | 303 | func normquery(v url.Values) string { 304 | queryString := v.Encode() 305 | 306 | // Go encodes a space as '+' but Amazon requires '%20'. Luckily any '+' in the 307 | // original query string has been percent escaped so all '+' chars that are left 308 | // were originally spaces. 309 | 310 | return strings.Replace(queryString, "+", "%20", -1) 311 | } 312 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/smartystreets/assertions/should" 8 | "github.com/smartystreets/gunit" 9 | ) 10 | 11 | func TestCommonFixture(t *testing.T) { 12 | gunit.Run(new(CommonFixture), t) 13 | } 14 | 15 | type CommonFixture struct { 16 | *gunit.Fixture 17 | } 18 | 19 | func (this *CommonFixture) serviceAndRegion(id string) []string { 20 | service, region := serviceAndRegion(id) 21 | return []string{service, region} 22 | } 23 | func (this *CommonFixture) TestServiceAndRegion() { 24 | this.So(this.serviceAndRegion("sqs.us-west-2.amazonaws.com"), should.Resemble, []string{"sqs", "us-west-2"}) 25 | this.So(this.serviceAndRegion("iam.amazonaws.com"), should.Resemble, []string{"iam", "us-east-1"}) 26 | this.So(this.serviceAndRegion("sns.us-west-2.amazonaws.com"), should.Resemble, []string{"sns", "us-west-2"}) 27 | this.So(this.serviceAndRegion("bucketname.s3.amazonaws.com"), should.Resemble, []string{"s3", "us-east-1"}) 28 | this.So(this.serviceAndRegion("s3.amazonaws.com"), should.Resemble, []string{"s3", "us-east-1"}) 29 | this.So(this.serviceAndRegion("s3-us-west-1.amazonaws.com"), should.Resemble, []string{"s3", "us-west-1"}) 30 | this.So(this.serviceAndRegion("s3-external-1.amazonaws.com"), should.Resemble, []string{"s3", "us-east-1"}) 31 | } 32 | 33 | func (this *CommonFixture) TestHashFunctions() { 34 | this.So(hashMD5([]byte("Pretend this is a REALLY long byte array...")), should.Equal, "KbVTY8Vl6VccnzQf1AGOFw==") 35 | this.So(hashSHA256([]byte("This is... Sparta!!")), should.Equal, 36 | "5c81a4ef1172e89b1a9d575f4cd82f4ed20ea9137e61aa7f1ab936291d24e79a") 37 | 38 | key := []byte("asdf1234") 39 | contents := "SmartyStreets was here" 40 | 41 | expectedHMAC_SHA256 := []byte{ 42 | 65, 46, 186, 78, 2, 155, 71, 104, 49, 37, 5, 66, 195, 129, 159, 227, 43 | 239, 53, 240, 107, 83, 21, 235, 198, 238, 216, 108, 149, 143, 222, 144, 94} 44 | this.So(hmacSHA256(key, contents), should.Resemble, expectedHMAC_SHA256) 45 | 46 | expectedHMAC_SHA1 := []byte{ 47 | 164, 77, 252, 0, 87, 109, 207, 110, 163, 75, 228, 122, 83, 255, 233, 237, 125, 206, 85, 70} 48 | this.So(hmacSHA1(key, contents), should.Resemble, expectedHMAC_SHA1) 49 | } 50 | 51 | func (this *CommonFixture) TestConcat() { 52 | this.So(concat("\n", "Test1", "Test2"), should.Equal, "Test1\nTest2") 53 | this.So(concat(".", "Test1"), should.Equal, "Test1") 54 | this.So(concat("\t", "1", "2", "3", "4"), should.Equal, "1\t2\t3\t4") 55 | } 56 | 57 | func (this *CommonFixture) TestURINormalization() { 58 | this.So( 59 | normuri("/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"), should.Equal, 60 | "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") 61 | 62 | this.So(normuri("/ /foo"), should.Equal, "/%20/foo") 63 | this.So(normuri("/(foo)"), should.Equal, "/%28foo%29") 64 | 65 | this.So( 66 | normquery(url.Values{"p": []string{" +&;-=._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"}}), 67 | should.Equal, 68 | "p=%20%2B%26%3B-%3D._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") 69 | } 70 | -------------------------------------------------------------------------------- /s3.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func signatureS3(stringToSign string, keys Credentials) string { 13 | hashed := hmacSHA1([]byte(keys.SecretAccessKey), stringToSign) 14 | return base64.StdEncoding.EncodeToString(hashed) 15 | } 16 | 17 | func stringToSignS3(request *http.Request) string { 18 | str := request.Method + "\n" 19 | 20 | if request.Header.Get("Content-Md5") != "" { 21 | str += request.Header.Get("Content-Md5") 22 | } else { 23 | body := readAndReplaceBody(request) 24 | if len(body) > 0 { 25 | str += hashMD5(body) 26 | } 27 | } 28 | str += "\n" 29 | 30 | str += request.Header.Get("Content-Type") + "\n" 31 | 32 | if request.Header.Get("Date") != "" { 33 | str += request.Header.Get("Date") 34 | } else { 35 | str += timestampS3() 36 | } 37 | 38 | str += "\n" 39 | 40 | canonicalHeaders := canonicalAmzHeadersS3(request) 41 | if canonicalHeaders != "" { 42 | str += canonicalHeaders 43 | } 44 | 45 | str += canonicalResourceS3(request) 46 | 47 | return str 48 | } 49 | 50 | func stringToSignS3Url(method string, expire time.Time, path string) string { 51 | return method + "\n\n\n" + timeToUnixEpochString(expire) + "\n" + path 52 | } 53 | 54 | func timeToUnixEpochString(t time.Time) string { 55 | return strconv.FormatInt(t.Unix(), 10) 56 | } 57 | 58 | func canonicalAmzHeadersS3(request *http.Request) string { 59 | var headers []string 60 | 61 | for header := range request.Header { 62 | standardized := strings.ToLower(strings.TrimSpace(header)) 63 | if strings.HasPrefix(standardized, "x-amz") { 64 | headers = append(headers, standardized) 65 | } 66 | } 67 | 68 | sort.Strings(headers) 69 | 70 | for i, header := range headers { 71 | headers[i] = header + ":" + strings.Replace(request.Header.Get(header), "\n", " ", -1) 72 | } 73 | 74 | if len(headers) > 0 { 75 | return strings.Join(headers, "\n") + "\n" 76 | } else { 77 | return "" 78 | } 79 | } 80 | 81 | func canonicalResourceS3(request *http.Request) string { 82 | res := "" 83 | 84 | if isS3VirtualHostedStyle(request) { 85 | bucketname := strings.Split(request.Host, ".")[0] 86 | res += "/" + bucketname 87 | } 88 | 89 | res += request.URL.Path 90 | 91 | for _, subres := range strings.Split(subresourcesS3, ",") { 92 | if strings.HasPrefix(request.URL.RawQuery, subres) { 93 | res += "?" + subres 94 | } 95 | } 96 | 97 | return res 98 | } 99 | 100 | func prepareRequestS3(request *http.Request) *http.Request { 101 | request.Header.Set("Date", timestampS3()) 102 | if request.URL.Path == "" { 103 | request.URL.Path += "/" 104 | } 105 | return request 106 | } 107 | 108 | // Info: http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html 109 | func isS3VirtualHostedStyle(request *http.Request) bool { 110 | service, _ := serviceAndRegion(request.Host) 111 | return service == "s3" && strings.Count(request.Host, ".") == 3 112 | } 113 | 114 | func timestampS3() string { 115 | return now().Format(timeFormatS3) 116 | } 117 | 118 | const ( 119 | timeFormatS3 = time.RFC1123Z 120 | subresourcesS3 = "acl,lifecycle,location,logging,notification,partNumber,policy,requestPayment,torrent,uploadId,uploads,versionId,versioning,versions,website" 121 | ) 122 | -------------------------------------------------------------------------------- /s3_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | "time" 9 | 10 | "net/http/httptest" 11 | 12 | "github.com/smartystreets/assertions" 13 | "github.com/smartystreets/assertions/should" 14 | "github.com/smartystreets/gunit" 15 | ) 16 | 17 | // http://docs.aws.amazon.com/AmazonS3/2006-03-01/dev/RESTAuthentication.html 18 | // Note: S3 now supports signed signature version 4 19 | // (but signed URL requests still utilize a lot of the same functionality) 20 | 21 | func TestSignatureS3Fixture(t *testing.T) { 22 | gunit.RunSequential(new(SignatureS3Fixture), t) 23 | } 24 | 25 | type SignatureS3Fixture struct { 26 | *gunit.Fixture 27 | 28 | keys Credentials 29 | request *http.Request 30 | } 31 | 32 | func (this *SignatureS3Fixture) Setup() { 33 | this.keys = *testCredS3 34 | this.request = test_plainRequestS3() 35 | 36 | now = func() time.Time { 37 | parsed, _ := time.Parse(timeFormatS3, exampleReqTsS3) 38 | return parsed 39 | } 40 | } 41 | 42 | func (this *SignatureS3Fixture) TestRequestShouldHaveADateHeader() { 43 | prepareRequestS3(this.request) 44 | this.So(this.request.Header.Get("Date"), should.Equal, exampleReqTsS3) 45 | } 46 | 47 | func (this *SignatureS3Fixture) TestRequestShouldHaveCanonicalizedAmzHeaders() { 48 | req2 := test_headerRequestS3() 49 | actual := canonicalAmzHeadersS3(req2) 50 | this.So(actual, should.Equal, expectedCanonAmzHeadersS3) 51 | } 52 | 53 | func (this *SignatureS3Fixture) TestCanonicalizedResourceBuiltProperly() { 54 | actual := canonicalResourceS3(this.request) 55 | this.So(actual, should.Equal, expectedCanonResourceS3) 56 | } 57 | 58 | func (this *SignatureS3Fixture) TestStringToSignShouldBeCorrect() { 59 | actual := stringToSignS3(this.request) 60 | this.So(actual, should.Equal, expectedStringToSignS3) 61 | } 62 | 63 | func (this *SignatureS3Fixture) TestFinalSignatureShouldBeExactlyCorrect() { 64 | actual := signatureS3(stringToSignS3(this.request), this.keys) 65 | this.So(actual, should.Equal, "bWq2s1WEIj+Ydj0vQ697zp+IXMU=") 66 | } 67 | 68 | func (this *SignatureS3Fixture) TestQueryStringAuthentication() { 69 | this.request = httptest.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg", nil) 70 | 71 | // The string to sign should be correct 72 | actual := stringToSignS3Url("GET", now(), this.request.URL.Path) 73 | this.So(actual, should.Equal, expectedStringToSignS3Url) 74 | 75 | // The signature of string to sign should be correct 76 | actualSignature := signatureS3(expectedStringToSignS3Url, this.keys) 77 | this.So(actualSignature, should.Equal, "R2K/+9bbnBIbVDCs7dqlz3XFtBQ=") 78 | 79 | // The finished signed URL should be correct 80 | expiry := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) 81 | this.So(SignS3Url(this.request, expiry, this.keys).URL.String(), should.Equal, expectedSignedS3Url) 82 | } 83 | 84 | func TestS3STSRequestPreparer(t *testing.T) { 85 | // Given a plain request with no custom headers 86 | request := test_plainRequestS3() 87 | 88 | // And a set of credentials with an STS token 89 | keys := *testCredS3WithSTS 90 | 91 | // It should include an X-Amz-Security-Token when the request is signed 92 | actualSigned := SignS3(request, keys) 93 | actual := actualSigned.Header.Get("X-Amz-Security-Token") 94 | 95 | assert := assertions.New(t) 96 | assert.So(actual, should.NotBeBlank) 97 | assert.So(actual, should.Equal, testCredS3WithSTS.SecurityToken) 98 | } 99 | 100 | func test_plainRequestS3() *http.Request { 101 | return httptest.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/photos/puppy.jpg", nil) 102 | } 103 | 104 | func test_headerRequestS3() *http.Request { 105 | request := test_plainRequestS3() 106 | request.Header.Set("X-Amz-Meta-Something", "more foobar") 107 | request.Header.Set("X-Amz-Date", "foobar") 108 | request.Header.Set("X-Foobar", "nanoo-nanoo") 109 | return request 110 | } 111 | 112 | func TestCanonical(t *testing.T) { 113 | expectedCanonicalString := "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson" 114 | 115 | origUrl := "https://s3.amazonaws.com/" 116 | resource := "/quotes/nelson" 117 | 118 | u, _ := url.ParseRequestURI(origUrl) 119 | u.Path = resource 120 | urlStr := fmt.Sprintf("%v", u) 121 | 122 | request, _ := http.NewRequest("PUT", urlStr, nil) 123 | request.Header.Add("Content-Md5", "c8fdb181845a4ca6b8fec737b3581d76") 124 | request.Header.Add("Content-Type", "text/html") 125 | request.Header.Add("Date", "Thu, 17 Nov 2005 18:49:58 GMT") 126 | request.Header.Add("X-Amz-Meta-Author", "foo@bar.com") 127 | request.Header.Add("X-Amz-Magic", "abracadabra") 128 | 129 | if stringToSignS3(request) != expectedCanonicalString { 130 | t.Errorf("----Got\n***%s***\n----Expected\n***%s***", stringToSignS3(request), expectedCanonicalString) 131 | } 132 | } 133 | 134 | var ( 135 | testCredS3 = &Credentials{ 136 | AccessKeyID: "AKIAIOSFODNN7EXAMPLE", 137 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 138 | } 139 | 140 | testCredS3WithSTS = &Credentials{ 141 | AccessKeyID: "AKIDEXAMPLE", 142 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", 143 | SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=", 144 | } 145 | 146 | expectedCanonAmzHeadersS3 = "x-amz-date:foobar\nx-amz-meta-something:more foobar\n" 147 | expectedCanonResourceS3 = "/johnsmith/photos/puppy.jpg" 148 | expectedStringToSignS3 = "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg" 149 | expectedStringToSignS3Url = "GET\n\n\n1175024202\n/johnsmith/photos/puppy.jpg" 150 | expectedSignedS3Url = "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Expires=1257894000&Signature=X%2FarTLAJP08uP1Bsap52rwmsVok%3D" 151 | exampleReqTsS3 = "Tue, 27 Mar 2007 19:36:42 +0000" 152 | ) 153 | -------------------------------------------------------------------------------- /sign2.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func prepareRequestV2(request *http.Request, keys Credentials) *http.Request { 11 | 12 | keyID := keys.AccessKeyID 13 | 14 | values := url.Values{} 15 | values.Set("AWSAccessKeyId", keyID) 16 | values.Set("SignatureVersion", "2") 17 | values.Set("SignatureMethod", "HmacSHA256") 18 | values.Set("Timestamp", timestampV2()) 19 | 20 | augmentRequestQuery(request, values) 21 | 22 | if request.URL.Path == "" { 23 | request.URL.Path += "/" 24 | } 25 | 26 | return request 27 | } 28 | 29 | func stringToSignV2(request *http.Request) string { 30 | str := request.Method + "\n" 31 | str += strings.ToLower(request.URL.Host) + "\n" 32 | str += request.URL.Path + "\n" 33 | str += canonicalQueryStringV2(request) 34 | return str 35 | } 36 | 37 | func signatureV2(strToSign string, keys Credentials) string { 38 | hashed := hmacSHA256([]byte(keys.SecretAccessKey), strToSign) 39 | return base64.StdEncoding.EncodeToString(hashed) 40 | } 41 | 42 | func canonicalQueryStringV2(request *http.Request) string { 43 | return request.URL.RawQuery 44 | } 45 | 46 | func timestampV2() string { 47 | return now().Format(timeFormatV2) 48 | } 49 | 50 | const timeFormatV2 = "2006-01-02T15:04:05" 51 | -------------------------------------------------------------------------------- /sign2_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | "time" 8 | 9 | "github.com/smartystreets/assertions" 10 | "github.com/smartystreets/assertions/should" 11 | "github.com/smartystreets/gunit" 12 | ) 13 | 14 | // http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html 15 | 16 | func TestSignature2Fixture(t *testing.T) { 17 | gunit.RunSequential(new(Signature2Fixture), t) 18 | } 19 | 20 | type Signature2Fixture struct { 21 | *gunit.Fixture 22 | 23 | keys Credentials 24 | } 25 | 26 | func (this *Signature2Fixture) Setup() { 27 | this.keys = *testCredV2 28 | 29 | // Mock time 30 | now = func() time.Time { 31 | parsed, _ := time.Parse(timeFormatV2, exampleReqTsV2) 32 | return parsed 33 | } 34 | } 35 | 36 | func (this *Signature2Fixture) TestSignUnpreparedPlanRequest() { 37 | request := test_plainRequestV2() 38 | prepareRequestV2(request, this.keys) 39 | this.So(request, should.Resemble, test_unsignedRequestV2()) 40 | } 41 | 42 | func (this *Signature2Fixture) TestSignPreparedUnsignedRequest() { 43 | request := test_unsignedRequestV2() 44 | actual := canonicalQueryStringV2(request) 45 | expected := canonicalQsV2 46 | this.So(actual, should.Equal, expected) 47 | this.So(request.URL.Path, should.Equal, "/") 48 | 49 | this.So(stringToSignV2(request), should.Equal, expectedStringToSignV2) 50 | this.So(signatureV2(stringToSignV2(request), this.keys), should.Equal, "i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf/Mj6vPxyYIs=") 51 | 52 | Sign2(request, this.keys) 53 | this.So(request.URL.String(), should.Equal, expectedFinalUrlV2) 54 | } 55 | 56 | func TestVersion2STSRequestPreparer(t *testing.T) { 57 | // Given a plain request 58 | request := test_plainRequestV2() 59 | 60 | // And a set of credentials with an STS token 61 | var keys Credentials 62 | keys = *testCredV2WithSTS 63 | 64 | // It should include the SecurityToken parameter when the request is signed 65 | actualSigned := Sign2(request, keys) 66 | actual := actualSigned.URL.Query()["SecurityToken"][0] 67 | 68 | assert := assertions.New(t) 69 | assert.So(actual, should.NotBeBlank) 70 | assert.So(actual, should.Equal, testCredV2WithSTS.SecurityToken) 71 | } 72 | 73 | func test_plainRequestV2() *http.Request { 74 | values := url.Values{} 75 | values.Set("Action", "DescribeJobFlows") 76 | values.Set("Version", "2009-03-31") 77 | 78 | address := baseUrlV2 + "?" + values.Encode() 79 | 80 | request, err := http.NewRequest("GET", address, nil) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | return request 86 | } 87 | 88 | func test_unsignedRequestV2() *http.Request { 89 | request := test_plainRequestV2() 90 | newUrl, _ := url.Parse(baseUrlV2 + "/?" + canonicalQsV2) 91 | request.URL = newUrl 92 | return request 93 | } 94 | 95 | var ( 96 | testCredV2 = &Credentials{ 97 | AccessKeyID: "AKIAIOSFODNN7EXAMPLE", 98 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 99 | } 100 | 101 | testCredV2WithSTS = &Credentials{ 102 | AccessKeyID: "AKIDEXAMPLE", 103 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", 104 | SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=", 105 | } 106 | 107 | exampleReqTsV2 = "2011-10-03T15:19:30" 108 | baseUrlV2 = "https://elasticmapreduce.amazonaws.com" 109 | canonicalQsV2 = "AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31" 110 | expectedStringToSignV2 = "GET\nelasticmapreduce.amazonaws.com\n/\n" + canonicalQsV2 111 | expectedFinalUrlV2 = baseUrlV2 + "/?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&Signature=i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf%2FMj6vPxyYIs%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31" 112 | ) 113 | -------------------------------------------------------------------------------- /sign3.go: -------------------------------------------------------------------------------- 1 | // Thanks to Michael Vierling for contributing sign3.go 2 | 3 | package awsauth 4 | 5 | import ( 6 | "encoding/base64" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func stringToSignV3(request *http.Request) string { 12 | // TASK 1. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#StringToSign 13 | 14 | return request.Header.Get("Date") + request.Header.Get("x-amz-nonce") 15 | } 16 | 17 | func signatureV3(stringToSign string, keys Credentials) string { 18 | // TASK 2. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#Signature 19 | 20 | hash := hmacSHA256([]byte(keys.SecretAccessKey), stringToSign) 21 | return base64.StdEncoding.EncodeToString(hash) 22 | } 23 | 24 | func buildAuthHeaderV3(signature string, keys Credentials) string { 25 | // TASK 3. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#AuthorizationHeader 26 | 27 | return "AWS3-HTTPS AWSAccessKeyId=" + keys.AccessKeyID + 28 | ", Algorithm=HmacSHA256" + 29 | ", Signature=" + signature 30 | } 31 | 32 | func prepareRequestV3(request *http.Request) *http.Request { 33 | ts := timestampV3() 34 | necessaryDefaults := map[string]string{ 35 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 36 | "x-amz-date": ts, 37 | "Date": ts, 38 | "x-amz-nonce": "", 39 | } 40 | 41 | for header, value := range necessaryDefaults { 42 | if request.Header.Get(header) == "" { 43 | request.Header.Set(header, value) 44 | } 45 | } 46 | 47 | if request.URL.Path == "" { 48 | request.URL.Path += "/" 49 | } 50 | 51 | return request 52 | } 53 | 54 | func timestampV3() string { 55 | return now().Format(timeFormatV3) 56 | } 57 | 58 | const timeFormatV3 = time.RFC1123 59 | -------------------------------------------------------------------------------- /sign3_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | "time" 8 | 9 | "github.com/smartystreets/assertions" 10 | "github.com/smartystreets/assertions/should" 11 | ) 12 | 13 | func TestSignature3(t *testing.T) { 14 | // http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html 15 | // http://docs.aws.amazon.com/ses/latest/DeveloperGuide/query-interface-authentication.html 16 | 17 | assert := assertions.New(t) 18 | 19 | // Given bogus credentials 20 | keys := *testCredV3 21 | 22 | // Mock time 23 | now = func() time.Time { 24 | parsed, _ := time.Parse(timeFormatV3, exampleReqTsV3) 25 | return parsed 26 | } 27 | 28 | // Given a plain request that is unprepared 29 | request := test_plainRequestV3() 30 | 31 | // The request should be prepared to be signed 32 | expectedUnsigned := test_unsignedRequestV3() 33 | prepareRequestV3(request) 34 | assert.So(request, should.Resemble, expectedUnsigned) 35 | 36 | // Given a prepared, but unsigned, request 37 | request = test_unsignedRequestV3() 38 | 39 | // The absolute path should be extracted correctly 40 | assert.So(request.URL.Path, should.Equal, "/") 41 | 42 | // The string to sign should be well-formed 43 | assert.So(stringToSignV3(request), should.Equal, expectedStringToSignV3) 44 | 45 | // The resulting signature should be correct 46 | assert.So(signatureV3(stringToSignV3(request), keys), should.Equal, "PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk=") 47 | 48 | // The final signed request should be correctly formed 49 | Sign3(request, keys) 50 | assert.So(request.Header.Get("X-Amzn-Authorization"), should.Resemble, expectedAuthHeaderV3) 51 | } 52 | 53 | func test_plainRequestV3() *http.Request { 54 | values := url.Values{} 55 | values.Set("Action", "GetSendStatistics") 56 | values.Set("Version", "2010-12-01") 57 | 58 | address := baseUrlV3 + "/?" + values.Encode() 59 | 60 | request, err := http.NewRequest("GET", address, nil) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | return request 66 | } 67 | 68 | func test_unsignedRequestV3() *http.Request { 69 | request := test_plainRequestV3() 70 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 71 | request.Header.Set("x-amz-date", exampleReqTsV3) 72 | request.Header.Set("Date", exampleReqTsV3) 73 | request.Header.Set("x-amz-nonce", "") 74 | return request 75 | } 76 | 77 | func TestVersion3STSRequestPreparer(t *testing.T) { 78 | // Given a plain request with no custom headers 79 | request := test_plainRequestV3() 80 | 81 | // And a set of credentials with an STS token 82 | var keys Credentials 83 | keys = *testCredV3WithSTS 84 | 85 | // It should include an X-Amz-Security-Token when the request is signed 86 | actualSigned := Sign3(request, keys) 87 | actual := actualSigned.Header.Get("X-Amz-Security-Token") 88 | 89 | assert := assertions.New(t) 90 | assert.So(actual, should.NotBeBlank) 91 | assert.So(actual, should.Equal, testCredV4WithSTS.SecurityToken) 92 | 93 | } 94 | 95 | var ( 96 | testCredV3 = &Credentials{ 97 | AccessKeyID: "AKIAIOSFODNN7EXAMPLE", 98 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 99 | } 100 | 101 | testCredV3WithSTS = &Credentials{ 102 | AccessKeyID: "AKIDEXAMPLE", 103 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", 104 | SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=", 105 | } 106 | 107 | exampleReqTsV3 = "Thu, 14 Aug 2008 17:08:48 GMT" 108 | baseUrlV3 = "https://email.us-east-1.amazonaws.com" 109 | expectedStringToSignV3 = exampleReqTsV3 110 | expectedAuthHeaderV3 = "AWS3-HTTPS AWSAccessKeyId=" + testCredV3.AccessKeyID + ", Algorithm=HmacSHA256, Signature=PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk=" 111 | ) 112 | -------------------------------------------------------------------------------- /sign4.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "encoding/hex" 5 | "net/http" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | func hashedCanonicalRequestV4(request *http.Request, meta *metadata) string { 11 | // TASK 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 12 | 13 | payload := readAndReplaceBody(request) 14 | payloadHash := hashSHA256(payload) 15 | request.Header.Set("X-Amz-Content-Sha256", payloadHash) 16 | 17 | // Set this in header values to make it appear in the range of headers to sign 18 | request.Header.Set("Host", request.Host) 19 | 20 | var sortedHeaderKeys []string 21 | for key, _ := range request.Header { 22 | switch key { 23 | case "Content-Type", "Content-Md5", "Host": 24 | default: 25 | if !strings.HasPrefix(key, "X-Amz-") { 26 | continue 27 | } 28 | } 29 | sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(key)) 30 | } 31 | sort.Strings(sortedHeaderKeys) 32 | 33 | var headersToSign string 34 | for _, key := range sortedHeaderKeys { 35 | value := strings.TrimSpace(request.Header.Get(key)) 36 | if key == "host" { 37 | //AWS does not include port in signing request. 38 | if strings.Contains(value, ":") { 39 | split := strings.Split(value, ":") 40 | port := split[1] 41 | if port == "80" || port == "443" { 42 | value = split[0] 43 | } 44 | } 45 | } 46 | headersToSign += key + ":" + value + "\n" 47 | } 48 | meta.signedHeaders = concat(";", sortedHeaderKeys...) 49 | canonicalRequest := concat("\n", request.Method, normuri(request.URL.Path), normquery(request.URL.Query()), headersToSign, meta.signedHeaders, payloadHash) 50 | 51 | return hashSHA256([]byte(canonicalRequest)) 52 | } 53 | 54 | func stringToSignV4(request *http.Request, hashedCanonReq string, meta *metadata) string { 55 | // TASK 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 56 | 57 | requestTs := request.Header.Get("X-Amz-Date") 58 | 59 | meta.algorithm = "AWS4-HMAC-SHA256" 60 | meta.service, meta.region = serviceAndRegion(request.Host) 61 | meta.date = tsDateV4(requestTs) 62 | meta.credentialScope = concat("/", meta.date, meta.region, meta.service, "aws4_request") 63 | 64 | return concat("\n", meta.algorithm, requestTs, meta.credentialScope, hashedCanonReq) 65 | } 66 | 67 | func signatureV4(signingKey []byte, stringToSign string) string { 68 | // TASK 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 69 | 70 | return hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) 71 | } 72 | 73 | func prepareRequestV4(request *http.Request) *http.Request { 74 | necessaryDefaults := map[string]string{ 75 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 76 | "X-Amz-Date": timestampV4(), 77 | } 78 | 79 | for header, value := range necessaryDefaults { 80 | if request.Header.Get(header) == "" { 81 | request.Header.Set(header, value) 82 | } 83 | } 84 | 85 | if request.URL.Path == "" { 86 | request.URL.Path += "/" 87 | } 88 | 89 | return request 90 | } 91 | 92 | func signingKeyV4(secretKey, date, region, service string) []byte { 93 | kDate := hmacSHA256([]byte("AWS4"+secretKey), date) 94 | kRegion := hmacSHA256(kDate, region) 95 | kService := hmacSHA256(kRegion, service) 96 | kSigning := hmacSHA256(kService, "aws4_request") 97 | return kSigning 98 | } 99 | 100 | func buildAuthHeaderV4(signature string, meta *metadata, keys Credentials) string { 101 | credential := keys.AccessKeyID + "/" + meta.credentialScope 102 | 103 | return meta.algorithm + 104 | " Credential=" + credential + 105 | ", SignedHeaders=" + meta.signedHeaders + 106 | ", Signature=" + signature 107 | } 108 | 109 | func timestampV4() string { 110 | return now().Format(timeFormatV4) 111 | } 112 | 113 | func tsDateV4(timestamp string) string { 114 | return timestamp[:8] 115 | } 116 | 117 | const timeFormatV4 = "20060102T150405Z" 118 | -------------------------------------------------------------------------------- /sign4_test.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httputil" 6 | "net/url" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/smartystreets/assertions" 11 | "github.com/smartystreets/assertions/should" 12 | ) 13 | 14 | func TestVersion4RequestPreparer_1(t *testing.T) { 15 | // Given a plain request with no custom headers 16 | request := test_plainRequestV4(false) 17 | prepareRequestV4(request) 18 | 19 | expectedUnsigned := test_unsignedRequestV4(true, false) 20 | expectedUnsigned.Header.Set("X-Amz-Date", timestampV4()) 21 | 22 | assert := assertions.New(t) 23 | 24 | // The necessary, default headers should be appended 25 | assert.So(dumpRequest(request), should.Equal, dumpRequest(expectedUnsigned)) 26 | 27 | // Forward-slash should be appended to URI if not present 28 | assert.So(request.URL.Path, should.Equal, "/") 29 | } 30 | 31 | func TestVersion4RequestPreparer_2(t *testing.T) { 32 | // And a set of credentials 33 | // It should be signed with an Authorization header 34 | request := test_plainRequestV4(false) 35 | actualSigned := Sign4(request, *testCredV4) 36 | actual := actualSigned.Header.Get("Authorization") 37 | 38 | assert := assertions.New(t) 39 | assert.So(actual, should.NotBeBlank) 40 | assert.So(actual, should.ContainSubstring, "Credential="+testCredV4.AccessKeyID) 41 | assert.So(actual, should.ContainSubstring, "SignedHeaders=") 42 | assert.So(actual, should.ContainSubstring, "Signature=") 43 | assert.So(actual, should.ContainSubstring, "AWS4") 44 | } 45 | 46 | func TestVersion4RequestPreparer_3(t *testing.T) { 47 | // Given a request with custom, necessary headers 48 | // The custom, necessary headers must not be changed 49 | request := test_unsignedRequestV4(true, false) 50 | prepareRequestV4(request) 51 | assertions.New(t).So(dumpRequest(request), should.Equal, dumpRequest(test_unsignedRequestV4(true, false))) 52 | } 53 | 54 | func TestVersion4STSRequestPreparer(t *testing.T) { 55 | // Given a plain request with no custom headers 56 | request := test_plainRequestV4(false) 57 | 58 | // And a set of credentials with an STS token 59 | var keys Credentials 60 | keys = *testCredV4WithSTS 61 | 62 | // It should include an X-Amz-Security-Token when the request is signed 63 | actualSigned := Sign4(request, keys) 64 | actual := actualSigned.Header.Get("X-Amz-Security-Token") 65 | 66 | assert := assertions.New(t) 67 | assert.So(actual, should.NotBeBlank) 68 | assert.So(actual, should.Equal, testCredV4WithSTS.SecurityToken) 69 | } 70 | 71 | func TestVersion4SigningTasks(t *testing.T) { 72 | // http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html 73 | 74 | // Given a bogus request and credentials from AWS documentation with an additional meta tag 75 | request := test_unsignedRequestV4(true, true) 76 | meta := new(metadata) 77 | assert := assertions.New(t) 78 | 79 | // (Task 1) The canonical request should be built correctly 80 | hashedCanonReq := hashedCanonicalRequestV4(request, meta) 81 | assert.So(hashedCanonReq, should.Equal, expectingV4["CanonicalHash"]) 82 | 83 | // (Task 2) The string to sign should be built correctly 84 | stringToSign := stringToSignV4(request, hashedCanonReq, meta) 85 | assert.So(stringToSign, should.Equal, expectingV4["StringToSign"]) 86 | 87 | // (Task 3) The version 4 signed signature should be correct 88 | signature := signatureV4(test_signingKeyV4(), stringToSign) 89 | assert.So(signature, should.Equal, expectingV4["SignatureV4"]) 90 | } 91 | 92 | func TestSignature4Helpers(t *testing.T) { 93 | // The signing key should be properly generated 94 | expected := []byte{152, 241, 216, 137, 254, 196, 244, 66, 26, 220, 82, 43, 171, 12, 225, 248, 46, 105, 41, 194, 98, 237, 21, 229, 169, 76, 144, 239, 209, 227, 176, 231} 95 | actual := test_signingKeyV4() 96 | 97 | assertions.New(t).So(actual, should.Resemble, expected) 98 | } 99 | func TestSignature4Helpers_1(t *testing.T) { 100 | // Authorization headers should be built properly 101 | meta := &metadata{ 102 | algorithm: "AWS4-HMAC-SHA256", 103 | credentialScope: "20110909/us-east-1/iam/aws4_request", 104 | signedHeaders: "content-type;host;x-amz-date", 105 | } 106 | expected := expectingV4["AuthHeader"] + expectingV4["SignatureV4"] 107 | actual := buildAuthHeaderV4(expectingV4["SignatureV4"], meta, *testCredV4) 108 | 109 | assertions.New(t).So(actual, should.Equal, expected) 110 | } 111 | func TestSignature4Helpers_2(t *testing.T) { 112 | // Timestamps should be in the correct format, in UTC time 113 | actual := timestampV4() 114 | 115 | assert := assertions.New(t) 116 | assert.So(len(actual), should.Equal, 16) 117 | assert.So(actual, should.NotContainSubstring, ":") 118 | assert.So(actual, should.NotContainSubstring, "-") 119 | assert.So(actual, should.NotContainSubstring, " ") 120 | assert.So(actual, should.EndWith, "Z") 121 | assert.So(actual, should.ContainSubstring, "T") 122 | } 123 | func TestSignature4Helpers_3(t *testing.T) { 124 | // Given an Version 4 AWS-formatted timestamp 125 | ts := "20110909T233600Z" 126 | 127 | // The date string should be extracted properly 128 | assertions.New(t).So(tsDateV4(ts), should.Equal, "20110909") 129 | } 130 | func TestSignature4Helpers_4(t *testing.T) { 131 | // Given any request with a body 132 | request := test_plainRequestV4(false) 133 | 134 | // Its body should be read and replaced without differences 135 | expected := []byte(requestValuesV4.Encode()) 136 | assert := assertions.New(t) 137 | 138 | actual1 := readAndReplaceBody(request) 139 | assert.So(actual1, should.Resemble, expected) 140 | 141 | actual2 := readAndReplaceBody(request) 142 | assert.So(actual2, should.Resemble, expected) 143 | } 144 | 145 | func test_plainRequestV4(trailingSlash bool) *http.Request { 146 | address := "http://iam.amazonaws.com" 147 | body := strings.NewReader(requestValuesV4.Encode()) 148 | 149 | if trailingSlash { 150 | address += "/" 151 | } 152 | 153 | request, err := http.NewRequest("POST", address, body) 154 | 155 | if err != nil { 156 | panic(err) 157 | } 158 | 159 | return request 160 | } 161 | 162 | func test_unsignedRequestV4(trailingSlash, tag bool) *http.Request { 163 | request := test_plainRequestV4(trailingSlash) 164 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 165 | request.Header.Set("X-Amz-Date", "20110909T233600Z") 166 | if tag { 167 | request.Header.Set("X-Amz-Meta-Foo", "Bar!") 168 | } 169 | return request 170 | } 171 | 172 | func test_signingKeyV4() []byte { 173 | return signingKeyV4(testCredV4.SecretAccessKey, "20110909", "us-east-1", "iam") 174 | } 175 | 176 | func dumpRequest(request *http.Request) string { 177 | dump, _ := httputil.DumpRequestOut(request, true) 178 | return string(dump) 179 | } 180 | 181 | var ( 182 | testCredV4 = &Credentials{ 183 | AccessKeyID: "AKIDEXAMPLE", 184 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", 185 | } 186 | 187 | testCredV4WithSTS = &Credentials{ 188 | AccessKeyID: "AKIDEXAMPLE", 189 | SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", 190 | SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=", 191 | } 192 | 193 | expectingV4 = map[string]string{ 194 | "CanonicalHash": "41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92", 195 | "StringToSign": "AWS4-HMAC-SHA256\n20110909T233600Z\n20110909/us-east-1/iam/aws4_request\n41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92", 196 | "SignatureV4": "08292a4b86aae1a6f80f1988182a33cbf73ccc70c5da505303e355a67cc64cb4", 197 | "AuthHeader": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=", 198 | } 199 | 200 | requestValuesV4 = &url.Values{ 201 | "Action": []string{"ListUsers"}, 202 | "Version": []string{"2010-05-08"}, 203 | } 204 | ) 205 | --------------------------------------------------------------------------------