github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/sig/javav2.go (about)

     1  package sig
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"net/http"
     8  	"net/url"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/treeverse/lakefs/pkg/auth/model"
    13  	"github.com/treeverse/lakefs/pkg/gateway/errors"
    14  	"github.com/treeverse/lakefs/pkg/logging"
    15  )
    16  
    17  // Implements the "signing protocol" as exists at aws-sdk-java @ 1.12.390 (commit 07926f08a70).
    18  // This should never have worked, on any version of S3, but the implementation is there, unmodified since 2015.
    19  // The code is below, for reference
    20  /*
    21  
    22  private String calculateStringToSignV2(SignableRequest<?> request) throws SdkClientException {
    23     URI endpoint = request.getEndpoint();
    24  
    25     StringBuilder data = new StringBuilder();
    26     data.append("POST")
    27  	   .append("\n")
    28  	   .append(getCanonicalizedEndpoint(endpoint)) // <----- bare host, lower-cased
    29  	   .append("\n")
    30  	   .append(getCanonicalizedResourcePath(request)) // <----- path relative to the endpoint
    31  	   .append("\n")
    32  	   .append(getCanonicalizedQueryString(request.getParameters()));  // <----- ordered query string parameters
    33     return data.toString();
    34  }
    35  
    36  */
    37  
    38  func canonicalJavaV2String(host string, query url.Values, path string) string {
    39  	cs := strings.ToUpper(http.MethodPost) // so weird.
    40  	cs += "\n"
    41  	cs += host
    42  	cs += "\n"
    43  	cs += path
    44  	cs += "\n"
    45  	cs += canonicalJavaV2Query(query)
    46  	return cs
    47  }
    48  
    49  func canonicalJavaV2Query(q url.Values) string {
    50  	escaped := make([][2]string, 0)
    51  	for k, vs := range q {
    52  		if strings.EqualFold(k, "signature") {
    53  			continue
    54  		}
    55  		escapedKey := url.QueryEscape(k)
    56  		for _, v := range vs {
    57  			pair := [2]string{escapedKey, url.QueryEscape(v)}
    58  			escaped = append(escaped, pair)
    59  		}
    60  	}
    61  	// sort
    62  	sort.Slice(escaped, func(i, j int) bool {
    63  		return escaped[i][0] < escaped[j][0]
    64  	})
    65  	// output
    66  	out := ""
    67  	for i, pair := range escaped {
    68  		out += pair[0] + "=" + pair[1]
    69  		isLast := i == len(escaped)-1
    70  		if !isLast {
    71  			out += "&"
    72  		}
    73  	}
    74  	return out
    75  }
    76  
    77  type JavaV2Signer struct {
    78  	bareDomain string
    79  	req        *http.Request
    80  	sigCtx     *JavaV2SignerContext
    81  }
    82  
    83  type JavaV2SignerContext struct {
    84  	awsAccessKeyID string
    85  	signature      []byte
    86  }
    87  
    88  func NewJavaV2SigAuthenticator(r *http.Request, bareDomain string) *JavaV2Signer {
    89  	return &JavaV2Signer{
    90  		req:        r,
    91  		bareDomain: bareDomain,
    92  	}
    93  }
    94  
    95  func (j *JavaV2SignerContext) GetAccessKeyID() string {
    96  	return j.awsAccessKeyID
    97  }
    98  
    99  func (j *JavaV2Signer) Parse() (SigContext, error) {
   100  	ctx := j.req.Context()
   101  	awsAccessKeyID := j.req.URL.Query().Get("AWSAccessKeyId")
   102  	if awsAccessKeyID == "" {
   103  		return nil, ErrHeaderMalformed
   104  	}
   105  	signature := j.req.URL.Query().Get("Signature")
   106  	if signature == "" {
   107  		return nil, ErrHeaderMalformed
   108  	}
   109  	sig, err := base64.StdEncoding.DecodeString(signature)
   110  	if err != nil {
   111  		logging.FromContext(ctx).Error("log header does not match v2 structure (isn't proper base64)")
   112  		return nil, ErrHeaderMalformed
   113  	}
   114  	sigMethod := j.req.URL.Query().Get("SignatureMethod")
   115  	if sigMethod != "HmacSHA256" {
   116  		return nil, ErrHeaderMalformed
   117  	}
   118  	sigVersion := j.req.URL.Query().Get("SignatureVersion")
   119  	if sigVersion != "2" {
   120  		return nil, ErrHeaderMalformed
   121  	}
   122  	sigCtx := &JavaV2SignerContext{
   123  		awsAccessKeyID: awsAccessKeyID,
   124  		signature:      sig,
   125  	}
   126  	j.sigCtx = sigCtx
   127  	return sigCtx, nil
   128  }
   129  
   130  func signCanonicalJavaV2String(msg string, signature []byte) []byte {
   131  	h := hmac.New(sha256.New, signature)
   132  	_, _ = h.Write([]byte(msg))
   133  	return h.Sum(nil)
   134  }
   135  
   136  func (j *JavaV2Signer) Verify(creds *model.Credential) error {
   137  	rawPath := j.req.URL.EscapedPath()
   138  
   139  	path := buildPath(j.req.Host, j.bareDomain, rawPath)
   140  	stringToSign := canonicalJavaV2String(j.req.Host, j.req.URL.Query(), path)
   141  	digest := signCanonicalJavaV2String(stringToSign, []byte(creds.SecretAccessKey))
   142  	if !Equal(digest, j.sigCtx.signature) {
   143  		return errors.ErrSignatureDoesNotMatch
   144  	}
   145  	return nil
   146  }