github.com/Ingenico-ePayments/connect-sdk-go@v0.0.0-20240318153750-1f8cd329b9c9/webhooks/validation/SignatureValidator.go (about)

     1  package validation
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha256"
     6  	"crypto/subtle"
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/Ingenico-ePayments/connect-sdk-go/communicator/communication"
    13  )
    14  
    15  var (
    16  	// ErrNilSecretKeyStore occurs when the provided secretKeyStore is nil
    17  	ErrNilSecretKeyStore = errors.New("nil secretKeyStore")
    18  
    19  	multipleHeaderFormat = `encountered multiple occurrences of header "%v"`
    20  	headerNotFoundFormat = `could not find header "%v"`
    21  	compareFailedFormat  = `failed to validate signature "%v"`
    22  )
    23  
    24  // SignatureValidator is a validator for webhooks signatures. Immutable and thread-safe.
    25  type SignatureValidator struct {
    26  	secretKeyStore SecretKeyStore
    27  }
    28  
    29  // SecretKeyStore returns the configured secret key store
    30  func (v *SignatureValidator) SecretKeyStore() SecretKeyStore {
    31  	return v.secretKeyStore
    32  }
    33  
    34  // Validate validates the given body using the given request headers
    35  func (v *SignatureValidator) Validate(body string, requestHeaders []communication.Header) error {
    36  	signature, err := getHeaderValue(requestHeaders, "X-GCS-Signature")
    37  	if err != nil {
    38  		return err
    39  	}
    40  
    41  	keyID, err := getHeaderValue(requestHeaders, "X-GCS-KeyId")
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	secretKey, err := v.secretKeyStore.GetSecretKey(keyID)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	mac := hmac.New(sha256.New, []byte(secretKey))
    52  
    53  	_, err = mac.Write([]byte(body))
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	unencodedResult := mac.Sum(nil)
    59  
    60  	encoder := base64.StdEncoding
    61  	expectedSignature := make([]byte, encoder.EncodedLen(len(unencodedResult)))
    62  	encoder.Encode(expectedSignature, unencodedResult)
    63  
    64  	isValid := subtle.ConstantTimeCompare([]byte(signature), expectedSignature) == 1
    65  	if !isValid {
    66  		return NewSignatureValidationError(fmt.Sprintf(compareFailedFormat, signature))
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  // NewSignatureValidator creates a signature validator with the given secretKeyStore
    73  func NewSignatureValidator(secretKeyStore SecretKeyStore) (*SignatureValidator, error) {
    74  	if secretKeyStore == nil {
    75  		return nil, ErrNilSecretKeyStore
    76  	}
    77  
    78  	return &SignatureValidator{secretKeyStore}, nil
    79  }
    80  
    81  func getHeaderValue(headers []communication.Header, name string) (string, error) {
    82  	lowerName := strings.ToLower(name)
    83  	var value string
    84  
    85  	found := false
    86  
    87  	for _, header := range headers {
    88  		if strings.ToLower(header.Name()) == lowerName {
    89  			if found {
    90  				sve := NewSignatureValidationError(fmt.Sprintf(multipleHeaderFormat, name))
    91  
    92  				return "", sve
    93  			}
    94  
    95  			found, value = true, header.Value()
    96  		}
    97  	}
    98  
    99  	if !found {
   100  		sve := NewSignatureValidationError(fmt.Sprintf(headerNotFoundFormat, name))
   101  
   102  		return "", sve
   103  	}
   104  
   105  	return value, nil
   106  }