Skip to content

Commit ed9269b

Browse files
http_config: Add HMAC SHA256 request signing support
Signed-off-by: Alexander Akhmetov <[email protected]>
1 parent cc17dab commit ed9269b

6 files changed

+357
-0
lines changed

Diff for: config/http_config.go

+122
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ package config
1616
import (
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".
706828
type SecretManager interface {
707829
// Fetch returns the secret data given a secret name indicated by `secretRef`.

0 commit comments

Comments
 (0)