@@ -16,17 +16,21 @@ package config
16
16
import (
17
17
"bytes"
18
18
"context"
19
+ "crypto/hmac"
19
20
"crypto/sha256"
20
21
"crypto/tls"
21
22
"crypto/x509"
23
+ "encoding/hex"
22
24
"encoding/json"
23
25
"errors"
24
26
"fmt"
27
+ "io"
25
28
"net"
26
29
"net/http"
27
30
"net/url"
28
31
"os"
29
32
"path/filepath"
33
+ "strconv"
30
34
"strings"
31
35
"sync"
32
36
"time"
@@ -302,6 +306,8 @@ type HTTPClientConfig struct {
302
306
BasicAuth * BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"`
303
307
// The HTTP authorization credentials for the targets.
304
308
Authorization * Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"`
309
+ // The HMAC signature configuration.
310
+ HMACSignature * HMACSignature `yaml:"hmac_signature,omitempty" json:"hmac_signature,omitempty"`
305
311
// The OAuth2 client credentials used to fetch a token for the targets.
306
312
OAuth2 * OAuth2 `yaml:"oauth2,omitempty" json:"oauth2,omitempty"`
307
313
// The bearer token for the targets. Deprecated in favour of
@@ -420,6 +426,11 @@ func (c *HTTPClientConfig) Validate() error {
420
426
return err
421
427
}
422
428
}
429
+ if c .HMACSignature != nil {
430
+ if err := c .HMACSignature .Validate (); err != nil {
431
+ return err
432
+ }
433
+ }
423
434
return nil
424
435
}
425
436
@@ -669,6 +680,14 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
669
680
rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
670
681
}
671
682
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
+
672
691
if cfg .HTTPHeaders != nil {
673
692
rt = NewHeadersRoundTripper (cfg .HTTPHeaders , rt )
674
693
}
@@ -702,6 +721,109 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
702
721
return NewTLSRoundTripperWithContext (ctx , tlsConfig , tlsSettings , newRT )
703
722
}
704
723
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
+
705
827
// SecretManager manages secret data mapped to names known as "references" or "refs".
706
828
type SecretManager interface {
707
829
// Fetch returns the secret data given a secret name indicated by `secretRef`.
0 commit comments