github.com/huaweicloud/golangsdk@v0.0.0-20210831081626-d823fe11ceba/signer_helper.go (about) 1 package golangsdk 2 3 import ( 4 "bytes" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "net/textproto" 14 "net/url" 15 "regexp" 16 "sort" 17 "strings" 18 "sync" 19 "time" 20 ) 21 22 // MemoryCache presents a thread safe memory cache 23 type MemoryCache struct { 24 sync.Mutex // handling r/w for cache 25 cacheHolder map[string]string // cache holder 26 cacheKeys []string // cache keys 27 MaxCount int // max cache entry count 28 } 29 30 // NewCache inits an new MemoryCache 31 func NewCache(maxCount int) *MemoryCache { 32 return &MemoryCache{ 33 cacheHolder: make(map[string]string, maxCount), 34 MaxCount: maxCount, 35 } 36 } 37 38 // Add an new cache item 39 func (cache *MemoryCache) Add(cacheKey string, cacheData string) { 40 cache.Lock() 41 defer cache.Unlock() 42 43 if len(cache.cacheKeys) >= cache.MaxCount && len(cache.cacheKeys) > 1 { 44 delete(cache.cacheHolder, cache.cacheKeys[0]) // delete first item 45 cache.cacheKeys = append(cache.cacheKeys[1:]) // pop first one 46 } 47 48 cache.cacheHolder[cacheKey] = cacheData 49 cache.cacheKeys = append(cache.cacheKeys, cacheKey) 50 } 51 52 // Get a cache item by its key 53 func (cache *MemoryCache) Get(cacheKey string) string { 54 cache.Lock() 55 defer cache.Unlock() 56 57 return cache.cacheHolder[cacheKey] 58 } 59 60 //caseInsencitiveStringArray represents string case insensitive sorting operations 61 type caseInsencitiveStringArray []string 62 63 // noEscape specifies whether the character should be encoded or not 64 var noEscape [256]bool 65 66 func init() { 67 // refer to https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html 68 for i := 0; i < len(noEscape); i++ { 69 noEscape[i] = (i >= 'A' && i <= 'Z') || 70 (i >= 'a' && i <= 'z') || 71 (i >= '0' && i <= '9') || 72 i == '.' || 73 i == '-' || 74 i == '_' || 75 i == '~' //java-sdk-core 3.0.1 HttpUtils.urlEncode 76 } 77 } 78 79 // SignOptions represents the options during signing http request, it is concurency safely 80 type SignOptions struct { 81 AccessKey string //Access Key 82 SecretKey string //Secret key 83 RegionName string // Region name 84 ServiceName string // Service Name 85 EnableCacheSignKey bool // Cache sign key for one day or not cache, cache is disabled by default 86 encodeUrl bool //internal use 87 SignAlgorithm string //The algorithm used for sign, the default value is "SDK-HMAC-SHA256" if you don't set its value 88 TimeOffsetInseconds int64 // TimeOffsetInseconds is used for adjust x-sdk-date if set its value 89 } 90 91 // StringBuilder wraps bytes.Buffer to implement a high performance string builder 92 type StringBuilder struct { 93 builder bytes.Buffer //string storage 94 } 95 96 // reqSignParams represents the option values used for signing http request 97 type reqSignParams struct { 98 SignOptions 99 RequestTime time.Time 100 Req *http.Request 101 } 102 103 // signKeyCacheEntry represents the cache entry of sign key 104 type signKeyCacheEntry struct { 105 Key []byte // sign key 106 NumberOfDaysSinceEpoch int64 // number of days since epoch 107 } 108 109 // The default sign algorithm 110 const SignAlgorithmHMACSHA256 = "SDK-HMAC-SHA256" 111 112 // The header key of content hash value 113 const ContentSha256HeaderKey = "x-sdk-content-sha256" 114 115 //A regular for searching empty string 116 var spaceRegexp = regexp.MustCompile(`\s+`) 117 118 // cache sign key 119 var cache = NewCache(300) 120 121 //Sign manipulates the http.Request instance with some required authentication headers for SK/SK auth 122 func Sign(req *http.Request, signOptions SignOptions) { 123 signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey) 124 signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey) 125 signOptions.encodeUrl = true 126 127 signParams := reqSignParams{ 128 SignOptions: signOptions, 129 RequestTime: time.Now(), 130 Req: req, 131 } 132 133 //t, _ := time.Parse(time.RFC3339, "2018-04-15T04:28:22+00:00") 134 //signParams.RequestTime = t 135 136 if signParams.SignAlgorithm == "" { 137 signParams.SignAlgorithm = SignAlgorithmHMACSHA256 138 } 139 140 addRequiredHeaders(req, signParams.getFormattedSigningDateTime()) 141 contentSha256 := "" 142 143 if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok { 144 contentSha256 = calculateContentHash(req) 145 } else { 146 contentSha256 = v[0] 147 } 148 149 canonicalRequest := createCanonicalRequest(signParams, contentSha256) 150 151 /*fmt.Println("canonicalRequest: " + canonicalRequest) 152 fmt.Println("*****")*/ 153 154 strToSign := createStringToSign(canonicalRequest, signParams) 155 signKey := deriveSigningKey(signParams) 156 signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm) 157 158 req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature)) 159 } 160 161 //ReSign manipulates the http.Request instance with some required authentication headers for SK/SK auth 162 func ReSign(req *http.Request, signOptions SignOptions) { 163 signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey) 164 signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey) 165 signOptions.encodeUrl = true 166 167 signParams := reqSignParams{ 168 SignOptions: signOptions, 169 RequestTime: time.Now(), 170 Req: req, 171 } 172 173 if signParams.SignAlgorithm == "" { 174 signParams.SignAlgorithm = SignAlgorithmHMACSHA256 175 } 176 177 setRequiredHeaders(req, signParams.getFormattedSigningDateTime()) 178 contentSha256 := "" 179 180 if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok { 181 contentSha256 = calculateContentHash(req) 182 } else { 183 contentSha256 = v[0] 184 } 185 186 canonicalRequest := createCanonicalRequest(signParams, contentSha256) 187 188 strToSign := createStringToSign(canonicalRequest, signParams) 189 signKey := deriveSigningKey(signParams) 190 signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm) 191 192 req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature)) 193 } 194 195 // deriveSigningKey returns a sign key from cache, or build it and insert it into cache 196 func deriveSigningKey(signParam reqSignParams) []byte { 197 if signParam.EnableCacheSignKey { 198 cacheKey := strings.Join([]string{signParam.SecretKey, 199 signParam.RegionName, 200 signParam.ServiceName, 201 }, "-") 202 203 cacheData := cache.Get(cacheKey) 204 205 if cacheData != "" { 206 var signKey signKeyCacheEntry 207 json.Unmarshal([]byte(cacheData), &signKey) 208 209 if signKey.NumberOfDaysSinceEpoch == signParam.getDaysSinceEpon() { 210 return signKey.Key 211 } 212 } 213 214 signKey := buildSignKey(signParam) 215 signKeyStr, _ := json.Marshal(signKeyCacheEntry{ 216 Key: signKey, 217 NumberOfDaysSinceEpoch: signParam.getDaysSinceEpon(), 218 }) 219 cache.Add(cacheKey, string(signKeyStr)) 220 return signKey 221 } else { 222 return buildSignKey(signParam) 223 } 224 } 225 226 func buildSignKey(signParam reqSignParams) []byte { 227 var kSecret StringBuilder 228 kSecret.Write("SDK").Write(signParam.SecretKey) 229 230 kDate := computeSignature(signParam.getFormattedSigningDate(), kSecret.GetBytes(), signParam.SignAlgorithm) 231 kRegion := computeSignature(signParam.RegionName, kDate, signParam.SignAlgorithm) 232 kService := computeSignature(signParam.ServiceName, kRegion, signParam.SignAlgorithm) 233 return computeSignature("sdk_request", kService, signParam.SignAlgorithm) 234 } 235 236 //HmacSha256 implements the Keyed-Hash Message Authentication Code computation 237 func HmacSha256(data string, key []byte) []byte { 238 mac := hmac.New(sha256.New, key) 239 mac.Write([]byte(data)) 240 return mac.Sum(nil) 241 } 242 243 // HashSha256 is a wrapper for sha256 implementation 244 func HashSha256(msg []byte) []byte { 245 sh256 := sha256.New() 246 sh256.Write(msg) 247 248 return sh256.Sum(nil) 249 } 250 251 // buildAuthorizationHeader builds the authentication header value 252 func buildAuthorizationHeader(signParam reqSignParams, signature []byte) string { 253 var signingCredentials StringBuilder 254 signingCredentials.Write(signParam.AccessKey).Write("/").Write(signParam.getScope()) 255 256 credential := "Credential=" + signingCredentials.ToString() 257 signerHeaders := "SignedHeaders=" + getSignedHeadersString(signParam.Req) 258 signatureHeader := "Signature=" + hex.EncodeToString(signature) 259 260 return signParam.SignAlgorithm + " " + strings.Join([]string{ 261 credential, 262 signerHeaders, 263 signatureHeader, 264 }, ", ") 265 } 266 267 // computeSignature computers the signature with the specified algorithm 268 // and it only supports SDK-HMAC-SHA256 in currently 269 func computeSignature(signData string, key []byte, algorithm string) []byte { 270 if algorithm == SignAlgorithmHMACSHA256 { 271 return HmacSha256(signData, key) 272 } else { 273 log.Fatalf("Unsupported algorithm %s, please use %s and try again", algorithm, SignAlgorithmHMACSHA256) 274 return nil 275 } 276 } 277 278 // createStringToSign build the need to be signed string 279 func createStringToSign(canonicalRequest string, signParams reqSignParams) string { 280 return strings.Join([]string{signParams.SignAlgorithm, 281 signParams.getFormattedSigningDateTime(), 282 signParams.getScope(), 283 hex.EncodeToString(HashSha256([]byte(canonicalRequest))), 284 }, "\n") 285 } 286 287 // getCanonicalizedResourcePath builds the valid url path for signing 288 func getCanonicalizedResourcePath(signParas reqSignParams) string { 289 urlStr := signParas.Req.URL.Path 290 if !strings.HasPrefix(urlStr, "/") { 291 urlStr = "/" + urlStr 292 } 293 294 if !strings.HasSuffix(urlStr, "/") { 295 urlStr = urlStr + "/" 296 } 297 298 if signParas.encodeUrl { 299 urlStr = urlEncode(urlStr, true) 300 } 301 302 if urlStr == "" { 303 urlStr = "/" 304 } 305 306 return urlStr 307 } 308 309 // urlEncode encodes url path and url querystring according to the following rules: 310 // The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the same. 311 //The special characters ".", "-", "*", and "_" remain the same. 312 //The space character " " is converted into a plus sign "%20". 313 //All other characters are unsafe and are first converted into one or more bytes using some encoding scheme. 314 func urlEncode(url string, urlPath bool) string { 315 var buf bytes.Buffer 316 for i := 0; i < len(url); i++ { 317 c := url[i] 318 if noEscape[c] || (c == '/' && urlPath) { 319 buf.WriteByte(c) 320 } else { 321 fmt.Fprintf(&buf, "%%%02X", c) 322 } 323 } 324 325 return buf.String() 326 } 327 328 // encodeQueryString build and encode querystring to a string for signing 329 func encodeQueryString(queryValues url.Values) string { 330 var encodedVals = make(map[string]string, len(queryValues)) 331 var keys = make([]string, len(queryValues)) 332 333 i := 0 334 335 for k := range queryValues { 336 keys[i] = urlEncode(k, false) 337 encodedVals[keys[i]] = k 338 i++ 339 } 340 341 caseInsensitiveSort(keys) 342 343 var queryStr StringBuilder 344 for i, k := range keys { 345 if i > 0 { 346 queryStr.Write("&") 347 } 348 349 queryStr.Write(k).Write("=").Write(urlEncode(queryValues.Get(encodedVals[k]), false)) 350 } 351 352 return queryStr.ToString() 353 } 354 355 // getCanonicalizedQueryString return empty string if in POST method and content is nil, otherwise returns sorted,encoded querystring 356 func getCanonicalizedQueryString(signParas reqSignParams) string { 357 if usePayloadForQueryParameters(signParas.Req) { 358 return "" 359 } else { 360 return encodeQueryString(signParas.Req.URL.Query()) 361 } 362 } 363 364 // createCanonicalRequest builds canonical string depends the official document for signing 365 func createCanonicalRequest(signParas reqSignParams, contentSha256 string) string { 366 return strings.Join([]string{signParas.Req.Method, 367 getCanonicalizedResourcePath(signParas), 368 getCanonicalizedQueryString(signParas), 369 getCanonicalizedHeaderString(signParas.Req), 370 getSignedHeadersString(signParas.Req), 371 contentSha256, 372 }, "\n") 373 } 374 375 // calculateContentHash computes the content hash value 376 func calculateContentHash(req *http.Request) string { 377 encodeParas := "" 378 379 //post and content is null use queryString as content -- according to document 380 if usePayloadForQueryParameters(req) { 381 encodeParas = req.URL.Query().Encode() 382 } else { 383 if req.Body == nil { 384 encodeParas = "" 385 } else { 386 readBody, _ := ioutil.ReadAll(req.Body) 387 req.Body = ioutil.NopCloser(bytes.NewBuffer(readBody)) 388 encodeParas = string(readBody) 389 } 390 } 391 392 return hex.EncodeToString(HashSha256([]byte(encodeParas))) 393 } 394 395 // usePayloadForQueryParameters specifies use querystring or not as content for compute content hash 396 func usePayloadForQueryParameters(req *http.Request) bool { 397 if strings.ToLower(req.Method) != "post" { 398 return false 399 } 400 401 return req.Body == nil 402 } 403 404 // getCanonicalizedHeaderString converts header map to a string for signing 405 func getCanonicalizedHeaderString(req *http.Request) string { 406 var headers StringBuilder 407 408 keys := make([]string, 0) 409 for k := range req.Header { 410 keys = append(keys, strings.TrimSpace(k)) 411 } 412 413 caseInsensitiveSort(keys) 414 415 for _, k := range keys { 416 k = strings.ToLower(k) 417 newKey := spaceRegexp.ReplaceAllString(k, " ") 418 headers.Write(newKey) 419 headers.Write(":") 420 421 val := req.Header.Get(k) 422 val = spaceRegexp.ReplaceAllString(val, " ") 423 headers.Write(val) 424 425 headers.Write("\n") 426 } 427 428 return headers.ToString() 429 } 430 431 // getSignedHeadersString builds the string for AuthorizationHeader and signing 432 func getSignedHeadersString(req *http.Request) string { 433 var headers StringBuilder 434 435 keys := make([]string, 0) 436 for k := range req.Header { 437 keys = append(keys, strings.TrimSpace(k)) 438 } 439 440 caseInsensitiveSort(keys) 441 442 for idx, k := range keys { 443 444 if idx > 0 { 445 headers.Write(";") 446 } 447 448 headers.Write(strings.ToLower(k)) 449 } 450 451 return headers.ToString() 452 } 453 454 // addRequiredHeaders adds the required heads to http.request instance 455 func addRequiredHeaders(req *http.Request, timeStr string) { 456 // golang handls port by default 457 req.Header.Add("Host", req.URL.Host) 458 req.Header.Add("X-Sdk-Date", timeStr) 459 } 460 461 // setRequiredHeaders sets the required heads to http.request for redirection 462 func setRequiredHeaders(req *http.Request, timeStr string) { 463 req.Header.Set("X-Sdk-Date", timeStr) 464 req.Header.Del("Authorization") 465 } 466 467 func (s caseInsencitiveStringArray) Len() int { 468 return len(s) 469 } 470 func (s caseInsencitiveStringArray) Swap(i, j int) { 471 s[i], s[j] = s[j], s[i] 472 } 473 func (s caseInsencitiveStringArray) Less(i, j int) bool { 474 return strings.ToLower(s[i]) < strings.ToLower(s[j]) 475 } 476 477 func caseInsensitiveSort(strSlice []string) { 478 sort.Sort(caseInsencitiveStringArray(strSlice)) 479 } 480 481 func (signParas *reqSignParams) getSigningDateTimeMilli() int64 { 482 return (signParas.RequestTime.UTC().Unix() - signParas.TimeOffsetInseconds) * 1000 483 } 484 485 func (signParas *reqSignParams) getSigningDateTime() time.Time { 486 return time.Unix(signParas.getSigningDateTimeMilli()/1000, 0) 487 } 488 489 func (signParas *reqSignParams) getDaysSinceEpon() int64 { 490 return signParas.getSigningDateTimeMilli() / 1000 / 3600 / 24 491 } 492 493 func (signParas *reqSignParams) getFormattedSigningDate() string { 494 return signParas.getSigningDateTime().UTC().Format("20060102") 495 } 496 func (signParas *reqSignParams) getFormattedSigningDateTime() string { 497 return signParas.getSigningDateTime().UTC().Format("20060102T150405Z") 498 } 499 500 func (signParas *reqSignParams) getScope() string { 501 return strings.Join([]string{signParas.getFormattedSigningDate(), 502 signParas.RegionName, 503 signParas.ServiceName, 504 "sdk_request", 505 }, "/") 506 } 507 508 func (buff *StringBuilder) Write(s string) *StringBuilder { 509 buff.builder.WriteString((s)) 510 return buff 511 } 512 513 func (buff *StringBuilder) ToString() string { 514 return buff.builder.String() 515 } 516 517 func (buff *StringBuilder) GetBytes() []byte { 518 return []byte(buff.ToString()) 519 }