@@ -16,17 +16,21 @@ package config
1616import (
1717 "bytes"
1818 "context"
19+ "crypto/hmac"
1920 "crypto/sha256"
2021 "crypto/tls"
2122 "crypto/x509"
23+ "encoding/hex"
2224 "encoding/json"
2325 "errors"
2426 "fmt"
27+ "io"
2528 "net"
2629 "net/http"
2730 "net/url"
2831 "os"
2932 "path/filepath"
33+ "strconv"
3034 "strings"
3135 "sync"
3236 "time"
@@ -302,6 +306,8 @@ type HTTPClientConfig struct {
302306 BasicAuth * BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"`
303307 // The HTTP authorization credentials for the targets.
304308 Authorization * Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"`
309+ // The HMAC signature configuration.
310+ HMACSignature * HMACSignature `yaml:"hmac_signature,omitempty" json:"hmac_signature,omitempty"`
305311 // The OAuth2 client credentials used to fetch a token for the targets.
306312 OAuth2 * OAuth2 `yaml:"oauth2,omitempty" json:"oauth2,omitempty"`
307313 // The bearer token for the targets. Deprecated in favour of
@@ -420,6 +426,11 @@ func (c *HTTPClientConfig) Validate() error {
420426 return err
421427 }
422428 }
429+ if c .HMACSignature != nil {
430+ if err := c .HMACSignature .Validate (); err != nil {
431+ return err
432+ }
433+ }
423434 return nil
424435}
425436
@@ -669,6 +680,14 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
669680 rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
670681 }
671682
683+ if cfg .HMACSignature != nil {
684+ secret , err := toSecret (opts .secretManager , cfg .HMACSignature .Secret , cfg .HMACSignature .SecretFile , cfg .HMACSignature .SecretRef )
685+ if err != nil {
686+ return nil , fmt .Errorf ("unable to use HMAC secret: %w" , err )
687+ }
688+ rt = NewHMACSignatureRoundTripper (secret , cfg .HMACSignature .Header , cfg .HMACSignature .TimestampHeader , rt )
689+ }
690+
672691 if cfg .HTTPHeaders != nil {
673692 rt = NewHeadersRoundTripper (cfg .HTTPHeaders , rt )
674693 }
@@ -702,6 +721,109 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
702721 return NewTLSRoundTripperWithContext (ctx , tlsConfig , tlsSettings , newRT )
703722}
704723
724+ // HMACSignature contains configuration for HMAC SHA256 signing.
725+ //
726+ // The HMAC signature is calculated over the request body and added to the
727+ // request headers.
728+ //
729+ // If the timestamp header is set, the timestamp is included in the HMAC
730+ // by concatenating the timestamp header value with the request body using
731+ // a colon character as separator.
732+ type HMACSignature struct {
733+ // The secret key used for HMAC signing.
734+ Secret Secret `yaml:"secret,omitempty" json:"secret,omitempty"`
735+ // The secret key file for HMAC signing.
736+ SecretFile string `yaml:"secret_file,omitempty" json:"secret_file,omitempty"`
737+ // SecretRef is the name of the secret within the secret manager to use as the HMAC key
738+ SecretRef string `yaml:"secret_ref,omitempty" json:"secret_ref,omitempty"`
739+ // Header is the name of the header containing the HMAC signature
740+ Header string `yaml:"header,omitempty" json:"header,omitempty"`
741+ // TimestampHeader is the name of the header containing the timestamp
742+ // used to generate the HMAC signature. If empty, time is not included.
743+ TimestampHeader string `yaml:"timestamp_header,omitempty" json:"timestamp_header,omitempty"`
744+ }
745+
746+ // SetDirectory joins any relative file paths with dir.
747+ func (h * HMACSignature ) SetDirectory (dir string ) {
748+ if h == nil {
749+ return
750+ }
751+ h .SecretFile = JoinDir (dir , h .SecretFile )
752+ }
753+
754+ // Validate checks that the HMAC signature config is valid.
755+ func (h * HMACSignature ) Validate () error {
756+ if h == nil {
757+ return nil
758+ }
759+ if nonZeroCount (len (h .Secret ) > 0 , len (h .SecretFile ) > 0 , len (h .SecretRef ) > 0 ) > 1 {
760+ return errors .New ("at most one of secret, secret_file & secret_ref must be configured" )
761+ }
762+ if h .Header == "" {
763+ h .Header = "X-HMAC-SHA256"
764+ }
765+ return nil
766+ }
767+
768+ // hmacRoundTripper adds HMAC signatures to HTTP requests.
769+ type hmacRoundTripper struct {
770+ secret SecretReader
771+ header string
772+ timestampHeader string
773+ rt http.RoundTripper
774+ }
775+
776+ // NewHMACSignatureRoundTripper creates a new round tripper that creates HMAC SHA256
777+ // signature and adds it to a header in the request.
778+ func NewHMACSignatureRoundTripper (secret SecretReader , header , timestampHeader string , rt http.RoundTripper ) http.RoundTripper {
779+ return & hmacRoundTripper {secret : secret , header : header , timestampHeader : timestampHeader , rt : rt }
780+ }
781+
782+ func (rt * hmacRoundTripper ) RoundTrip (req * http.Request ) (* http.Response , error ) {
783+ if rt .secret == nil {
784+ return rt .rt .RoundTrip (req )
785+ }
786+
787+ secret , err := rt .secret .Fetch (req .Context ())
788+ if err != nil {
789+ return nil , fmt .Errorf ("unable to read HMAC secret: %w" , err )
790+ }
791+
792+ var body []byte
793+ if req .Body != nil {
794+ body , err = io .ReadAll (req .Body )
795+ if err != nil {
796+ return nil , fmt .Errorf ("error reading request body: %w" , err )
797+ }
798+ req .Body = io .NopCloser (bytes .NewBuffer (body ))
799+ }
800+ req = cloneRequest (req )
801+
802+ mac := hmac .New (sha256 .New , []byte (secret ))
803+
804+ // If the timestamp header is set, include the timestamp in the HMAC
805+ // using colon as separator between the timestamp and the request body.
806+ if rt .timestampHeader != "" {
807+ timestamp := strconv .FormatInt (time .Now ().Unix (), 10 )
808+ req .Header .Set (rt .timestampHeader , timestamp )
809+ mac .Write ([]byte (timestamp ))
810+ mac .Write ([]byte (":" ))
811+ }
812+
813+ mac .Write ([]byte (body ))
814+ signature := hex .EncodeToString (mac .Sum (nil ))
815+
816+ req .Header .Set (rt .header , signature )
817+
818+ return rt .rt .RoundTrip (req )
819+ }
820+
821+ func (rt * hmacRoundTripper ) CloseIdleConnections () {
822+ if ci , ok := rt .rt .(closeIdler ); ok {
823+ ci .CloseIdleConnections ()
824+ }
825+ }
826+
705827// SecretManager manages secret data mapped to names known as "references" or "refs".
706828type SecretManager interface {
707829 // Fetch returns the secret data given a secret name indicated by `secretRef`.
0 commit comments