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  }