feat(wasm-plugin): add hmac-auth-apisix plugin (#2815)

This commit is contained in:
韩贤涛
2025-08-26 21:10:43 +08:00
committed by GitHub
parent c0ddbccbfe
commit b2ffeff7b8
5 changed files with 997 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
}