@@ -10,73 +10,84 @@ import (
10
10
"net/http"
11
11
"net/url"
12
12
"sort"
13
+ "strconv"
13
14
"strings"
14
15
"time"
15
16
)
16
17
17
- const signingAlgo = "AWS4-HMAC-SHA256"
18
- const awsServiceName = "aps"
19
-
20
18
type Signer interface {
21
19
Sign (req * http.Request ) error
22
20
}
23
21
24
22
type DefaultSigner struct {
25
- iSO8601Date string
26
- canonicalHeaders string
27
- signedHeaders string
28
- credentialScope string
29
- config * Config
30
- payloadHash string
23
+ config * Config
31
24
}
32
25
33
26
func NewDefaultSigner (config * Config ) Signer {
34
- return & DefaultSigner {
35
- config : config ,
27
+ // initialize noEscape array. This way we can avoid using init() functions
28
+ for i := 0 ; i < len (noEscape ); i ++ {
29
+ // AWS expects every character except these to be escaped
30
+ noEscape [i ] = (i >= 'A' && i <= 'Z' ) ||
31
+ (i >= 'a' && i <= 'z' ) ||
32
+ (i >= '0' && i <= '9' ) ||
33
+ i == '-' ||
34
+ i == '.' ||
35
+ i == '_' ||
36
+ i == '~'
36
37
}
38
+
39
+ return & DefaultSigner {config : config }
37
40
}
38
41
39
42
func (d * DefaultSigner ) Sign (req * http.Request ) error {
40
43
now := time .Now ().UTC ()
41
- d .iSO8601Date = now .Format ("20060102T150405Z" )
42
- d .credentialScope = fmt .Sprintf (
43
- "%s/%s/%s/aws4_request" ,
44
- now .UTC ().Format ("20060102" ),
45
- d .config .Region ,
46
- awsServiceName ,
47
- )
44
+ iSO8601Date := now .Format (timeFormat )
45
+
46
+ credentialScope := buildCredentialScope (now , d .config .Region )
48
47
49
48
payloadHash , err := d .getPayloadHash (req )
50
49
if err != nil {
51
50
return err
52
51
}
53
52
54
- d . payloadHash = payloadHash
55
- d . addRequiredHeaders ( req )
56
- d . canonicalHeaders , d . signedHeaders = d . getCanonicalAndSignedHeaders ( req )
53
+ req . Header . Set ( "Host" , req . Host )
54
+ req . Header . Set ( amzDateKey , iSO8601Date )
55
+ req . Header . Set ( contentSHAKey , payloadHash )
57
56
58
- canonicalReq := d .createCanonicalRequest (req )
59
- stringToSign , err := d .createStringToSign (canonicalReq )
60
- if err != nil {
61
- return err
62
- }
57
+ _ , signedHeadersStr , canonicalHeaderStr := buildCanonicalHeaders (req )
58
+
59
+ canonicalQueryString := getCanonicalQueryString (req .URL )
60
+ canonicalReq := buildCanonicalString (
61
+ req .Method ,
62
+ getCanonicalURI (req .URL ),
63
+ canonicalQueryString ,
64
+ canonicalHeaderStr ,
65
+ signedHeadersStr ,
66
+ payloadHash ,
67
+ )
68
+
69
+ signature := sign (
70
+ deriveKey (d .config .AwsSecretAccessKey , d .config .Region ),
71
+ buildStringToSign (iSO8601Date , credentialScope , canonicalReq ),
72
+ )
63
73
64
- signature := d .sign (d .createSigningKey (), stringToSign )
65
74
authorizationHeader := fmt .Sprintf (
66
75
"%s Credential=%s/%s, SignedHeaders=%s, Signature=%s" ,
67
- signingAlgo ,
76
+ signingAlgorithm ,
68
77
d .config .AwsAccessKeyID ,
69
- d . credentialScope ,
70
- d . signedHeaders ,
78
+ credentialScope ,
79
+ signedHeadersStr ,
71
80
signature ,
72
81
)
73
- req .Header .Set ("Authorization" , authorizationHeader )
82
+
83
+ req .URL .RawQuery = canonicalQueryString
84
+ req .Header .Set (authorizationHeaderKey , authorizationHeader )
74
85
return nil
75
86
}
76
87
77
88
func (d * DefaultSigner ) getPayloadHash (req * http.Request ) (string , error ) {
78
89
if req .Body == nil {
79
- return hex . EncodeToString ( sha256 . New (). Sum ( nil )) , nil
90
+ return emptyStringSHA256 , nil
80
91
}
81
92
82
93
reqBody , err := io .ReadAll (req .Body )
@@ -98,101 +109,144 @@ func (d *DefaultSigner) getPayloadHash(req *http.Request) (string, error) {
98
109
return payloadHash , nil
99
110
}
100
111
101
- func (d * DefaultSigner ) addRequiredHeaders (req * http.Request ) {
102
- req .Header .Set ("Host" , req .Host )
103
- req .Header .Set ("x-amz-date" , d .iSO8601Date )
104
- req .Header .Set ("x-amz-content-sha256" , d .payloadHash )
112
+ func buildCredentialScope (signingTime time.Time , region string ) string {
113
+ return fmt .Sprintf (
114
+ "%s/%s/%s/aws4_request" ,
115
+ signingTime .UTC ().Format (shortTimeFormat ),
116
+ region ,
117
+ awsServiceName ,
118
+ )
105
119
}
106
120
107
- func (d * DefaultSigner ) getCanonicalAndSignedHeaders (req * http.Request ) (string , string ) {
108
- var headers []string
109
- var signedHeaders []string
121
+ func buildCanonicalString (method , uri , query , canonicalHeaders , signedHeaders , payloadHash string ) string {
122
+ return strings .Join ([]string {
123
+ method ,
124
+ uri ,
125
+ query ,
126
+ canonicalHeaders ,
127
+ signedHeaders ,
128
+ payloadHash ,
129
+ }, "\n " )
130
+ }
110
131
111
- for key , value := range req . Header {
112
- lowercaseKey := strings . ToLower ( key )
113
- encodedValue := strings . TrimSpace ( strings . Join ( value , "," ))
114
- headers = append ( headers , lowercaseKey + ":" + encodedValue )
115
- signedHeaders = append ( signedHeaders , lowercaseKey )
116
- }
132
+ var ignoredHeaders = map [ string ] struct {} {
133
+ "Authorization" : struct {}{},
134
+ "User-Agent" : struct {}{},
135
+ "X-Amzn-Trace-Id" : struct {}{},
136
+ "Expect" : struct {}{},
137
+ }
117
138
118
- sort . Strings ( headers )
119
- sort . Strings ( signedHeaders )
139
+ func buildCanonicalHeaders ( req * http. Request ) ( signed http. Header , signedHeaders , canonicalHeadersStr string ) {
140
+ host , header , length := req . Host , req . Header , req . ContentLength
120
141
121
- canonicalHeaders := strings .Join (headers , "\n " ) + "\n "
122
- canonicalSignedHeaders := strings .Join (signedHeaders , ";" )
123
- return canonicalHeaders , canonicalSignedHeaders
124
- }
142
+ signed = make (http.Header )
125
143
126
- func (d * DefaultSigner ) createCanonicalRequest (req * http.Request ) string {
127
- return strings .Join ([]string {
128
- req .Method ,
129
- d .getCanonicalURI (req .URL ),
130
- d .getCanonicalQueryString (req .URL ),
131
- d .canonicalHeaders ,
132
- d .signedHeaders ,
133
- d .payloadHash ,
134
- }, "\n " )
135
- }
144
+ var headers []string
145
+ const hostHeader = "host"
146
+ headers = append (headers , hostHeader )
147
+ signed [hostHeader ] = append (signed [hostHeader ], host )
148
+
149
+ const contentLengthHeader = "content-length"
150
+ if length > 0 {
151
+ headers = append (headers , contentLengthHeader )
152
+ signed [contentLengthHeader ] = append (signed [contentLengthHeader ], strconv .FormatInt (length , 10 ))
153
+ }
154
+
155
+ for k , v := range header {
156
+ if _ , ok := ignoredHeaders [k ]; ok {
157
+ continue // ignored header
158
+ }
159
+ if strings .EqualFold (k , contentLengthHeader ) {
160
+ // prevent signing already handled content-length header.
161
+ continue
162
+ }
136
163
137
- func (d * DefaultSigner ) getCanonicalURI (u * url.URL ) string {
138
- if u .Path == "" {
139
- return "/"
164
+ lowerCaseKey := strings .ToLower (k )
165
+ if _ , ok := signed [lowerCaseKey ]; ok {
166
+ // include additional values
167
+ signed [lowerCaseKey ] = append (signed [lowerCaseKey ], v ... )
168
+ continue
169
+ }
170
+
171
+ headers = append (headers , lowerCaseKey )
172
+ signed [lowerCaseKey ] = v
140
173
}
174
+ sort .Strings (headers )
141
175
142
- // The spec requires not to encode `/`
143
- segments := strings .Split (u .Path , "/" )
144
- for i , segment := range segments {
145
- segments [i ] = url .PathEscape (segment )
176
+ signedHeaders = strings .Join (headers , ";" )
177
+
178
+ var canonicalHeaders strings.Builder
179
+ n := len (headers )
180
+ const colon = ':'
181
+ for i := 0 ; i < n ; i ++ {
182
+ if headers [i ] == hostHeader {
183
+ canonicalHeaders .WriteString (hostHeader )
184
+ canonicalHeaders .WriteRune (colon )
185
+ canonicalHeaders .WriteString (stripExcessSpaces (host ))
186
+ } else {
187
+ canonicalHeaders .WriteString (headers [i ])
188
+ canonicalHeaders .WriteRune (colon )
189
+ // Trim out leading, trailing, and dedup inner spaces from signed header values.
190
+ values := signed [headers [i ]]
191
+ for j , v := range values {
192
+ cleanedValue := strings .TrimSpace (stripExcessSpaces (v ))
193
+ canonicalHeaders .WriteString (cleanedValue )
194
+ if j < len (values )- 1 {
195
+ canonicalHeaders .WriteRune (',' )
196
+ }
197
+ }
198
+ }
199
+ canonicalHeaders .WriteRune ('\n' )
146
200
}
201
+ canonicalHeadersStr = canonicalHeaders .String ()
147
202
148
- return strings . Join ( segments , "/" )
203
+ return signed , signedHeaders , canonicalHeadersStr
149
204
}
150
205
151
- func ( d * DefaultSigner ) getCanonicalQueryString (u * url.URL ) string {
152
- queryParams := u . Query ( )
153
- var queryPairs [] string
206
+ func getCanonicalURI (u * url.URL ) string {
207
+ return escapePath ( getURIPath ( u ), false )
208
+ }
154
209
155
- for key , values := range queryParams {
156
- for _ , value := range values {
157
- queryPairs = append (queryPairs , url .QueryEscape (key )+ "=" + url .QueryEscape (value ))
158
- }
159
- }
210
+ func getCanonicalQueryString (u * url.URL ) string {
211
+ query := u .Query ()
160
212
161
- sort .Strings (queryPairs )
213
+ // Sort Each Query Key's Values
214
+ for key := range query {
215
+ sort .Strings (query [key ])
216
+ }
162
217
163
- return strings .Join (queryPairs , "&" )
218
+ var rawQuery strings.Builder
219
+ rawQuery .WriteString (strings .Replace (query .Encode (), "+" , "%20" , - 1 ))
220
+ return rawQuery .String ()
164
221
}
165
222
166
- func ( d * DefaultSigner ) createStringToSign ( canonicalRequest string ) ( string , error ) {
223
+ func buildStringToSign ( amzDate , credentialScope , canonicalRequestString string ) string {
167
224
hash := sha256 .New ()
168
- if _ , err := hash .Write ([]byte (canonicalRequest )); err != nil {
169
- return "" , err
170
- }
171
- return fmt .Sprintf (
172
- "%s\n %s\n %s\n %s" ,
173
- signingAlgo ,
174
- d .iSO8601Date ,
175
- d .credentialScope ,
225
+ hash .Write ([]byte (canonicalRequestString ))
226
+ return strings .Join ([]string {
227
+ signingAlgorithm ,
228
+ amzDate ,
229
+ credentialScope ,
176
230
hex .EncodeToString (hash .Sum (nil )),
177
- ), nil
231
+ }, " \n " )
178
232
}
179
233
180
- func ( d * DefaultSigner ) createSigningKey ( ) string {
181
- signingDate := time .Now ().UTC ().Format ("20060102" )
182
- dateKey := d . hmacSHA256 ([]byte ("AWS4" + d . config . AwsSecretAccessKey ), signingDate )
183
- dateRegionKey := d . hmacSHA256 (dateKey , d . config . Region )
184
- dateRegionServiceKey := d . hmacSHA256 (dateRegionKey , awsServiceName )
185
- signingKey := d . hmacSHA256 (dateRegionServiceKey , "aws4_request" )
234
+ func deriveKey ( secretKey , region string ) string {
235
+ signingDate := time .Now ().UTC ().Format (shortTimeFormat )
236
+ hmacDate := hmacSHA256 ([]byte ("AWS4" + secretKey ), signingDate )
237
+ hmacRegion := hmacSHA256 (hmacDate , region )
238
+ hmacService := hmacSHA256 (hmacRegion , awsServiceName )
239
+ signingKey := hmacSHA256 (hmacService , "aws4_request" )
186
240
return string (signingKey )
187
241
}
188
242
189
- func ( d * DefaultSigner ) hmacSHA256 (key []byte , data string ) []byte {
243
+ func hmacSHA256 (key []byte , data string ) []byte {
190
244
h := hmac .New (sha256 .New , key )
191
245
h .Write ([]byte (data ))
192
246
return h .Sum (nil )
193
247
}
194
248
195
- func ( d * DefaultSigner ) sign (signingKey string , strToSign string ) string {
249
+ func sign (signingKey string , strToSign string ) string {
196
250
h := hmac .New (sha256 .New , []byte (signingKey ))
197
251
h .Write ([]byte (strToSign ))
198
252
sig := hex .EncodeToString (h .Sum (nil ))
0 commit comments