Skip to content

http_config: Add HMAC SHA256 request signing support #758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions config/http_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ package config
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -302,6 +306,8 @@ type HTTPClientConfig struct {
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"`
// The HTTP authorization credentials for the targets.
Authorization *Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"`
// The HMAC signature configuration.
HMACSignature *HMACSignature `yaml:"hmac_signature,omitempty" json:"hmac_signature,omitempty"`
// The OAuth2 client credentials used to fetch a token for the targets.
OAuth2 *OAuth2 `yaml:"oauth2,omitempty" json:"oauth2,omitempty"`
// The bearer token for the targets. Deprecated in favour of
Expand Down Expand Up @@ -420,6 +426,11 @@ func (c *HTTPClientConfig) Validate() error {
return err
}
}
if c.HMACSignature != nil {
if err := c.HMACSignature.Validate(); err != nil {
return err
}
}
return nil
}

Expand Down Expand Up @@ -669,6 +680,14 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
rt = NewOAuth2RoundTripper(clientSecret, cfg.OAuth2, rt, &opts)
}

if cfg.HMACSignature != nil {
secret, err := toSecret(opts.secretManager, cfg.HMACSignature.Secret, cfg.HMACSignature.SecretFile, cfg.HMACSignature.SecretRef)
if err != nil {
return nil, fmt.Errorf("unable to use HMAC secret: %w", err)
}
rt = NewHMACSignatureRoundTripper(secret, cfg.HMACSignature.Header, cfg.HMACSignature.TimestampHeader, rt)
}

if cfg.HTTPHeaders != nil {
rt = NewHeadersRoundTripper(cfg.HTTPHeaders, rt)
}
Expand Down Expand Up @@ -702,6 +721,109 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
return NewTLSRoundTripperWithContext(ctx, tlsConfig, tlsSettings, newRT)
}

// HMACSignature contains configuration for HMAC SHA256 signing.
//
// The HMAC signature is calculated over the request body and added to the
// request headers.
//
// If the timestamp header is set, the timestamp is included in the HMAC
// by concatenating the timestamp header value with the request body using
// a colon character as separator.
type HMACSignature struct {
// The secret key used for HMAC signing.
Secret Secret `yaml:"secret,omitempty" json:"secret,omitempty"`
// The secret key file for HMAC signing.
SecretFile string `yaml:"secret_file,omitempty" json:"secret_file,omitempty"`
// SecretRef is the name of the secret within the secret manager to use as the HMAC key
SecretRef string `yaml:"secret_ref,omitempty" json:"secret_ref,omitempty"`
// Header is the name of the header containing the HMAC signature
Header string `yaml:"header,omitempty" json:"header,omitempty"`
// TimestampHeader is the name of the header containing the timestamp
// used to generate the HMAC signature. If empty, time is not included.
TimestampHeader string `yaml:"timestamp_header,omitempty" json:"timestamp_header,omitempty"`
}

// SetDirectory joins any relative file paths with dir.
func (h *HMACSignature) SetDirectory(dir string) {
if h == nil {
return
}
h.SecretFile = JoinDir(dir, h.SecretFile)
}

// Validate checks that the HMAC signature config is valid.
func (h *HMACSignature) Validate() error {
if h == nil {
return nil
}
if nonZeroCount(len(h.Secret) > 0, len(h.SecretFile) > 0, len(h.SecretRef) > 0) > 1 {
return errors.New("at most one of secret, secret_file & secret_ref must be configured")
}
if h.Header == "" {
h.Header = "X-HMAC-SHA256"
}
return nil
}

// hmacRoundTripper adds HMAC signatures to HTTP requests.
type hmacRoundTripper struct {
secret SecretReader
header string
timestampHeader string
rt http.RoundTripper
}

// NewHMACSignatureRoundTripper creates a new round tripper that creates HMAC SHA256
// signature and adds it to a header in the request.
func NewHMACSignatureRoundTripper(secret SecretReader, header, timestampHeader string, rt http.RoundTripper) http.RoundTripper {
return &hmacRoundTripper{secret: secret, header: header, timestampHeader: timestampHeader, rt: rt}
}

func (rt *hmacRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.secret == nil {
return rt.rt.RoundTrip(req)
}

secret, err := rt.secret.Fetch(req.Context())
if err != nil {
return nil, fmt.Errorf("unable to read HMAC secret: %w", err)
}

var body []byte
if req.Body != nil {
body, err = io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("error reading request body: %w", err)
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
}
req = cloneRequest(req)

mac := hmac.New(sha256.New, []byte(secret))

// If the timestamp header is set, include the timestamp in the HMAC
// using colon as separator between the timestamp and the request body.
if rt.timestampHeader != "" {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
req.Header.Set(rt.timestampHeader, timestamp)
mac.Write([]byte(timestamp))
mac.Write([]byte(":"))
}

mac.Write([]byte(body))
signature := hex.EncodeToString(mac.Sum(nil))

req.Header.Set(rt.header, signature)

return rt.rt.RoundTrip(req)
}

func (rt *hmacRoundTripper) CloseIdleConnections() {
if ci, ok := rt.rt.(closeIdler); ok {
ci.CloseIdleConnections()
}
}

// SecretManager manages secret data mapped to names known as "references" or "refs".
type SecretManager interface {
// Fetch returns the secret data given a secret name indicated by `secretRef`.
Expand Down
Loading
Loading