github.com/google/osv-scalibr@v0.4.1/veles/secrets/common/awssignerv4/awssignerv4.go (about) 1 // Copyright 2025 Google LLC 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 // Package awssignerv4 provides an implementation of AWS Signature Version 4 signing. 16 // It allows signing HTTP requests using AWS credentials 17 package awssignerv4 18 19 import ( 20 "bytes" 21 "crypto/hmac" 22 "crypto/sha256" 23 "encoding/hex" 24 "fmt" 25 "io" 26 "net/http" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/google/uuid" 32 ) 33 34 // Signer provides AWS Signature Version 4 signing for HTTP requests. 35 // 36 // ref: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html 37 type Signer struct { 38 Config 39 } 40 41 // Config used to create a Signer. 42 type Config struct { 43 Service string // AWS service name (e.g. "s3") 44 Region string // AWS region (e.g., "us-east-1") 45 Now func() time.Time 46 UUID func() string 47 SignedHeaders []string 48 } 49 50 // New creates a new Signer using the given config. 51 func New(cfg Config) *Signer { 52 s := &Signer{ 53 Config: cfg, 54 } 55 if s.Now == nil { 56 s.Now = time.Now().UTC 57 } 58 if s.UUID == nil { 59 s.UUID = func() string { return uuid.New().String() } 60 } 61 return s 62 } 63 64 // Sign applies AWS Signature Version 4 signing to an HTTP request. 65 // 66 // Example usage: 67 // 68 // s := signer.Signer{Service: "s3", Region: "us-east-1"} 69 // req, _ := http.NewRequest("GET", "https://my-bucket.s3.amazonaws.com/my-object", nil) 70 // err := s.Sign(req, "AKIAEXAMPLE", "secretkey123") 71 func (s *Signer) Sign(req *http.Request, accessID, secret string) error { 72 now := s.Now() 73 amzDate := now.Format("20060102T150405Z") 74 date := now.Format("20060102") 75 76 payload := "" 77 if req.Body != nil { 78 // read the body without disrupting it 79 body, err := io.ReadAll(req.Body) 80 if err != nil { 81 _ = req.Body.Close() 82 } 83 payload = string(body) 84 req.Body = io.NopCloser(bytes.NewReader(body)) 85 } 86 87 payloadHash := sha256Hex(payload) 88 89 // Mutate the request headers 90 req.Header.Set("Amz-Sdk-Invocation-Id", s.UUID()) 91 req.Header.Set("Amz-Sdk-Request", "attempt=1; max=3") 92 req.Header.Set("X-Amz-Content-Sha256", payloadHash) 93 req.Header.Set("X-Amz-Date", amzDate) 94 if len(payload) > 0 { 95 req.Header.Set("Content-Length", strconv.Itoa(len(payload))) 96 } 97 98 var canonicalHeadersB strings.Builder 99 for _, h := range s.SignedHeaders { 100 v := req.Header.Get(h) 101 if len(v) == 0 { 102 return fmt.Errorf("header %q not found", h) 103 } 104 canonicalHeadersB.WriteString(fmt.Sprintf("%s:%s\n", h, v)) 105 } 106 canonicalHeaders := canonicalHeadersB.String() 107 signedHeaders := strings.Join(s.SignedHeaders, ";") 108 109 canonicalRequest := strings.Join([]string{ 110 req.Method, req.URL.Path, 111 req.URL.RawQuery, canonicalHeaders, 112 signedHeaders, payloadHash, 113 }, "\n") 114 canonicalRequestHash := sha256Hex(canonicalRequest) 115 116 // String to sign 117 credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", date, s.Region, s.Service) 118 stringToSign := strings.Join([]string{ 119 "AWS4-HMAC-SHA256", amzDate, credentialScope, canonicalRequestHash, 120 }, "\n") 121 122 // Signature 123 signingKey := getSignatureKey(secret, date, s.Region, s.Service) 124 signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) 125 126 authorizationHeader := fmt.Sprintf( 127 "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", 128 accessID, credentialScope, signedHeaders, signature, 129 ) 130 req.Header.Set("Authorization", authorizationHeader) 131 return nil 132 } 133 134 func hmacSHA256(key []byte, data string) []byte { 135 h := hmac.New(sha256.New, key) 136 h.Write([]byte(data)) 137 return h.Sum(nil) 138 } 139 140 // SHA256 hash helper 141 func sha256Hex(data string) string { 142 hash := sha256.Sum256([]byte(data)) 143 return hex.EncodeToString(hash[:]) 144 } 145 146 // Derive signing key 147 // 148 // ref: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#signing-key 149 func getSignatureKey(secret, date, region, service string) []byte { 150 kDate := hmacSHA256([]byte("AWS4"+secret), date) 151 kRegion := hmacSHA256(kDate, region) 152 kService := hmacSHA256(kRegion, service) 153 kSigning := hmacSHA256(kService, "aws4_request") 154 return kSigning 155 }