Signer.go raw

   1  // Copyright 2018-2025 JDCLOUD.COM
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //     http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  //
  15  // This signer is modified from AWS V4 signer algorithm.
  16  
  17  package core
  18  
  19  import (
  20  	"bytes"
  21  	"crypto/hmac"
  22  	"crypto/sha256"
  23  	"encoding/hex"
  24  	"fmt"
  25  	"github.com/gofrs/uuid"
  26  	"io"
  27  	"net/http"
  28  	"net/url"
  29  	"sort"
  30  	"strings"
  31  	"time"
  32  )
  33  
  34  const (
  35  	authHeaderPrefix = "JDCLOUD2-HMAC-SHA256"
  36  	timeFormat       = "20060102T150405Z"
  37  	shortTimeFormat  = "20060102"
  38  
  39  	// emptyStringSHA256 is a SHA256 of an empty string
  40  	emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
  41  )
  42  
  43  var ignoredHeaders = []string{"Authorization", "User-Agent", "X-Jdcloud-Request-Id"}
  44  var noEscape [256]bool
  45  
  46  func init() {
  47  	for i := 0; i < len(noEscape); i++ {
  48  		// expects every character except these to be escaped
  49  		noEscape[i] = (i >= 'A' && i <= 'Z') ||
  50  			(i >= 'a' && i <= 'z') ||
  51  			(i >= '0' && i <= '9') ||
  52  			i == '-' ||
  53  			i == '.' ||
  54  			i == '_' ||
  55  			i == '~'
  56  	}
  57  }
  58  
  59  type Signer struct {
  60  	Credentials Credential
  61  	Logger      Logger
  62  }
  63  
  64  func NewSigner(credsProvider Credential, logger Logger) *Signer {
  65  	return &Signer{
  66  		Credentials: credsProvider,
  67  		Logger:      logger,
  68  	}
  69  }
  70  
  71  type signingCtx struct {
  72  	ServiceName      string
  73  	Region           string
  74  	Request          *http.Request
  75  	Body             io.ReadSeeker
  76  	Query            url.Values
  77  	Time             time.Time
  78  	ExpireTime       time.Duration
  79  	SignedHeaderVals http.Header
  80  
  81  	credValues         Credential
  82  	formattedTime      string
  83  	formattedShortTime string
  84  
  85  	bodyDigest       string
  86  	signedHeaders    string
  87  	canonicalHeaders string
  88  	canonicalString  string
  89  	credentialString string
  90  	stringToSign     string
  91  	signature        string
  92  	authorization    string
  93  }
  94  
  95  // Sign signs the request by using AWS V4 signer algorithm, and adds Authorization header
  96  func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
  97  	return v4.signWithBody(r, body, service, region, 0, signTime)
  98  }
  99  
 100  func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration,
 101  	signTime time.Time) (http.Header, error) {
 102  
 103  	ctx := &signingCtx{
 104  		Request:     r,
 105  		Body:        body,
 106  		Query:       r.URL.Query(),
 107  		Time:        signTime,
 108  		ExpireTime:  exp,
 109  		ServiceName: service,
 110  		Region:      region,
 111  	}
 112  
 113  	for key := range ctx.Query {
 114  		sort.Strings(ctx.Query[key])
 115  	}
 116  
 117  	if ctx.isRequestSigned() {
 118  		ctx.Time = time.Now()
 119  	}
 120  
 121  	ctx.credValues = v4.Credentials
 122  	ctx.build()
 123  
 124  	v4.logSigningInfo(ctx)
 125  	return ctx.SignedHeaderVals, nil
 126  }
 127  
 128  const logSignInfoMsg = `DEBUG: Request Signature:
 129  ---[ CANONICAL STRING  ]-----------------------------
 130  %s
 131  ---[ STRING TO SIGN ]--------------------------------
 132  %s%s
 133  -----------------------------------------------------`
 134  
 135  func (v4 *Signer) logSigningInfo(ctx *signingCtx) {
 136  	signedURLMsg := ""
 137  	msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg)
 138  	v4.Logger.Log(LogInfo, msg)
 139  }
 140  
 141  func (ctx *signingCtx) build() {
 142  	ctx.buildTime()             // no depends
 143  	ctx.buildNonce()            // no depends
 144  	ctx.buildCredentialString() // no depends
 145  	ctx.buildBodyDigest()
 146  
 147  	unsignedHeaders := ctx.Request.Header
 148  	ctx.buildCanonicalHeaders(unsignedHeaders)
 149  	ctx.buildCanonicalString() // depends on canon headers / signed headers
 150  	ctx.buildStringToSign()    // depends on canon string
 151  	ctx.buildSignature()       // depends on string to sign
 152  
 153  	parts := []string{
 154  		authHeaderPrefix + " Credential=" + ctx.credValues.AccessKey + "/" + ctx.credentialString,
 155  		"SignedHeaders=" + ctx.signedHeaders,
 156  		"Signature=" + ctx.signature,
 157  	}
 158  	ctx.Request.Header.Set("Authorization", strings.Join(parts, ", "))
 159  }
 160  
 161  func (ctx *signingCtx) buildTime() {
 162  	ctx.formattedTime = ctx.Time.UTC().Format(timeFormat)
 163  	ctx.formattedShortTime = ctx.Time.UTC().Format(shortTimeFormat)
 164  
 165  	ctx.Request.Header.Set("x-jdcloud-date", ctx.formattedTime)
 166  }
 167  
 168  func (ctx *signingCtx) buildNonce() {
 169  	nonce, _ := uuid.NewV4()
 170  	ctx.Request.Header.Set("x-jdcloud-nonce", nonce.String())
 171  }
 172  
 173  func (ctx *signingCtx) buildCredentialString() {
 174  	ctx.credentialString = strings.Join([]string{
 175  		ctx.formattedShortTime,
 176  		ctx.Region,
 177  		ctx.ServiceName,
 178  		"jdcloud2_request",
 179  	}, "/")
 180  }
 181  
 182  func (ctx *signingCtx) buildCanonicalHeaders(header http.Header) {
 183  	var headers []string
 184  	headers = append(headers, "host")
 185  	for k, v := range header {
 186  		canonicalKey := http.CanonicalHeaderKey(k)
 187  		if shouldIgnore(canonicalKey, ignoredHeaders) {
 188  			continue // ignored header
 189  		}
 190  		if ctx.SignedHeaderVals == nil {
 191  			ctx.SignedHeaderVals = make(http.Header)
 192  		}
 193  
 194  		lowerCaseKey := strings.ToLower(k)
 195  		if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
 196  			// include additional values
 197  			ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
 198  			continue
 199  		}
 200  
 201  		headers = append(headers, lowerCaseKey)
 202  		ctx.SignedHeaderVals[lowerCaseKey] = v
 203  	}
 204  	sort.Strings(headers)
 205  
 206  	ctx.signedHeaders = strings.Join(headers, ";")
 207  
 208  	headerValues := make([]string, len(headers))
 209  	for i, k := range headers {
 210  		if k == "host" {
 211  			if ctx.Request.Host != "" {
 212  				headerValues[i] = "host:" + ctx.Request.Host
 213  			} else {
 214  				headerValues[i] = "host:" + ctx.Request.URL.Host
 215  			}
 216  		} else {
 217  			headerValues[i] = k + ":" +
 218  				strings.Join(ctx.SignedHeaderVals[k], ",")
 219  		}
 220  	}
 221  	stripExcessSpaces(headerValues)
 222  	ctx.canonicalHeaders = strings.Join(headerValues, "\n")
 223  }
 224  
 225  func (ctx *signingCtx) buildCanonicalString() {
 226  	uri := getURIPath(ctx.Request.URL)
 227  
 228  	ctx.canonicalString = strings.Join([]string{
 229  		ctx.Request.Method,
 230  		uri,
 231  		ctx.Request.URL.RawQuery,
 232  		ctx.canonicalHeaders + "\n",
 233  		ctx.signedHeaders,
 234  		ctx.bodyDigest,
 235  	}, "\n")
 236  }
 237  
 238  func (ctx *signingCtx) buildStringToSign() {
 239  	ctx.stringToSign = strings.Join([]string{
 240  		authHeaderPrefix,
 241  		ctx.formattedTime,
 242  		ctx.credentialString,
 243  		hex.EncodeToString(makeSha256([]byte(ctx.canonicalString))),
 244  	}, "\n")
 245  }
 246  
 247  func (ctx *signingCtx) buildSignature() {
 248  	secret := ctx.credValues.SecretKey
 249  	date := makeHmac([]byte("JDCLOUD2"+secret), []byte(ctx.formattedShortTime))
 250  	region := makeHmac(date, []byte(ctx.Region))
 251  	service := makeHmac(region, []byte(ctx.ServiceName))
 252  	credentials := makeHmac(service, []byte("jdcloud2_request"))
 253  	signature := makeHmac(credentials, []byte(ctx.stringToSign))
 254  	ctx.signature = hex.EncodeToString(signature)
 255  }
 256  
 257  func (ctx *signingCtx) buildBodyDigest() {
 258  	var hash string
 259  	if ctx.Body == nil {
 260  		hash = emptyStringSHA256
 261  	} else {
 262  		hash = hex.EncodeToString(makeSha256Reader(ctx.Body))
 263  	}
 264  
 265  	ctx.bodyDigest = hash
 266  }
 267  
 268  // isRequestSigned returns if the request is currently signed or presigned
 269  func (ctx *signingCtx) isRequestSigned() bool {
 270  	if ctx.Request.Header.Get("Authorization") != "" {
 271  		return true
 272  	}
 273  
 274  	return false
 275  }
 276  
 277  func makeHmac(key []byte, data []byte) []byte {
 278  	hash := hmac.New(sha256.New, key)
 279  	hash.Write(data)
 280  	return hash.Sum(nil)
 281  }
 282  
 283  func makeSha256(data []byte) []byte {
 284  	hash := sha256.New()
 285  	hash.Write(data)
 286  	return hash.Sum(nil)
 287  }
 288  
 289  func makeSha256Reader(reader io.ReadSeeker) []byte {
 290  	hash := sha256.New()
 291  	start, _ := reader.Seek(0, 1)
 292  	defer reader.Seek(start, 0)
 293  
 294  	io.Copy(hash, reader)
 295  	return hash.Sum(nil)
 296  }
 297  
 298  const doubleSpace = "  "
 299  
 300  // stripExcessSpaces will rewrite the passed in slice's string values to not
 301  // contain muliple side-by-side spaces.
 302  func stripExcessSpaces(vals []string) {
 303  	var j, k, l, m, spaces int
 304  	for i, str := range vals {
 305  		// Trim trailing spaces
 306  		for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
 307  		}
 308  
 309  		// Trim leading spaces
 310  		for k = 0; k < j && str[k] == ' '; k++ {
 311  		}
 312  		str = str[k : j+1]
 313  
 314  		// Strip multiple spaces.
 315  		j = strings.Index(str, doubleSpace)
 316  		if j < 0 {
 317  			vals[i] = str
 318  			continue
 319  		}
 320  
 321  		buf := []byte(str)
 322  		for k, m, l = j, j, len(buf); k < l; k++ {
 323  			if buf[k] == ' ' {
 324  				if spaces == 0 {
 325  					// First space.
 326  					buf[m] = buf[k]
 327  					m++
 328  				}
 329  				spaces++
 330  			} else {
 331  				// End of multiple spaces.
 332  				spaces = 0
 333  				buf[m] = buf[k]
 334  				m++
 335  			}
 336  		}
 337  
 338  		vals[i] = string(buf[:m])
 339  	}
 340  }
 341  
 342  func getURIPath(u *url.URL) string {
 343  	var uri string
 344  
 345  	if len(u.Opaque) > 0 {
 346  		uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
 347  	} else {
 348  		uri = u.EscapedPath()
 349  	}
 350  
 351  	if len(uri) == 0 {
 352  		uri = "/"
 353  	}
 354  
 355  	return uri
 356  }
 357  
 358  func shouldIgnore(header string, ignoreHeaders []string) bool {
 359  	for _, v := range ignoreHeaders {
 360  		if v == header {
 361  			return true
 362  		}
 363  	}
 364  	return false
 365  }
 366  
 367  // EscapePath escapes part of a URL path
 368  func EscapePath(path string, encodeSep bool) string {
 369  	var buf bytes.Buffer
 370  	for i := 0; i < len(path); i++ {
 371  		c := path[i]
 372  		if noEscape[c] || (c == '/' && !encodeSep) {
 373  			buf.WriteByte(c)
 374  		} else {
 375  			fmt.Fprintf(&buf, "%%%02X", c)
 376  		}
 377  	}
 378  	return buf.String()
 379  }
 380