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