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