http_signer.go raw

   1  // Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates.  All rights reserved.
   2  // This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
   3  
   4  package common
   5  
   6  import (
   7  	"bytes"
   8  	"crypto"
   9  	"crypto/rand"
  10  	"crypto/rsa"
  11  	"crypto/sha256"
  12  	"encoding/base64"
  13  	"fmt"
  14  	"io"
  15  	"io/ioutil"
  16  	"net/http"
  17  	"strings"
  18  )
  19  
  20  // HTTPRequestSigner the interface to sign a request
  21  type HTTPRequestSigner interface {
  22  	Sign(r *http.Request) error
  23  }
  24  
  25  // KeyProvider interface that wraps information about the key's account owner
  26  type KeyProvider interface {
  27  	PrivateRSAKey() (*rsa.PrivateKey, error)
  28  	KeyID() (string, error)
  29  }
  30  
  31  const signerVersion = "1"
  32  
  33  // SignerBodyHashPredicate a function that allows to disable/enable body hashing
  34  // of requests and headers associated with body content
  35  type SignerBodyHashPredicate func(r *http.Request) bool
  36  
  37  // ociRequestSigner implements the http-signatures-draft spec
  38  // as described in https://tools.ietf.org/html/draft-cavage-http-signatures-08
  39  type ociRequestSigner struct {
  40  	KeyProvider    KeyProvider
  41  	GenericHeaders []string
  42  	BodyHeaders    []string
  43  	ShouldHashBody SignerBodyHashPredicate
  44  }
  45  
  46  var (
  47  	defaultGenericHeaders    = []string{"date", "(request-target)", "host"}
  48  	defaultBodyHeaders       = []string{"content-length", "content-type", "x-content-sha256"}
  49  	defaultBodyHashPredicate = func(r *http.Request) bool {
  50  		return r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch
  51  	}
  52  )
  53  
  54  // DefaultGenericHeaders list of default generic headers that is used in signing
  55  func DefaultGenericHeaders() []string {
  56  	return makeACopy(defaultGenericHeaders)
  57  }
  58  
  59  // DefaultBodyHeaders list of default body headers that is used in signing
  60  func DefaultBodyHeaders() []string {
  61  	return makeACopy(defaultBodyHeaders)
  62  }
  63  
  64  // DefaultRequestSigner creates a signer with default parameters.
  65  func DefaultRequestSigner(provider KeyProvider) HTTPRequestSigner {
  66  	return RequestSigner(provider, defaultGenericHeaders, defaultBodyHeaders)
  67  }
  68  
  69  // RequestSignerExcludeBody creates a signer without hash the body.
  70  func RequestSignerExcludeBody(provider KeyProvider) HTTPRequestSigner {
  71  	bodyHashPredicate := func(r *http.Request) bool {
  72  		// week request signer will not hash the body
  73  		return false
  74  	}
  75  	return RequestSignerWithBodyHashingPredicate(provider, defaultGenericHeaders, defaultBodyHeaders, bodyHashPredicate)
  76  }
  77  
  78  // NewSignerFromOCIRequestSigner creates a copy of the request signer and attaches the new SignerBodyHashPredicate
  79  // returns an error if the passed signer is not of type ociRequestSigner
  80  func NewSignerFromOCIRequestSigner(oldSigner HTTPRequestSigner, predicate SignerBodyHashPredicate) (HTTPRequestSigner, error) {
  81  	if oldS, ok := oldSigner.(ociRequestSigner); ok {
  82  		s := ociRequestSigner{
  83  			KeyProvider:    oldS.KeyProvider,
  84  			GenericHeaders: oldS.GenericHeaders,
  85  			BodyHeaders:    oldS.BodyHeaders,
  86  			ShouldHashBody: predicate,
  87  		}
  88  		return s, nil
  89  
  90  	}
  91  	return nil, fmt.Errorf("can not create a signer, input signer needs to be of type ociRequestSigner")
  92  }
  93  
  94  // RequestSigner creates a signer that utilizes the specified headers for signing
  95  // and the default predicate for using the body of the request as part of the signature
  96  func RequestSigner(provider KeyProvider, genericHeaders, bodyHeaders []string) HTTPRequestSigner {
  97  	return ociRequestSigner{
  98  		KeyProvider:    provider,
  99  		GenericHeaders: genericHeaders,
 100  		BodyHeaders:    bodyHeaders,
 101  		ShouldHashBody: defaultBodyHashPredicate}
 102  }
 103  
 104  // RequestSignerWithBodyHashingPredicate creates a signer that utilizes the specified headers for signing, as well as a predicate for using
 105  // the body of the request and bodyHeaders parameter as part of the signature
 106  func RequestSignerWithBodyHashingPredicate(provider KeyProvider, genericHeaders, bodyHeaders []string, shouldHashBody SignerBodyHashPredicate) HTTPRequestSigner {
 107  	return ociRequestSigner{
 108  		KeyProvider:    provider,
 109  		GenericHeaders: genericHeaders,
 110  		BodyHeaders:    bodyHeaders,
 111  		ShouldHashBody: shouldHashBody}
 112  }
 113  
 114  func (signer ociRequestSigner) getSigningHeaders(r *http.Request) []string {
 115  	var result []string
 116  	result = append(result, signer.GenericHeaders...)
 117  
 118  	if signer.ShouldHashBody(r) {
 119  		result = append(result, signer.BodyHeaders...)
 120  	}
 121  
 122  	return result
 123  }
 124  
 125  func (signer ociRequestSigner) getSigningString(request *http.Request) string {
 126  	signingHeaders := signer.getSigningHeaders(request)
 127  	signingParts := make([]string, len(signingHeaders))
 128  	for i, part := range signingHeaders {
 129  		var value string
 130  		part = strings.ToLower(part)
 131  		switch part {
 132  		case "(request-target)":
 133  			value = getRequestTarget(request)
 134  		case "host":
 135  			value = request.URL.Host
 136  			if len(value) == 0 {
 137  				value = request.Host
 138  			}
 139  		default:
 140  			value = request.Header.Get(part)
 141  		}
 142  		signingParts[i] = fmt.Sprintf("%s: %s", part, value)
 143  	}
 144  
 145  	signingString := strings.Join(signingParts, "\n")
 146  	return signingString
 147  
 148  }
 149  
 150  func getRequestTarget(request *http.Request) string {
 151  	lowercaseMethod := strings.ToLower(request.Method)
 152  	return fmt.Sprintf("%s %s", lowercaseMethod, request.URL.RequestURI())
 153  }
 154  
 155  func calculateHashOfBody(request *http.Request) (err error) {
 156  	var hash string
 157  	hash, err = GetBodyHash(request)
 158  	if err != nil {
 159  		return
 160  	}
 161  	request.Header.Set(requestHeaderXContentSHA256, hash)
 162  	return
 163  }
 164  
 165  // drainBody reads all of b to memory and then returns two equivalent
 166  // ReadClosers yielding the same bytes.
 167  //
 168  // It returns an error if the initial slurp of all bytes fails. It does not attempt
 169  // to make the returned ReadClosers have identical error-matching behavior.
 170  func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
 171  	if b == http.NoBody {
 172  		// No copying needed. Preserve the magic sentinel meaning of NoBody.
 173  		return http.NoBody, http.NoBody, nil
 174  	}
 175  	var buf bytes.Buffer
 176  	if _, err = buf.ReadFrom(b); err != nil {
 177  		return nil, b, err
 178  	}
 179  	if err = b.Close(); err != nil {
 180  		return nil, b, err
 181  	}
 182  	return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
 183  }
 184  
 185  func hashAndEncode(data []byte) string {
 186  	hashedContent := sha256.Sum256(data)
 187  	hash := base64.StdEncoding.EncodeToString(hashedContent[:])
 188  	return hash
 189  }
 190  
 191  // GetBodyHash creates a base64 string from the hash of body the request
 192  func GetBodyHash(request *http.Request) (hashString string, err error) {
 193  	if request.Body == nil {
 194  		request.ContentLength = 0
 195  		request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))
 196  		return hashAndEncode([]byte("")), nil
 197  	}
 198  
 199  	var data []byte
 200  	var bReader io.Reader
 201  	bReader, request.Body, err = drainBody(request.Body)
 202  	if err != nil {
 203  		return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
 204  	}
 205  
 206  	data, err = ioutil.ReadAll(bReader)
 207  	if err != nil {
 208  		return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
 209  	}
 210  
 211  	// Since the request can be coming from a binary body. Make an attempt to set the body length
 212  	request.ContentLength = int64(len(data))
 213  	request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))
 214  
 215  	hashString = hashAndEncode(data)
 216  	return
 217  }
 218  
 219  func (signer ociRequestSigner) computeSignature(request *http.Request) (signature string, err error) {
 220  	signingString := signer.getSigningString(request)
 221  	hasher := sha256.New()
 222  	hasher.Write([]byte(signingString))
 223  	hashed := hasher.Sum(nil)
 224  
 225  	privateKey, err := signer.KeyProvider.PrivateRSAKey()
 226  	if err != nil {
 227  		return
 228  	}
 229  
 230  	var unencodedSig []byte
 231  	unencodedSig, e := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
 232  	if e != nil {
 233  		err = fmt.Errorf("can not compute signature while signing the request %s: ", e.Error())
 234  		return
 235  	}
 236  
 237  	signature = base64.StdEncoding.EncodeToString(unencodedSig)
 238  	return
 239  }
 240  
 241  // Sign signs the http request, by inspecting the necessary headers. Once signed
 242  // the request will have the proper 'Authorization' header set, otherwise
 243  // and error is returned
 244  func (signer ociRequestSigner) Sign(request *http.Request) (err error) {
 245  	if signer.ShouldHashBody(request) {
 246  		err = calculateHashOfBody(request)
 247  		if err != nil {
 248  			return
 249  		}
 250  	}
 251  
 252  	var signature string
 253  	if signature, err = signer.computeSignature(request); err != nil {
 254  		return
 255  	}
 256  
 257  	signingHeaders := strings.Join(signer.getSigningHeaders(request), " ")
 258  
 259  	var keyID string
 260  	if keyID, err = signer.KeyProvider.KeyID(); err != nil {
 261  		return
 262  	}
 263  
 264  	authValue := fmt.Sprintf("Signature version=\"%s\",headers=\"%s\",keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\"",
 265  		signerVersion, signingHeaders, keyID, signature)
 266  
 267  	request.Header.Set(requestHeaderAuthorization, authValue)
 268  
 269  	return
 270  }
 271