mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
401 lines
12 KiB
Go
401 lines
12 KiB
Go
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
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 = cfg.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 摘要
|
|
digestCreated := calculateBodyDigest(body)
|
|
|
|
// 比较请求头中的 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) {
|
|
params := &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":
|
|
params.KeyId = value
|
|
case "algorithm":
|
|
params.Algorithm = value
|
|
case "signature":
|
|
params.Signature = value
|
|
case "headers":
|
|
// 分割 headers 字段
|
|
if value != "" {
|
|
params.Headers = strings.Split(value, " ")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证必要字段
|
|
if params.KeyId == "" || params.Signature == "" {
|
|
return nil, fmt.Errorf("keyId or signature missing")
|
|
}
|
|
|
|
if params.Algorithm == "" {
|
|
return nil, fmt.Errorf("algorithm missing")
|
|
}
|
|
|
|
// 根据 keyId 查找消费者名称
|
|
consumerName := ""
|
|
found := false
|
|
for _, consumer := range cfg.Consumers {
|
|
if consumer.AccessKey == params.KeyId {
|
|
consumerName = consumer.Name
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, fmt.Errorf("Invalid keyId")
|
|
}
|
|
|
|
params.ConsumerName = consumerName
|
|
return params, 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 := generateSigningString(hmacParams)
|
|
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 {
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// calculateBodyDigest 计算请求体的 SHA-256 摘要
|
|
func calculateBodyDigest(body []byte) string {
|
|
hash := sha256.Sum256(body)
|
|
encodedDigest := base64.StdEncoding.EncodeToString(hash[:])
|
|
return "SHA-256=" + encodedDigest
|
|
}
|
|
|
|
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
|
|
}
|