diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go new file mode 100644 index 000000000..60883c73c --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go @@ -0,0 +1,178 @@ +package config + +import ( + "encoding/json" + "errors" + + "github.com/higress-group/wasm-go/pkg/log" + "github.com/tidwall/gjson" +) + +var ( + // RuleSet 插件是否至少在一个 domain 或 route 上生效 + RuleSet bool + // allowed_algorithms 配置中允许的算法 + validAlgorithms = map[string]bool{ + "hmac-sha1": true, + "hmac-sha256": true, + "hmac-sha512": true, + } +) + +type HmacAuthConfig struct { + Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"` + GlobalAuth *bool `json:"global_auth,omitempty" yaml:"global_auth,omitempty"` + AllowedAlgorithms []string `json:"allowed_algorithms,omitempty" yaml:"allowed_algorithms,omitempty"` + ClockSkew int `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` + SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` + ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` + HideCredentials bool `json:"hide_credentials,omitempty" yaml:"hide_credentials,omitempty"` + AnonymousConsumer string `json:"anonymous_consumer,omitempty" yaml:"anonymous_consumer,omitempty"` + Allow []string `json:"allow" yaml:"allow"` +} + +type Consumer struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + AccessKey string `json:"access_key" yaml:"access_key"` + SecretKey string `json:"secret_key" yaml:"secret_key"` +} + +func ParseGlobalConfig(jsonData gjson.Result, global *HmacAuthConfig) error { + log.Debug("global config") + RuleSet = false + + // 处理 consumers 配置 + consumers := jsonData.Get("consumers") + if !consumers.Exists() { + return errors.New("consumers is required") + } + if len(consumers.Array()) == 0 { + return errors.New("consumers cannot be empty") + } + + accessKeyMap := make(map[string]string) + for _, item := range consumers.Array() { + ak := item.Get("access_key") + if !ak.Exists() || ak.String() == "" { + return errors.New("consumer access_key is required") + } + sk := item.Get("secret_key") + if !sk.Exists() || sk.String() == "" { + return errors.New("consumer secret_key is required") + } + if _, ok := accessKeyMap[ak.String()]; ok { + return errors.New("duplicate consumer access_key: " + ak.String()) + } + + consumer := Consumer{ + AccessKey: ak.String(), + SecretKey: sk.String(), + } + + name := item.Get("name") + if name.Exists() && name.String() != "" { + consumer.Name = name.String() + } else { + // 如果没有提供 name,则使用 access_key 作为 name + consumer.Name = ak.String() + } + + global.Consumers = append(global.Consumers, consumer) + accessKeyMap[ak.String()] = ak.String() + } + + // 处理 global_auth 配置 + globalAuth := jsonData.Get("global_auth") + if globalAuth.Exists() { + ga := globalAuth.Bool() + global.GlobalAuth = &ga + } + + // 处理 allowed_algorithms 配置 + allowedAlgorithms := jsonData.Get("allowed_algorithms") + if allowedAlgorithms.Exists() && len(allowedAlgorithms.Array()) > 0 { + global.AllowedAlgorithms = []string{} + for _, item := range allowedAlgorithms.Array() { + algorithm := item.String() + if !validAlgorithms[algorithm] { + return errors.New("invalid allowed_algorithm: " + algorithm + ". Must be one of: hmac-sha1, hmac-sha256, hmac-sha512") + } + global.AllowedAlgorithms = append(global.AllowedAlgorithms, algorithm) + } + } else { + // 如果未设置,则使用默认值 + global.AllowedAlgorithms = []string{"hmac-sha1", "hmac-sha256", "hmac-sha512"} + } + + // 处理 clock_skew 配置 + clockSkew := jsonData.Get("clock_skew") + if !clockSkew.Exists() { + // 如果未设置,则使用默认值300 + global.ClockSkew = 300 + } else if clockSkew.Int() >= 1 { + global.ClockSkew = int(clockSkew.Int()) + } + + // 处理 signed_headers 配置 + signedHeaders := jsonData.Get("signed_headers") + if signedHeaders.Exists() { + global.SignedHeaders = []string{} + for _, item := range signedHeaders.Array() { + global.SignedHeaders = append(global.SignedHeaders, item.String()) + } + } + + // 处理 validate_request_body 配置 + validateRequestBody := jsonData.Get("validate_request_body") + if validateRequestBody.Exists() { + global.ValidateRequestBody = validateRequestBody.Bool() + } + + // 处理 hide_credentials 配置 + hideCredentials := jsonData.Get("hide_credentials") + if hideCredentials.Exists() { + global.HideCredentials = hideCredentials.Bool() + } + + // 处理 anonymous_consumer 配置 + anonymousConsumer := jsonData.Get("anonymous_consumer") + if anonymousConsumer.Exists() { + global.AnonymousConsumer = anonymousConsumer.String() + } + + if globalBytes, err := json.Marshal(global); err == nil { + log.Debugf("global: %s", string(globalBytes)) + } + return nil +} + +func ParseOverrideRuleConfig(jsonData gjson.Result, global HmacAuthConfig, config *HmacAuthConfig) error { + log.Debug("domain/route config") + *config = global + + // 处理 allow 配置 + allow := jsonData.Get("allow") + if allow.Exists() { + config.Allow = []string{} + consumerNames := make(map[string]bool) + for _, consumer := range config.Consumers { + consumerNames[consumer.Name] = true + } + + for _, item := range allow.Array() { + allowedName := item.String() + config.Allow = append(config.Allow, allowedName) + + // 检查允许的名称是否在消费者列表中 + if !consumerNames[allowedName] { + log.Warnf("allowed consumer name '%s' is not in the consumers list", allowedName) + } + } + } + + RuleSet = true + if configBytes, err := json.Marshal(config); err == nil { + log.Debugf("config: %s", string(configBytes)) + } + return nil +} diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/config/config_test.go b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config_test.go new file mode 100644 index 000000000..aa72f77de --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config_test.go @@ -0,0 +1,388 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +func TestParseGlobalConfig(t *testing.T) { + tests := []struct { + name string + jsonConfig string + expectError bool + validate func(*testing.T, *HmacAuthConfig) + }{ + { + name: "Valid config with named consumers", + jsonConfig: `{ + "consumers": [ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1" + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2" + } + ] + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, 2, len(config.Consumers)) + assert.Equal(t, "consumer1", config.Consumers[0].Name) + assert.Equal(t, "ak1", config.Consumers[0].AccessKey) + assert.Equal(t, "sk1", config.Consumers[0].SecretKey) + assert.Equal(t, "consumer2", config.Consumers[1].Name) + assert.Equal(t, "ak2", config.Consumers[1].AccessKey) + assert.Equal(t, "sk2", config.Consumers[1].SecretKey) + + // 默认值检查 + assert.Equal(t, []string{"hmac-sha1", "hmac-sha256", "hmac-sha512"}, config.AllowedAlgorithms) + assert.Equal(t, 300, config.ClockSkew) + }, + }, + { + name: "Valid config without names (use access_key as name)", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + }, + { + "access_key": "ak2", + "secret_key": "sk2" + } + ] + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, 2, len(config.Consumers)) + assert.Equal(t, "ak1", config.Consumers[0].Name) + assert.Equal(t, "ak1", config.Consumers[0].AccessKey) + assert.Equal(t, "ak2", config.Consumers[1].Name) + assert.Equal(t, "ak2", config.Consumers[1].AccessKey) + }, + }, + { + name: "Missing consumers", + jsonConfig: `{ + "other_field": "value" + }`, + expectError: true, + }, + { + name: "Empty consumers array", + jsonConfig: `{ + "consumers": [] + }`, + expectError: true, + }, + { + name: "Missing access_key", + jsonConfig: `{ + "consumers": [ + { + "secret_key": "sk1" + } + ] + }`, + expectError: true, + }, + { + name: "Missing secret_key", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1" + } + ] + }`, + expectError: true, + }, + { + name: "Duplicate access_key", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + }, + { + "access_key": "ak1", + "secret_key": "sk2" + } + ] + }`, + expectError: true, + }, + { + name: "Valid global_auth", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "global_auth": true + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.NotNil(t, config.GlobalAuth) + assert.True(t, *config.GlobalAuth) + }, + }, + { + name: "Valid allowed_algorithms", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "allowed_algorithms": ["hmac-sha256"] + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{"hmac-sha256"}, config.AllowedAlgorithms) + }, + }, + { + name: "Invalid allowed_algorithms", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "allowed_algorithms": ["invalid-algorithm"] + }`, + expectError: true, + }, + { + name: "Valid clock_skew", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "clock_skew": 600 + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, 600, config.ClockSkew) + }, + }, + { + name: "Valid signed_headers", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "signed_headers": ["host", "date"] + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{"host", "date"}, config.SignedHeaders) + }, + }, + { + name: "Valid validate_request_body", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "validate_request_body": true + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.True(t, config.ValidateRequestBody) + }, + }, + { + name: "Valid hide_credentials", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "hide_credentials": true + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.True(t, config.HideCredentials) + }, + }, + { + name: "Valid anonymous_consumer", + jsonConfig: `{ + "consumers": [ + { + "access_key": "ak1", + "secret_key": "sk1" + } + ], + "anonymous_consumer": "anonymous" + }`, + expectError: false, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, "anonymous", config.AnonymousConsumer) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonData := gjson.Parse(tt.jsonConfig) + config := &HmacAuthConfig{} + + defer func() { + if r := recover(); r != nil { + // 忽略日志相关的 panic + } + }() + + err := ParseGlobalConfig(jsonData, config) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, config) + } + } + }) + } +} + +func TestParseOverrideRuleConfig(t *testing.T) { + globalConfig := HmacAuthConfig{ + Consumers: []Consumer{ + { + Name: "consumer1", + AccessKey: "ak1", + SecretKey: "sk1", + }, + { + Name: "consumer2", + AccessKey: "ak2", + SecretKey: "sk2", + }, + }, + AllowedAlgorithms: []string{"hmac-sha1", "hmac-sha256", "hmac-sha512"}, + ClockSkew: 300, + } + + tests := []struct { + name string + jsonConfig string + validate func(*testing.T, *HmacAuthConfig) + expectError bool + }{ + { + name: "Default values when no overrides", + jsonConfig: `{ + "some_other_field": "value" + }`, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{"hmac-sha1", "hmac-sha256", "hmac-sha512"}, config.AllowedAlgorithms) + assert.Equal(t, 300, config.ClockSkew) + assert.Equal(t, 2, len(config.Consumers)) + }, + expectError: false, + }, + { + name: "Valid allow list", + jsonConfig: `{ + "allow": ["consumer1", "consumer2"] + }`, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{"consumer1", "consumer2"}, config.Allow) + }, + expectError: false, + }, + { + name: "Allow list with non-existent consumer", + jsonConfig: `{ + "allow": ["consumer1", "nonexistent"] + }`, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{"consumer1", "nonexistent"}, config.Allow) + }, + expectError: false, + }, + { + name: "Empty allow list", + jsonConfig: `{ + "allow": [] + }`, + validate: func(t *testing.T, config *HmacAuthConfig) { + assert.Equal(t, []string{}, config.Allow) + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonData := gjson.Parse(tt.jsonConfig) + config := &HmacAuthConfig{} + + defer func() { + if r := recover(); r != nil { + // 忽略日志相关的 panic + } + }() + + err := ParseOverrideRuleConfig(jsonData, globalConfig, config) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, config) + } + } + }) + } +} + +func TestValidAlgorithms(t *testing.T) { + tests := []struct { + algorithm string + valid bool + }{ + {"hmac-sha1", true}, + {"hmac-sha256", true}, + {"hmac-sha512", true}, + {"invalid-algorithm", false}, + {"", false}, + {"hmac-md5", false}, + } + + for _, tt := range tests { + t.Run(tt.algorithm, func(t *testing.T) { + _, exists := validAlgorithms[tt.algorithm] + assert.Equal(t, tt.valid, exists) + }) + } +} diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod b/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod new file mode 100644 index 000000000..9762d4870 --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod @@ -0,0 +1,22 @@ +module hmac-auth-apisix + +go 1.24.1 + +toolchain go1.24.4 + +require ( + github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 + github.com/higress-group/wasm-go v1.0.1 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/gjson v1.18.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/resp v0.1.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum b/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum new file mode 100644 index 000000000..35e747e20 --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA= +github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY= +github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= +github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/main.go b/plugins/wasm-go/extensions/hmac-auth-apisix/main.go new file mode 100644 index 000000000..b0d2005fd --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/main.go @@ -0,0 +1,384 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "fmt" + "hash" + "math" + "regexp" + "strings" + "time" + + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" + "github.com/higress-group/wasm-go/pkg/log" + "github.com/higress-group/wasm-go/pkg/wrapper" + "hmac-auth-apisix/config" +) + +const ( + // 认证涉及的请求头 + authorizationHeader = "Authorization" + dateHeader = "Date" + digestHeader = "Digest" + // 认证通过后在请求头 consumerHeader 中添加消费者信息 + consumerHeader = "X-Mse-Consumer" + + signaturePrefix = "Signature " + errorResponseTemplate = `{"message":"client request can't be validated: %s"}` +) + +var ( + // 使用正则表达式匹配 key="value" 格式 + fieldRegex = regexp.MustCompile(`(\w+)="([^"]*)"`) +) + +func main() {} + +func init() { + wrapper.SetCtx( + "hmac-auth-apisix", + wrapper.ParseOverrideConfig(config.ParseGlobalConfig, config.ParseOverrideRuleConfig), + wrapper.ProcessRequestHeaders(onHttpRequestHeaders), + wrapper.ProcessRequestBody(onHttpRequestBody), + ) +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.HmacAuthConfig) types.Action { + var ( + // 未配置 allow 列表,表示插件在该 domain/route 未生效 + noAllow = len(cfg.Allow) == 0 + globalAuthNoSet = cfg.GlobalAuth == nil + globalAuthSetTrue = !globalAuthNoSet && *cfg.GlobalAuth + globalAuthSetFalse = !globalAuthNoSet && !*cfg.GlobalAuth + ruleSet = config.RuleSet + ) + + // 不需要认证而直接放行的情况: + // - global_auth == false 且 当前 domain/route 未配置该插件 + // - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件 + if globalAuthSetFalse || (globalAuthNoSet && ruleSet) { + if noAllow { + log.Info("authorization is not required") + ctx.DontReadRequestBody() + return types.ActionContinue + } + } + // 提取 HMAC 字段和消费者信息 + hmacParams, err := retrieveHmacFieldsAndConsumer(cfg) + if err != nil { + // 只有在完全无法解析认证信息时才考虑匿名消费者 + if cfg.AnonymousConsumer != "" { + ctx.DontReadRequestBody() + setConsumerHeader(cfg.AnonymousConsumer) + return types.ActionContinue + } + return sendUnauthorizedResponse(err.Error()) + } + log.Debugf("HMAC params extracted: keyId=%s, algorithm=%s, signature=%s, headers=%v, consumerName=%s", + hmacParams.KeyId, hmacParams.Algorithm, hmacParams.Signature, hmacParams.Headers, hmacParams.ConsumerName) + + if globalAuthSetTrue && !noAllow { // 全局生效,但当前 domain/route 配置了 allow 列表 + if !contains(cfg.Allow, hmacParams.ConsumerName) { + log.Warnf("consumer %q is not allowed", hmacParams.ConsumerName) + return sendUnauthorizedResponse("consumer '" + hmacParams.ConsumerName + "' is not allowed") + } + } else if globalAuthSetFalse || (globalAuthNoSet && ruleSet) { // 非全局生效 + if !noAllow && !contains(cfg.Allow, hmacParams.ConsumerName) { // 配置了 allow 列表且当前消费者不在 allow 列表中 + log.Warnf("consumer %q is not allowed", hmacParams.ConsumerName) + return sendUnauthorizedResponse("consumer '" + hmacParams.ConsumerName + "' is not allowed") + } + } + + // 校验时间偏差 + if cfg.ClockSkew > 0 { + if err := validateClockSkew(cfg.ClockSkew); err != nil { + return sendUnauthorizedResponse(err.Error()) + } + } + + // 验证算法是否允许 + if !contains(cfg.AllowedAlgorithms, hmacParams.Algorithm) { + return sendUnauthorizedResponse("Invalid algorithm") + } + + // 验证签名头 + if len(cfg.SignedHeaders) > 0 { + if len(hmacParams.Headers) == 0 { + return sendUnauthorizedResponse("headers missing") + } + + // 检查所有配置的签名头是否都在签名中 + signedHeadersMap := make(map[string]bool) + for _, header := range hmacParams.Headers { + signedHeadersMap[strings.ToLower(header)] = true + } + + for _, requiredHeader := range cfg.SignedHeaders { + lowerRequiredHeader := strings.ToLower(requiredHeader) + if !signedHeadersMap[lowerRequiredHeader] { + return sendUnauthorizedResponse("expected header \"" + requiredHeader + "\" missing in signing") + } + } + } + + // 验证 HMAC 签名 + if err := validateSignature(hmacParams, cfg); err != nil { + return sendUnauthorizedResponse(err.Error()) + } + + // 验证成功,设置消费者信息 + setConsumerHeader(hmacParams.ConsumerName) + + // 如果需要隐藏凭证 + if cfg.HideCredentials { + proxywasm.RemoveHttpRequestHeader(authorizationHeader) + } + + // 如果有请求体且需要验证请求体,进入 onHttpRequestBody 方法 + if wrapper.HasRequestBody() && cfg.ValidateRequestBody { + return types.HeaderStopIteration + } + ctx.DontReadRequestBody() + return types.ActionContinue +} + +func onHttpRequestBody(ctx wrapper.HttpContext, cfg config.HmacAuthConfig, body []byte) types.Action { + if cfg.ValidateRequestBody { + digestHeaderVal, _ := proxywasm.GetHttpRequestHeader(digestHeader) + if digestHeaderVal == "" { + return sendUnauthorizedResponse("Invalid digest") + } + + // 计算请求体的 SHA-256 摘要 + hash := sha256.Sum256(body) + encodedDigest := base64.StdEncoding.EncodeToString(hash[:]) + digestCreated := "SHA-256=" + encodedDigest + + // 比较请求头中的 Digest 和服务端计算的摘要 + if digestCreated != digestHeaderVal { + log.Warnf("Request body digest validation failed. Expected: %s, Got: %s, Body size: %d bytes", + digestCreated, digestHeaderVal, len(body)) + return sendUnauthorizedResponse("Invalid digest") + } + } + return types.ActionContinue +} + +// HmacParams 存储从 Authorization 头解析出的 HMAC 参数 +type HmacParams struct { + KeyId string + Algorithm string + Signature string + Headers []string + ConsumerName string +} + +// retrieveHmacFieldsAndConsumer 从 Authorization 头中提取 HMAC 参数和消费者信息 +func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, error) { + hmacParams := &HmacParams{} + + // 获取 Authorization 头 + authString, err := proxywasm.GetHttpRequestHeader(authorizationHeader) + if err != nil { + if err == types.ErrorStatusNotFound { + return nil, fmt.Errorf("missing Authorization header") + } + return nil, err + } + + // 检查是否以 "Signature " 开头 + if !strings.HasPrefix(authString, signaturePrefix) { + return nil, fmt.Errorf("Authorization header does not start with 'Signature '") + } + + // 使用正则表达式解析字段,跳过 "Signature " 前缀 + matches := fieldRegex.FindAllStringSubmatch(authString[len(signaturePrefix):], -1) + + for _, match := range matches { + if len(match) == 3 { + key := match[1] + value := match[2] + + switch key { + case "keyId": + hmacParams.KeyId = value + case "algorithm": + hmacParams.Algorithm = value + case "signature": + hmacParams.Signature = value + case "headers": + // 分割 headers 字段 + if value != "" { + hmacParams.Headers = strings.Split(value, " ") + } + } + } + } + + // 验证必要字段 + if hmacParams.KeyId == "" || hmacParams.Signature == "" { + return nil, fmt.Errorf("keyId or signature missing") + } + + if hmacParams.Algorithm == "" { + return nil, fmt.Errorf("algorithm missing") + } + + // 根据 keyId 查找消费者名称 + consumerName := "" + found := false + for _, consumer := range cfg.Consumers { + if consumer.AccessKey == hmacParams.KeyId { + consumerName = consumer.Name + found = true + break + } + } + + if !found { + return nil, fmt.Errorf("Invalid keyId") + } + + hmacParams.ConsumerName = consumerName + return hmacParams, nil +} + +// validateClockSkew 检查时间偏差 +func validateClockSkew(clockSkew int) error { + dateHeaderVal, _ := proxywasm.GetHttpRequestHeader(dateHeader) + if dateHeaderVal == "" { + return fmt.Errorf("Date header missing. failed to validate clock skew") + } + + // 解析 GMT 格式时间 + dateTime, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", dateHeaderVal) + if err != nil { + return fmt.Errorf("Invalid GMT format time") + } + + // 计算时间差 + currentTime := time.Now() + diff := math.Abs(float64(currentTime.Unix() - dateTime.Unix())) + + // 检查是否超过 clock_skew + if int(diff) > clockSkew { + return fmt.Errorf("Clock skew exceeded") + } + + return nil +} + +// validateSignature 验证签名 +func validateSignature(hmacParams *HmacParams, cfg config.HmacAuthConfig) error { + // 根据 keyId 查找对应的 secretKey + secretKey := "" + found := false + for _, consumer := range cfg.Consumers { + if consumer.AccessKey == hmacParams.KeyId { + secretKey = consumer.SecretKey + found = true + break + } + } + + if !found { + return fmt.Errorf("Invalid keyId") + } + + // 生成 HMAC 签名 + signingString, err := generateSigningString(hmacParams) + if err != nil { + return fmt.Errorf("Failed to generate signing string") + } + expectedSignature, err := generateHmacSignature(secretKey, hmacParams.Algorithm, signingString) + if err != nil { + return err + } + + // 比较签名 + if hmacParams.Signature != expectedSignature { + log.Warnf("Signature validation failed. Algorithm: %s, Expected: %s, Got: %s, Signing String: %s", + hmacParams.Algorithm, expectedSignature, hmacParams.Signature, signingString) + return fmt.Errorf("Invalid signature") + } + + return nil +} + +// generateSigningString 生成签名字符串 +func generateSigningString(hmacParams *HmacParams) (string, error) { + var signingStringItems []string + signingStringItems = append(signingStringItems, hmacParams.KeyId) + + // 获取请求方法和路径 + requestMethod, err := proxywasm.GetHttpRequestHeader(":method") + if err != nil { + requestMethod = "GET" + } + + requestURI, err := proxywasm.GetHttpRequestHeader(":path") + if err != nil || requestURI == "" { + requestURI = "/" + } + + if len(hmacParams.Headers) > 0 { + for _, h := range hmacParams.Headers { + if h == "@request-target" { + requestTarget := requestMethod + " " + requestURI + signingStringItems = append(signingStringItems, requestTarget) + } else { + headerValue, err := proxywasm.GetHttpRequestHeader(h) + if err == nil { + signingStringItems = append(signingStringItems, h+": "+headerValue) + } + } + } + } + + signingString := strings.Join(signingStringItems, "\n") + "\n" + return signingString, nil +} + +// generateHmacSignature 生成 HMAC 签名 +func generateHmacSignature(secretKey, algorithm, message string) (string, error) { + var mac hash.Hash + + switch algorithm { + case "hmac-sha1": + mac = hmac.New(sha1.New, []byte(secretKey)) + case "hmac-sha256": + mac = hmac.New(sha256.New, []byte(secretKey)) + case "hmac-sha512": + mac = hmac.New(sha512.New, []byte(secretKey)) + default: + return "", fmt.Errorf("unsupported algorithm: %s", algorithm) + } + + mac.Write([]byte(message)) + signature := mac.Sum(nil) + return base64.StdEncoding.EncodeToString(signature), nil +} + +func sendUnauthorizedResponse(message string) types.Action { + errorResponse := fmt.Sprintf(errorResponseTemplate, message) + proxywasm.SendHttpResponse(401, nil, []byte(errorResponse), -1) + return types.ActionContinue +} + +func setConsumerHeader(name string) { + _ = proxywasm.AddHttpRequestHeader(consumerHeader, name) +} + +func contains(arr []string, item string) bool { + for _, i := range arr { + if i == item { + return true + } + } + return false +}