From 23f69f7f9cea295376523a91a739366bd36cef28 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Fri, 31 May 2024 08:53:39 +0930 Subject: [PATCH] x-pack/filebeat/input/http_endpoint: add support for base64-encoded HMAC headers (#39655) --- CHANGELOG.next.asciidoc | 2 +- .../docs/inputs/input-http-endpoint.asciidoc | 1 + .../input/http_endpoint/handler_test.go | 75 +++++++++++++++++++ .../filebeat/input/http_endpoint/validate.go | 22 +++++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 5b707bc1c5d..ef638899274 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -284,10 +284,10 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Implement Elastic Agent status and health reporting for CEL Filebeat input. {pull}39209[39209] - Add HTTP metrics to CEL input. {issue}39501[39501] {pull}39503[39503] - Add default user-agent to CEL HTTP requests. {issue}39502[39502] {pull}39587[39587] -- Improve reindexing support in security module pipelines. {issue}38224[38224] {pull}[] - Improve reindexing support in security module pipelines. {issue}38224[38224] {pull}39588[39588] - Make HTTP Endpoint input GA. {issue}38979[38979] {pull}39410[39410] - Update CEL mito extensions to v1.12.2. {pull}39755[39755] +- Add support for base64-encoded HMAC headers to HTTP Endpoint. {pull}39655[39655] *Auditbeat* diff --git a/x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc b/x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc index bc0ec78cdf8..6acceb3bd50 100644 --- a/x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc +++ b/x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc @@ -262,6 +262,7 @@ The secret stored in the header name specified by `secret.header`. Certain webho ==== `hmac.header` The name of the header that contains the HMAC signature: `X-Dropbox-Signature`, `X-Hub-Signature-256`, etc. +HMAC signatures may be encoded as hex or base64. [float] ==== `hmac.key` diff --git a/x-pack/filebeat/input/http_endpoint/handler_test.go b/x-pack/filebeat/input/http_endpoint/handler_test.go index 4c464a34f50..b41d20e72c2 100644 --- a/x-pack/filebeat/input/http_endpoint/handler_test.go +++ b/x-pack/filebeat/input/http_endpoint/handler_test.go @@ -259,6 +259,81 @@ func Test_apiResponse(t *testing.T) { wantStatus: http.StatusOK, wantResponse: `{"message": "success"}`, }, + { + name: "hmac_hex", + conf: func() config { + c := defaultConfig() + c.Prefix = "." + c.HMACHeader = "Test-HMAC" + c.HMACKey = "Test-HMAC-Key" + c.HMACType = "sha1" + c.HMACPrefix = "sha1:" + return c + }(), + request: func() *http.Request { + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Test-HMAC", "sha1:f6bf232bf1f0ca3d768f8b6bd5c26a204ba57e89") + return req + }(), + events: []mapstr.M{ + { + "id": int64(0), + }, + }, + wantStatus: http.StatusOK, + wantResponse: `{"message": "success"}`, + }, + { + name: "hmac_base64", + conf: func() config { + c := defaultConfig() + c.Prefix = "." + c.HMACHeader = "Test-HMAC" + c.HMACKey = "Test-HMAC-Key" + c.HMACType = "sha1" + c.HMACPrefix = "sha1:" + return c + }(), + request: func() *http.Request { + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Test-HMAC", "sha1:9r8jK/Hwyj12j4tr1cJqIEulfok=") + return req + }(), + events: []mapstr.M{ + { + "id": int64(0), + }, + }, + wantStatus: http.StatusOK, + wantResponse: `{"message": "success"}`, + }, + { + name: "hmac_raw_base64", + conf: func() config { + c := defaultConfig() + c.Prefix = "." + c.HMACHeader = "Test-HMAC" + c.HMACKey = "Test-HMAC-Key" + c.HMACType = "sha1" + c.HMACPrefix = "sha1:" + return c + }(), + request: func() *http.Request { + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Test-HMAC", "sha1:9r8jK/Hwyj12j4tr1cJqIEulfok") + return req + }(), + events: []mapstr.M{ + { + "id": int64(0), + }, + }, + wantStatus: http.StatusOK, + wantResponse: `{"message": "success"}`, + }, { name: "single_event_gzip", conf: defaultConfig(), diff --git a/x-pack/filebeat/input/http_endpoint/validate.go b/x-pack/filebeat/input/http_endpoint/validate.go index c44a3ccb536..950746d627c 100644 --- a/x-pack/filebeat/input/http_endpoint/validate.go +++ b/x-pack/filebeat/input/http_endpoint/validate.go @@ -9,6 +9,7 @@ import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -69,7 +70,7 @@ func (v *apiValidator) validateRequest(r *http.Request) (status int, err error) if v.hmacPrefix != "" { hmacHeaderValue = strings.TrimPrefix(hmacHeaderValue, v.hmacPrefix) } - signature, err := hex.DecodeString(hmacHeaderValue) + signature, err := decodeHeaderValue(hmacHeaderValue) if err != nil { return http.StatusUnauthorized, fmt.Errorf("invalid HMAC signature hex: %w", err) } @@ -104,3 +105,22 @@ func (v *apiValidator) validateRequest(r *http.Request) (status int, err error) return http.StatusAccepted, nil } + +// decoders is the priority-ordered set of decoders to use for HMAC header values. +var decoders = [...]func(string) ([]byte, error){ + hex.DecodeString, + base64.RawStdEncoding.DecodeString, + base64.StdEncoding.DecodeString, +} + +func decodeHeaderValue(s string) ([]byte, error) { + var errs []error + for _, d := range &decoders { + b, err := d(s) + if err == nil { + return b, nil + } + errs = append(errs, err) + } + return nil, errors.Join(errs...) +}