package backup import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "net/url" "slices" "strings" "time" ) // emptySHA256 is the payload hash of a request with no body. const emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // sign adds AWS Signature Version 4 headers for S3 to req: X-Amz-Date, // X-Amz-Content-Sha256 and Authorization. Every header already on the // request is signed, along with the host. func sign(req *http.Request, payloadSHA256, accessKey, secretKey, region string, at time.Time) { at = at.UTC() stamp := at.Format("20060102T150405Z") date := at.Format("20060102") req.Header.Set("X-Amz-Date", stamp) req.Header.Set("X-Amz-Content-Sha256", payloadSHA256) host := req.Host if host == "" { host = req.URL.Host } headers := map[string]string{"host": host} for name, values := range req.Header { headers[strings.ToLower(name)] = strings.TrimSpace(strings.Join(values, ",")) } names := make([]string, 0, len(headers)) for name := range headers { names = append(names, name) } slices.Sort(names) var canonHeaders strings.Builder for _, name := range names { canonHeaders.WriteString(name + ":" + headers[name] + "\n") } signed := strings.Join(names, ";") canonical := strings.Join([]string{ req.Method, canonicalPath(req.URL), canonicalQuery(req.URL.Query()), canonHeaders.String(), signed, payloadSHA256, }, "\n") scope := date + "/" + region + "/s3/aws4_request" toSign := "AWS4-HMAC-SHA256\n" + stamp + "\n" + scope + "\n" + hexSHA256([]byte(canonical)) key := hmacSHA256([]byte("AWS4"+secretKey), date) key = hmacSHA256(key, region) key = hmacSHA256(key, "s3") key = hmacSHA256(key, "aws4_request") signature := hex.EncodeToString(hmacSHA256(key, toSign)) req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+accessKey+"/"+scope+ ", SignedHeaders="+signed+", Signature="+signature) } // canonicalPath is the request path with each segment URI-encoded once. func canonicalPath(u *url.URL) string { p := u.EscapedPath() if p == "" { return "/" } if unescaped, err := url.PathUnescape(p); err == nil { p = unescaped } segments := strings.Split(p, "/") for i, s := range segments { segments[i] = uriEncode(s) } return strings.Join(segments, "/") } // canonicalQuery is the query sorted by name, names and values URI-encoded. func canonicalQuery(q url.Values) string { pairs := make([]string, 0, len(q)) for name, values := range q { for _, v := range values { pairs = append(pairs, uriEncode(name)+"="+uriEncode(v)) } } slices.Sort(pairs) return strings.Join(pairs, "&") } // uriEncode percent-encodes everything but the unreserved characters, as // SigV4 requires; url.QueryEscape differs on space and '~'. func uriEncode(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.' || c == '~' { b.WriteByte(c) continue } b.WriteString("%" + strings.ToUpper(hex.EncodeToString([]byte{c}))) } return b.String() } func hmacSHA256(key []byte, msg string) []byte { m := hmac.New(sha256.New, key) m.Write([]byte(msg)) return m.Sum(nil) } func hexSHA256(b []byte) string { sum := sha256.Sum256(b) return hex.EncodeToString(sum[:]) }