forked from smarty-archives/go-aws-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
s3sigv2.go
156 lines (139 loc) · 5.37 KB
/
s3sigv2.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Package s3sigv2 implements AWS S3 request signing using Signature Version 2,
// documentation on the signature structure can be found at
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html.
package s3sigv2
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"hash"
"net/http"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/request"
)
const (
s3TimeFormat = time.RFC1123Z
s3Subresources = "acl,lifecycle,location,logging,notification,partNumber,policy,requestPayment,torrent,uploadId,uploads,versionId,versioning,versions,website"
)
var subresourcesArray []string
// S3CredentialsPair stores the information necessary to authenticate against
// the S3-compatible API and provides methods to create signatures and/or attach
// them to requests.
type S3CredentialsPair struct {
AccessKeyID string
SecretAccessKey string
SecurityToken string `json:"Token"`
hmacSHA1 hash.Hash
}
// GetSignatureBytes returns the raw bytes of the generated request signature
// (not entire auth header). This will insert a `Date` header into the request
// if it doesn't exist, as S3 signatures require a valid `Date` or `x-amz-date`
// header.
func (c *S3CredentialsPair) GetSignatureBytes(req *http.Request) []byte {
prepareRequest(req)
return c.SignBytesHmacSHA1([]byte(stringToSign(req)))
}
// SignHTTPRequest signs a request by adding missing headers and constructing a
// string to use for the `Authorization` request header.
func (c *S3CredentialsPair) SignHTTPRequest(req *http.Request) *http.Request {
prepareRequest(req)
signature := c.SignBytesHmacSHA1([]byte(stringToSign(req)))
authHeader := "AWS " + c.AccessKeyID + ":" + base64.StdEncoding.EncodeToString(signature)
req.Header.Set("Authorization", authHeader)
return req
}
// SignSDKRequest signs a request by adding missing headers and constructing a
// string to use for the `Authorization` request header. This is just a
// shorthand for
// `s3CredentialsPair.SignHTTPRequest(request.Request.HTTPRequest)`.
func (c *S3CredentialsPair) SignSDKRequest(req *request.Request) {
c.SignHTTPRequest(req.HTTPRequest)
}
// SignBytesHmacSHA1 signs a []byte using the SecretAccessKey and returns it.
func (c *S3CredentialsPair) SignBytesHmacSHA1(content []byte) []byte {
if c.hmacSHA1 == nil {
c.hmacSHA1 = hmac.New(sha1.New, []byte(c.SecretAccessKey))
}
c.hmacSHA1.Write(content)
hash := c.hmacSHA1.Sum(nil)
c.hmacSHA1.Reset()
return hash
}
// stringToSign generates a raw string that will later be signed using HMAC-SHA1
// so that the request destination can verify the request using the secret key.
// Refer to Amazon's documentation on the signature specification at
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader
// for more information.
func stringToSign(req *http.Request) string {
str := req.Method + "\n"
// The signature specification only requires MD5 in stringToSign when
// the Content-MD5 header is present. http.Header.Get() will return an
// empty string when there are no values for that header in the header
// map.
str += req.Header.Get("Content-MD5") + "\n"
str += req.Header.Get("Content-Type") + "\n"
str += req.Header.Get("Date") + "\n"
if canonicalHeaders := canonicalAmzHeaders(req); canonicalHeaders != "" {
str += canonicalHeaders
}
str += canonicalResource(req)
return str
}
// canonicalAmzHeaders generates a string from a HTTP request of x-amz headers
// and their values in order with comma-separated values. Refer to Amazon's
// documentation on the signature specification at
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationConstructingCanonicalizedAmzHeaders
// for more information.
func canonicalAmzHeaders(req *http.Request) string {
var headers []string
for header := range req.Header {
standardized := strings.ToLower(strings.TrimSpace(header))
if strings.HasPrefix(standardized, "x-amz") {
headers = append(headers, standardized)
}
}
sort.Strings(headers)
for i, header := range headers {
headers[i] = header + ":" + strings.Replace(req.Header.Get(header), "\n", " ", -1)
}
if len(headers) > 0 {
return strings.Join(headers, "\n") + "\n"
}
return ""
}
// canonicalResource generates an S3 "canonical resource" Refer to Amazon's
// documentation on the signature specification at
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheCanonicalizedResourceElement
// for more information
func canonicalResource(req *http.Request) string {
resource := ""
// TODO: use a more reliable method to determine virtual hosts
if strings.Count(req.Host, ".") == 3 {
bucketname := strings.Split(req.Host, ".")[0]
resource += "/" + bucketname
}
resource += req.URL.Path
if subresourcesArray == nil {
subresourcesArray = strings.Split(s3Subresources, ",")
}
for _, subres := range subresourcesArray {
if strings.HasPrefix(req.URL.RawQuery, subres) {
resource += "?" + subres
}
}
return resource
}
// prepareRequest inserts a `Date` header into the request, inserts a security
// token header into the request if supplied, and normalizes the request path if
// it is empty.
func prepareRequest(req *http.Request, token ...string) {
req.Header.Set("Date", time.Now().UTC().Format(s3TimeFormat))
if len(token) > 0 && len(token[0]) > 0 {
req.Header.Set("X-Amz-Security-Token", token[0])
}
if req.URL.Path == "" {
req.URL.Path += "/"
}
}