aksk_signer.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /*
  2. Package signer providers functions for sign http request before request cloud.
  3. */
  4. package aksk
  5. import (
  6. "bytes"
  7. "crypto/hmac"
  8. "crypto/sha256"
  9. "encoding/hex"
  10. "encoding/json"
  11. "fmt"
  12. "io/ioutil"
  13. "log"
  14. "net/http"
  15. "net/textproto"
  16. "net/url"
  17. "regexp"
  18. "sort"
  19. "strings"
  20. "time"
  21. )
  22. //caseInsencitiveStringArray represents string case insensitive sorting operations
  23. type caseInsencitiveStringArray []string
  24. // noEscape specifies whether the character should be encoded or not
  25. var noEscape [256]bool
  26. func init() {
  27. // refer to https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html
  28. for i := 0; i < len(noEscape); i++ {
  29. noEscape[i] = (i >= 'A' && i <= 'Z') ||
  30. (i >= 'a' && i <= 'z') ||
  31. (i >= '0' && i <= '9') ||
  32. i == '.' ||
  33. i == '-' ||
  34. i == '_' ||
  35. i == '~' //java-sdk-core 3.0.1 HttpUtils.urlEncode
  36. }
  37. }
  38. /*
  39. SignOptions represents the options during signing http request, it is concurrency safely.
  40. Sample code:
  41. client := &http.Client{
  42. Timeout: time.Duration(3 * time.Second),
  43. }
  44. req, err := http.NewRequest("POST", "https://30030113-3657-4fb6-a7ef-90764239b038.apigw.xxx.yyy.com/app1?name=value",bytes.NewBuffer([]byte("demo")))
  45. signOptions := signer.SignOptions{
  46. AccessKey: "------------",
  47. SecretKey: "------------",
  48. }
  49. signer.Sign(req, signOptions)
  50. resp, err := client.Do(req)
  51. */
  52. type SignOptions struct {
  53. AccessKey string //Access Key
  54. SecretKey string //Secret key
  55. RegionName string // Region name
  56. ServiceName string // Service Name
  57. EnableCacheSignKey bool // Cache sign key for one day or not cache, cache is disabled by default
  58. encodeUrl bool //internal use
  59. SignAlgorithm string //The algorithm used for sign, the default value is "SDK-HMAC-SHA256" if you don't set its value
  60. TimeOffsetInseconds int64 // TimeOffsetInseconds is used for adjust x-sdk-date if set its value
  61. }
  62. // StringBuilder wraps bytes.Buffer to implement a high performance string builder.
  63. type StringBuilder struct {
  64. builder bytes.Buffer //string storage
  65. }
  66. // reqSignParams represents the option values used for signing http request.
  67. type reqSignParams struct {
  68. SignOptions
  69. RequestTime time.Time
  70. Req *http.Request
  71. }
  72. // signKeyCacheEntry represents the cache entry of sign key.
  73. type signKeyCacheEntry struct {
  74. Key []byte // sign key
  75. NumberOfDaysSinceEpoch int64 // number of days since epoch
  76. }
  77. // The default sign algorithm
  78. const SignAlgorithmHMACSHA256 = "SDK-HMAC-SHA256"
  79. // The header key of content hash value
  80. const ContentSha256HeaderKey = "x-sdk-content-sha256"
  81. //A regular for searching empty string
  82. var spaceRegexp = regexp.MustCompile(`\s+`)
  83. // cache sign key
  84. var cache = NewCache(300)
  85. //Sign manipulates the http.Request instance with some required authentication headers for SK/SK auth.
  86. func Sign(req *http.Request, signOptions SignOptions) {
  87. signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey)
  88. signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey)
  89. signOptions.encodeUrl = true
  90. signParams := reqSignParams{
  91. SignOptions: signOptions,
  92. RequestTime: time.Now(),
  93. Req: req,
  94. }
  95. //t, _ := time.Parse(time.RFC3339, "2018-04-15T04:28:22+00:00")
  96. //signParams.RequestTime = t
  97. if signParams.SignAlgorithm == "" {
  98. signParams.SignAlgorithm = SignAlgorithmHMACSHA256
  99. }
  100. addRequiredHeaders(req, signParams.getFormattedSigningDateTime())
  101. contentSha256 := ""
  102. if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok {
  103. contentSha256 = calculateContentHash(req)
  104. } else {
  105. contentSha256 = v[0]
  106. }
  107. canonicalRequest := createCanonicalRequest(signParams, contentSha256)
  108. /*fmt.Println("canonicalRequest: " + canonicalRequest)
  109. fmt.Println("*****")*/
  110. strToSign := createStringToSign(canonicalRequest, signParams)
  111. signKey := deriveSigningKey(signParams)
  112. signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm)
  113. req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature))
  114. }
  115. // deriveSigningKey returns a sign key from cache, or build it and insert it into cache
  116. func deriveSigningKey(signParam reqSignParams) []byte {
  117. if signParam.EnableCacheSignKey {
  118. cacheKey := strings.Join([]string{signParam.SecretKey,
  119. signParam.RegionName,
  120. signParam.ServiceName,
  121. }, "-")
  122. cacheData := cache.Get(cacheKey)
  123. if cacheData != "" {
  124. var signKey signKeyCacheEntry
  125. json.Unmarshal([]byte(cacheData), &signKey)
  126. if signKey.NumberOfDaysSinceEpoch == signParam.getDaysSinceEpon() {
  127. return signKey.Key
  128. }
  129. }
  130. signKey := buildSignKey(signParam)
  131. signKeyStr, _ := json.Marshal(signKeyCacheEntry{
  132. Key: signKey,
  133. NumberOfDaysSinceEpoch: signParam.getDaysSinceEpon(),
  134. })
  135. cache.Add(cacheKey, string(signKeyStr))
  136. return signKey
  137. } else {
  138. return buildSignKey(signParam)
  139. }
  140. }
  141. func buildSignKey(signParam reqSignParams) []byte {
  142. var kSecret StringBuilder
  143. kSecret.Write("SDK").Write(signParam.SecretKey)
  144. kDate := computeSignature(signParam.getFormattedSigningDate(), kSecret.GetBytes(), signParam.SignAlgorithm)
  145. kRegion := computeSignature(signParam.RegionName, kDate, signParam.SignAlgorithm)
  146. kService := computeSignature(signParam.ServiceName, kRegion, signParam.SignAlgorithm)
  147. return computeSignature("sdk_request", kService, signParam.SignAlgorithm)
  148. }
  149. //HmacSha256 implements the Keyed-Hash Message Authentication Code computation.
  150. func HmacSha256(data string, key []byte) []byte {
  151. mac := hmac.New(sha256.New, key)
  152. mac.Write([]byte(data))
  153. return mac.Sum(nil)
  154. }
  155. // HashSha256 is a wrapper for sha256 implementation.
  156. func HashSha256(msg []byte) []byte {
  157. sh256 := sha256.New()
  158. sh256.Write(msg)
  159. return sh256.Sum(nil)
  160. }
  161. // buildAuthorizationHeader builds the authentication header value
  162. func buildAuthorizationHeader(signParam reqSignParams, signature []byte) string {
  163. var signingCredentials StringBuilder
  164. signingCredentials.Write(signParam.AccessKey).Write("/").Write(signParam.getScope())
  165. credential := "Credential=" + signingCredentials.ToString()
  166. signerHeaders := "SignedHeaders=" + getSignedHeadersString(signParam.Req)
  167. signatureHeader := "Signature=" + hex.EncodeToString(signature)
  168. return signParam.SignAlgorithm + " " + strings.Join([]string{
  169. credential,
  170. signerHeaders,
  171. signatureHeader,
  172. }, ", ")
  173. }
  174. // computeSignature computers the signature with the specified algorithm
  175. // and it only supports SDK-HMAC-SHA256 in currently
  176. func computeSignature(signData string, key []byte, algorithm string) []byte {
  177. if algorithm == SignAlgorithmHMACSHA256 {
  178. return HmacSha256(signData, key)
  179. } else {
  180. log.Fatalf("Unsupported algorithm %s, please use %s and try again", algorithm, SignAlgorithmHMACSHA256)
  181. return nil
  182. }
  183. }
  184. // createStringToSign build the need to be signed string
  185. func createStringToSign(canonicalRequest string, signParams reqSignParams) string {
  186. return strings.Join([]string{signParams.SignAlgorithm,
  187. signParams.getFormattedSigningDateTime(),
  188. signParams.getScope(),
  189. hex.EncodeToString(HashSha256([]byte(canonicalRequest))),
  190. }, "\n")
  191. }
  192. // getCanonicalizedResourcePath builds the valid url path for signing
  193. func getCanonicalizedResourcePath(signParas reqSignParams) string {
  194. urlStr := signParas.Req.URL.Path
  195. if !strings.HasPrefix(urlStr, "/") {
  196. urlStr = "/" + urlStr
  197. }
  198. if !strings.HasSuffix(urlStr, "/") {
  199. urlStr = urlStr + "/"
  200. }
  201. if signParas.encodeUrl {
  202. urlStr = urlEncode(urlStr, true)
  203. }
  204. if urlStr == "" {
  205. urlStr = "/"
  206. }
  207. return urlStr
  208. }
  209. // urlEncode encodes url path and url querystring according to the following rules:
  210. // The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the same.
  211. //The special characters ".", "-", "*", and "_" remain the same.
  212. //The space character " " is converted into a plus sign "%20".
  213. //All other characters are unsafe and are first converted into one or more bytes using some encoding scheme.
  214. func urlEncode(url string, urlPath bool) string {
  215. var buf bytes.Buffer
  216. for i := 0; i < len(url); i++ {
  217. c := url[i]
  218. if noEscape[c] || (c == '/' && urlPath) {
  219. buf.WriteByte(c)
  220. } else {
  221. fmt.Fprintf(&buf, "%%%02X", c)
  222. }
  223. }
  224. return buf.String()
  225. }
  226. // encodeQueryString build and encode querystring to a string for signing
  227. func encodeQueryString(queryValues url.Values) string {
  228. var encodedVals = make(map[string]string, len(queryValues))
  229. var keys = make([]string, len(queryValues))
  230. i := 0
  231. for k, _ := range queryValues {
  232. keys[i] = urlEncode(k, false)
  233. encodedVals[keys[i]] = k
  234. i++
  235. }
  236. caseInsensitiveSort(keys)
  237. var queryStr StringBuilder
  238. for i, k := range keys {
  239. if i > 0 {
  240. queryStr.Write("&")
  241. }
  242. queryStr.Write(k).Write("=").Write(urlEncode(queryValues.Get(encodedVals[k]), false))
  243. }
  244. return queryStr.ToString()
  245. }
  246. // getCanonicalizedQueryString return empty string if in POST method and content is nil, otherwise returns sorted,encoded querystring
  247. func getCanonicalizedQueryString(signParas reqSignParams) string {
  248. if usePayloadForQueryParameters(signParas.Req) {
  249. return ""
  250. } else {
  251. return encodeQueryString(signParas.Req.URL.Query())
  252. }
  253. }
  254. // createCanonicalRequest builds canonical string depends the official document for signing
  255. func createCanonicalRequest(signParas reqSignParams, contentSha256 string) string {
  256. return strings.Join([]string{signParas.Req.Method,
  257. getCanonicalizedResourcePath(signParas),
  258. getCanonicalizedQueryString(signParas),
  259. getCanonicalizedHeaderString(signParas.Req),
  260. getSignedHeadersString(signParas.Req),
  261. contentSha256,
  262. }, "\n")
  263. }
  264. // calculateContentHash computes the content hash value
  265. func calculateContentHash(req *http.Request) string {
  266. encodeParas := ""
  267. //post and content is null use queryString as content -- according to document
  268. if usePayloadForQueryParameters(req) {
  269. encodeParas = req.URL.Query().Encode()
  270. } else {
  271. if req.Body == nil {
  272. encodeParas = ""
  273. } else {
  274. readBody, _ := ioutil.ReadAll(req.Body)
  275. req.Body = ioutil.NopCloser(bytes.NewBuffer(readBody))
  276. encodeParas = string(readBody)
  277. }
  278. }
  279. return hex.EncodeToString(HashSha256([]byte(encodeParas)))
  280. }
  281. // usePayloadForQueryParameters specifies use querystring or not as content for compute content hash
  282. func usePayloadForQueryParameters(req *http.Request) bool {
  283. if strings.ToLower(req.Method) != "post" {
  284. return false
  285. }
  286. return req.Body == nil
  287. }
  288. // getCanonicalizedHeaderString converts header map to a string for signing
  289. func getCanonicalizedHeaderString(req *http.Request) string {
  290. var headers StringBuilder
  291. keys := make([]string, 0)
  292. for k, _ := range req.Header {
  293. keys = append(keys, strings.TrimSpace(k))
  294. }
  295. caseInsensitiveSort(keys)
  296. for _, k := range keys {
  297. k = strings.ToLower(k)
  298. newKey := spaceRegexp.ReplaceAllString(k, " ")
  299. headers.Write(newKey)
  300. headers.Write(":")
  301. val := req.Header.Get(k)
  302. val = spaceRegexp.ReplaceAllString(val, " ")
  303. headers.Write(val)
  304. headers.Write("\n")
  305. }
  306. return headers.ToString()
  307. }
  308. // getSignedHeadersString builds the string for AuthorizationHeader and signing
  309. func getSignedHeadersString(req *http.Request) string {
  310. var headers StringBuilder
  311. keys := make([]string, 0)
  312. for k, _ := range req.Header {
  313. keys = append(keys, strings.TrimSpace(k))
  314. }
  315. caseInsensitiveSort(keys)
  316. for idx, k := range keys {
  317. if idx > 0 {
  318. headers.Write(";")
  319. }
  320. headers.Write(strings.ToLower(k))
  321. }
  322. return headers.ToString()
  323. }
  324. // addRequiredHeaders adds the required heads to http.request instance
  325. func addRequiredHeaders(req *http.Request, timeStr string) {
  326. // golang handls port by default
  327. req.Header.Add("Host", req.URL.Host)
  328. req.Header.Add("X-Sdk-Date", timeStr)
  329. }
  330. func (s caseInsencitiveStringArray) Len() int {
  331. return len(s)
  332. }
  333. func (s caseInsencitiveStringArray) Swap(i, j int) {
  334. s[i], s[j] = s[j], s[i]
  335. }
  336. func (s caseInsencitiveStringArray) Less(i, j int) bool {
  337. return strings.ToLower(s[i]) < strings.ToLower(s[j])
  338. }
  339. func caseInsensitiveSort(strSlice []string) {
  340. sort.Sort(caseInsencitiveStringArray(strSlice))
  341. }
  342. func (signParas *reqSignParams) getSigningDateTimeMilli() int64 {
  343. return (signParas.RequestTime.UTC().Unix() - signParas.TimeOffsetInseconds) * 1000
  344. }
  345. func (signParas *reqSignParams) getSigningDateTime() time.Time {
  346. return time.Unix(signParas.getSigningDateTimeMilli()/1000, 0)
  347. }
  348. func (signParas *reqSignParams) getDaysSinceEpon() int64 {
  349. return signParas.getSigningDateTimeMilli() / 1000 / 3600 / 24
  350. }
  351. func (signParas *reqSignParams) getFormattedSigningDate() string {
  352. return signParas.getSigningDateTime().UTC().Format("20060102")
  353. }
  354. func (signParas *reqSignParams) getFormattedSigningDateTime() string {
  355. return signParas.getSigningDateTime().UTC().Format("20060102T150405Z")
  356. }
  357. func (signParas *reqSignParams) getScope() string {
  358. return strings.Join([]string{signParas.getFormattedSigningDate(),
  359. signParas.RegionName,
  360. signParas.ServiceName,
  361. "sdk_request",
  362. }, "/")
  363. }
  364. func (buff *StringBuilder) Write(s string) *StringBuilder {
  365. buff.builder.WriteString(s)
  366. return buff
  367. }
  368. func (buff *StringBuilder) ToString() string {
  369. return buff.builder.String()
  370. }
  371. func (buff *StringBuilder) GetBytes() []byte {
  372. return []byte(buff.ToString())
  373. }