Skip to content

Commit 75673a2

Browse files
committed
Fix SigV4 canonical query string for valueless parameters
CreateMultipartUpload uses `?uploads` which gives RawQuery="uploads", but AWS SigV4 requires valueless parameters to be encoded as "uploads=". Fix the `sign` function to build the canonical query string via url.ParseQuery so all parameters are properly sorted and encoded regardless of how the URL was constructed.
1 parent 6d15cb3 commit 75673a2

1 file changed

Lines changed: 28 additions & 1 deletion

File tree

cmd/gradle-cache/s3.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,33 @@ func (c *s3Client) objectURL(bucket, key string) string {
430430
return sb.String()
431431
}
432432

433+
// canonQueryString builds the AWS SigV4 canonical query string from rawQuery.
434+
// Unlike req.URL.RawQuery (which may omit the "=" for valueless params like
435+
// "?uploads"), this function always emits "key=" for empty values, percent-
436+
// encodes with %XX (not "+"), and sorts parameters alphabetically.
437+
func canonQueryString(rawQuery string) string {
438+
if rawQuery == "" {
439+
return ""
440+
}
441+
params, _ := url.ParseQuery(rawQuery)
442+
pairs := make([]string, 0, len(params))
443+
for k, vs := range params {
444+
ek := awsQueryEscape(k)
445+
for _, v := range vs {
446+
pairs = append(pairs, ek+"="+awsQueryEscape(v))
447+
}
448+
}
449+
sort.Strings(pairs)
450+
return strings.Join(pairs, "&")
451+
}
452+
453+
// awsQueryEscape percent-encodes s per the AWS SigV4 spec: all characters
454+
// except unreserved (A-Z a-z 0-9 - _ . ~) become %XX with uppercase hex.
455+
// Unlike url.QueryEscape it never uses "+".
456+
func awsQueryEscape(s string) string {
457+
return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
458+
}
459+
433460
// sign adds AWS Signature Version 4 headers to req using UNSIGNED-PAYLOAD,
434461
// which is permitted for all requests over HTTPS.
435462
func (c *s3Client) sign(req *http.Request) {
@@ -465,7 +492,7 @@ func (c *s3Client) sign(req *http.Request) {
465492

466493
canonReq := req.Method + "\n" +
467494
req.URL.EscapedPath() + "\n" +
468-
req.URL.RawQuery + "\n" +
495+
canonQueryString(req.URL.RawQuery) + "\n" +
469496
canonHdrs.String() + "\n" +
470497
signedNames.String() + "\n" +
471498
"UNSIGNED-PAYLOAD"

0 commit comments

Comments
 (0)