sign.go raw
1 package base
2
3 import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/md5"
7 "crypto/sha256"
8 "encoding/base64"
9 "encoding/hex"
10 "fmt"
11 "io/ioutil"
12 "net/http"
13 "net/url"
14 "sort"
15 "strings"
16 "time"
17 )
18
19 func (c Credentials) Sign(request *http.Request) *http.Request {
20 query := request.URL.Query()
21 request.URL.RawQuery = query.Encode()
22
23 if request.URL.Path == "" {
24 request.URL.Path += "/"
25 }
26 requestParam := RequestParam{
27 IsSignUrl: false,
28 Body: readAndReplaceBody(request),
29 Host: request.Host,
30 Path: request.URL.Path,
31 Method: request.Method,
32 Date: now(),
33 QueryList: query,
34 Headers: request.Header,
35 }
36 signRequest := GetSignRequest(requestParam, c)
37
38 request.Header.Set("Host", signRequest.Host)
39 request.Header.Set("Content-Type", signRequest.ContentType)
40 request.Header.Set("X-Date", signRequest.XDate)
41 request.Header.Set("X-Content-Sha256", signRequest.XContentSha256)
42 request.Header.Set("Authorization", signRequest.Authorization)
43 if signRequest.XSecurityToken != "" {
44 request.Header.Set("X-Security-Token", signRequest.XSecurityToken)
45 }
46 return request
47 }
48
49 func (c Credentials) SignUrl(request *http.Request) string {
50 query := request.URL.Query()
51
52 requestParam := RequestParam{
53 IsSignUrl: true,
54 Body: readAndReplaceBody(request),
55 Host: request.Host,
56 Path: request.URL.Path,
57 Method: request.Method,
58 Date: now(),
59 QueryList: query,
60 Headers: request.Header,
61 }
62 signRequest := GetSignRequest(requestParam, c)
63
64 query.Set("X-Date", signRequest.XDate)
65 query.Set("X-NotSignBody", signRequest.XNotSignBody)
66 query.Set("X-Credential", signRequest.XCredential)
67 query.Set("X-Algorithm", signRequest.XAlgorithm)
68 query.Set("X-SignedHeaders", signRequest.XSignedHeaders)
69 query.Set("X-SignedQueries", signRequest.XSignedQueries)
70 query.Set("X-Signature", signRequest.XSignature)
71 if signRequest.XSecurityToken != "" {
72 query.Set("X-Security-Token", signRequest.XSecurityToken)
73 }
74 return query.Encode()
75 }
76
77 func GetSignRequest(requestParam RequestParam, credentials Credentials) SignRequest {
78 formatDate := appointTimestampV4(requestParam.Date)
79 meta := getMetaData(credentials, tsDateV4(formatDate))
80
81 requestSignMap := make(map[string][]string)
82
83 signRequest := SignRequest{
84 XDate: formatDate,
85 XSecurityToken: credentials.SessionToken,
86 }
87 var bodyHash string
88 if requestParam.IsSignUrl {
89 for k, v := range requestParam.QueryList {
90 requestSignMap[k] = v
91 }
92 if credentials.SessionToken != "" {
93 requestSignMap["X-Security-Token"] = []string{credentials.SessionToken}
94 }
95 requestSignMap["X-Date"], requestSignMap["X-NotSignBody"], requestSignMap["X-Credential"], requestSignMap["X-Algorithm"], requestSignMap["X-SignedHeaders"], requestSignMap["X-SignedQueries"] =
96 []string{formatDate}, []string{""}, []string{credentials.AccessKeyID + "/" + meta.credentialScope}, []string{meta.algorithm}, []string{meta.signedHeaders}, []string{""}
97
98 keys := make([]string, 0, len(requestSignMap))
99 for k := range requestSignMap {
100 keys = append(keys, k)
101 }
102 sort.Strings(keys)
103 requestSignMap["X-SignedQueries"] = []string{strings.Join(keys, ";")}
104
105 signRequest.XNotSignBody, signRequest.XCredential, signRequest.XAlgorithm, signRequest.XSignedHeaders, signRequest.XSignedQueries =
106 "", credentials.AccessKeyID+"/"+meta.credentialScope, meta.algorithm, meta.signedHeaders, strings.Join(keys, ";")
107 bodyHash = hashSHA256([]byte{})
108 } else {
109 for k, v := range requestParam.Headers {
110 requestSignMap[k] = v
111 }
112 if credentials.SessionToken != "" {
113 requestSignMap["X-Security-Token"] = []string{credentials.SessionToken}
114 }
115 if requestSignMap["Content-Type"] == nil {
116 signRequest.ContentType = "application/x-www-form-urlencoded; charset=utf-8"
117 } else {
118 signRequest.ContentType = requestSignMap["Content-Type"][0]
119 }
120 requestSignMap["X-Date"], requestSignMap["Host"], requestSignMap["Content-Type"] = []string{formatDate}, []string{requestParam.Host}, []string{signRequest.ContentType}
121
122 if len(requestParam.Body) == 0 {
123 bodyHash = hashSHA256([]byte{})
124 } else {
125 bodyHash = hashSHA256(requestParam.Body)
126 }
127 requestSignMap["X-Content-Sha256"] = []string{bodyHash}
128 signRequest.Host, signRequest.XContentSha256 = requestParam.Host, bodyHash
129 }
130
131 signature := getSignatureStr(requestParam, meta, credentials.SecretAccessKey, formatDate, requestSignMap, bodyHash)
132 if requestParam.IsSignUrl {
133 signRequest.XSignature = signature
134 } else {
135 signRequest.Authorization = buildAuthHeaderV4(signature, meta, credentials)
136 }
137 return signRequest
138 }
139
140 func getSignatureStr(requestParam RequestParam, meta *metadata, secretAccessKey string,
141 formatDate string, requestSignMap map[string][]string, bodyHash string) string {
142 // Task 1
143 hashedCanonReq := hashedCanonicalRequestV4(requestParam, meta, requestSignMap, bodyHash)
144
145 // Task 2
146 stringToSign := concat("\n", meta.algorithm, formatDate, meta.credentialScope, hashedCanonReq)
147
148 // Task 3
149 signingKey := signingKeyV4(secretAccessKey, meta.date, meta.region, meta.service)
150 return signatureV4(signingKey, stringToSign)
151 }
152
153 func hashedCanonicalRequestV4(param RequestParam, meta *metadata, requestSignMap map[string][]string, bodyHash string) string {
154 var canonicalRequest string
155 if param.IsSignUrl {
156 queryList := make(url.Values)
157 for k, v := range requestSignMap {
158 for i := range v {
159 queryList.Set(k, v[i])
160 }
161 }
162 canonicalRequest = concat("\n", param.Method, normuri(param.Path), normquery(queryList), "\n", meta.signedHeaders, bodyHash)
163 } else {
164 canonicalHeaders := getCanonicalHeaders(param, meta, requestSignMap)
165 canonicalRequest = concat("\n", param.Method, normuri(param.Path), normquery(param.QueryList), canonicalHeaders, meta.signedHeaders, bodyHash)
166 }
167 return hashSHA256([]byte(canonicalRequest))
168 }
169
170 func getCanonicalHeaders(param RequestParam, meta *metadata, requestSignMap map[string][]string) string {
171 signMap := make(map[string][]string)
172 signedHeaders := sortHeaders(requestSignMap, signMap)
173 if !param.IsSignUrl {
174 meta.signedHeaders = concat(";", signedHeaders...)
175 }
176 if param.Path == "" {
177 param.Path = "/"
178 }
179 var headersToSign string
180 for _, key := range signedHeaders {
181 value := strings.TrimSpace(signMap[key][0])
182 if key == "host" {
183 if strings.Contains(value, ":") {
184 split := strings.Split(value, ":")
185 port := split[1]
186 if port == "80" || port == "443" {
187 value = split[0]
188 }
189 }
190 }
191 headersToSign += key + ":" + value + "\n"
192 }
193 return headersToSign
194 }
195
196 func sortHeaders(requestSignMap map[string][]string, signMap map[string][]string) []string {
197 var sortedHeaderKeys []string
198 for k, v := range requestSignMap {
199 signMap[strings.ToLower(k)] = v
200 switch k {
201 case "Content-Type", "Content-Md5", "Host", "X-Security-Token":
202 default:
203 if !strings.HasPrefix(k, "X-") {
204 continue
205 }
206 }
207 sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(k))
208 }
209 sort.Strings(sortedHeaderKeys)
210 return sortedHeaderKeys
211 }
212
213 func getMetaData(credentials Credentials, date string) *metadata {
214 meta := new(metadata)
215 meta.date, meta.service, meta.region, meta.signedHeaders, meta.algorithm = date, credentials.Service, credentials.Region, "", "HMAC-SHA256"
216 meta.credentialScope = concat("/", meta.date, meta.region, meta.service, "request")
217 return meta
218 }
219
220 func signatureV4(signingKey []byte, stringToSign string) string {
221 return hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
222 }
223
224 func signingKeyV4(secretKey, date, region, service string) []byte {
225 kDate := hmacSHA256([]byte(secretKey), date)
226 kRegion := hmacSHA256(kDate, region)
227 kService := hmacSHA256(kRegion, service)
228 kSigning := hmacSHA256(kService, "request")
229 return kSigning
230 }
231
232 func buildAuthHeaderV4(signature string, meta *metadata, keys Credentials) string {
233 credential := keys.AccessKeyID + "/" + meta.credentialScope
234
235 return meta.algorithm +
236 " Credential=" + credential +
237 ", SignedHeaders=" + meta.signedHeaders +
238 ", Signature=" + signature
239 }
240
241 func timestampV4() string {
242 return now().Format(timeFormatV4)
243 }
244
245 func appointTimestampV4(date time.Time) string {
246 return date.Format(timeFormatV4)
247 }
248 func tsDateV4(timestamp string) string {
249 return timestamp[:8]
250 }
251
252 func hmacSHA256(key []byte, content string) []byte {
253 mac := hmac.New(sha256.New, key)
254 mac.Write([]byte(content))
255 return mac.Sum(nil)
256 }
257
258 func hashSHA256(content []byte) string {
259 h := sha256.New()
260 h.Write(content)
261 return fmt.Sprintf("%x", h.Sum(nil))
262 }
263
264 func hashMD5(content []byte) string {
265 h := md5.New()
266 h.Write(content)
267 return base64.StdEncoding.EncodeToString(h.Sum(nil))
268 }
269
270 func readAndReplaceBody(request *http.Request) []byte {
271 if request.Body == nil {
272 return []byte{}
273 }
274 payload, _ := ioutil.ReadAll(request.Body)
275 request.Body = ioutil.NopCloser(bytes.NewReader(payload))
276 return payload
277 }
278
279 func concat(delim string, str ...string) string {
280 return strings.Join(str, delim)
281 }
282
283 var now = func() time.Time {
284 return time.Now().UTC()
285 }
286
287 func normuri(uri string) string {
288 parts := strings.Split(uri, "/")
289 for i := range parts {
290 parts[i] = encodePathFrag(parts[i])
291 }
292 return strings.Join(parts, "/")
293 }
294
295 func encodePathFrag(s string) string {
296 hexCount := 0
297 for i := 0; i < len(s); i++ {
298 c := s[i]
299 if shouldEscape(c) {
300 hexCount++
301 }
302 }
303 t := make([]byte, len(s)+2*hexCount)
304 j := 0
305 for i := 0; i < len(s); i++ {
306 c := s[i]
307 if shouldEscape(c) {
308 t[j] = '%'
309 t[j+1] = "0123456789ABCDEF"[c>>4]
310 t[j+2] = "0123456789ABCDEF"[c&15]
311 j += 3
312 } else {
313 t[j] = c
314 j++
315 }
316 }
317 return string(t)
318 }
319
320 func shouldEscape(c byte) bool {
321 if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
322 return false
323 }
324 if '0' <= c && c <= '9' {
325 return false
326 }
327 if c == '-' || c == '_' || c == '.' || c == '~' {
328 return false
329 }
330 return true
331 }
332
333 func normquery(v url.Values) string {
334 queryString := v.Encode()
335
336 return strings.Replace(queryString, "+", "%20", -1)
337 }
338