mirror of
https://github.com/alibaba/higress.git
synced 2026-03-18 01:07:29 +08:00
fix: bedrock canonical URI sig (#3590)
This commit is contained in:
1
.github/workflows/wasm-plugin-unit-test.yml
vendored
1
.github/workflows/wasm-plugin-unit-test.yml
vendored
@@ -207,7 +207,6 @@ jobs:
|
||||
|
||||
- name: Install required tools
|
||||
run: |
|
||||
go install github.com/wadey/gocovmerge@latest
|
||||
sudo apt-get update && sudo apt-get install -y bc
|
||||
|
||||
- name: Download all test results
|
||||
|
||||
@@ -1372,44 +1372,11 @@ func encodeSigV4Path(path string) string {
|
||||
if seg == "" {
|
||||
continue
|
||||
}
|
||||
// Normalize to "single-encoded" form:
|
||||
// - raw ":" -> %3A
|
||||
// - already encoded "%3A" -> still %3A (not %253A)
|
||||
decoded, err := url.PathUnescape(seg)
|
||||
if err == nil {
|
||||
segments[i] = sigV4EscapePathSegment(decoded)
|
||||
} else {
|
||||
// If segment has invalid escape sequence, fall back to escaping raw segment.
|
||||
segments[i] = sigV4EscapePathSegment(seg)
|
||||
}
|
||||
segments[i] = url.PathEscape(seg)
|
||||
}
|
||||
return strings.Join(segments, "/")
|
||||
}
|
||||
|
||||
func sigV4EscapePathSegment(segment string) string {
|
||||
const upperHex = "0123456789ABCDEF"
|
||||
var b strings.Builder
|
||||
b.Grow(len(segment) * 3)
|
||||
for i := 0; i < len(segment); i++ {
|
||||
c := segment[i]
|
||||
if isSigV4Unreserved(c) {
|
||||
b.WriteByte(c)
|
||||
continue
|
||||
}
|
||||
b.WriteByte('%')
|
||||
b.WriteByte(upperHex[c>>4])
|
||||
b.WriteByte(upperHex[c&0x0F])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isSigV4Unreserved(c byte) bool {
|
||||
return (c >= 'A' && c <= 'Z') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '_' || c == '.' || c == '~'
|
||||
}
|
||||
|
||||
func getSignatureKey(key, dateStamp, region, service string) []byte {
|
||||
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
|
||||
kRegion := hmacSha256(kDate, region)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeSigV4Path(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "raw model id keeps colon",
|
||||
path: "/model/global.amazon.nova-2-lite-v1:0/converse-stream",
|
||||
want: "/model/global.amazon.nova-2-lite-v1:0/converse-stream",
|
||||
},
|
||||
{
|
||||
name: "pre-encoded model id escapes percent to avoid mismatch",
|
||||
path: "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream",
|
||||
want: "/model/global.amazon.nova-2-lite-v1%253A0/converse-stream",
|
||||
},
|
||||
{
|
||||
name: "raw inference profile arn keeps colon and slash delimiters",
|
||||
path: "/model/arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.anthropic.claude-sonnet-4-20250514-v1:0/converse",
|
||||
want: "/model/arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.anthropic.claude-sonnet-4-20250514-v1:0/converse",
|
||||
},
|
||||
{
|
||||
name: "encoded inference profile arn preserves escaped slash as double-escaped percent",
|
||||
path: "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.anthropic.claude-sonnet-4-20250514-v1%3A0/converse",
|
||||
want: "/model/arn%253Aaws%253Abedrock%253Aus-east-1%253A123456789012%253Ainference-profile%252Fglobal.anthropic.claude-sonnet-4-20250514-v1%253A0/converse",
|
||||
},
|
||||
{
|
||||
name: "query string is stripped before canonical encoding",
|
||||
path: "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream?trace=1&foo=bar",
|
||||
want: "/model/global.amazon.nova-2-lite-v1%253A0/converse-stream",
|
||||
},
|
||||
{
|
||||
name: "invalid percent sequence falls back to escaped percent",
|
||||
path: "/model/abc%ZZxyz/converse",
|
||||
want: "/model/abc%25ZZxyz/converse",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, encodeSigV4Path(tt.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverwriteRequestPathHeaderPreservesSingleEncodedRequestPath(t *testing.T) {
|
||||
p := &bedrockProvider{}
|
||||
plainModel := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.amazon.nova-2-lite-v1:0"
|
||||
preEncodedModel := url.QueryEscape(plainModel)
|
||||
|
||||
t.Run("plain model is encoded once", func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
p.overwriteRequestPathHeader(headers, bedrockChatCompletionPath, plainModel)
|
||||
assert.Equal(t, "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.amazon.nova-2-lite-v1%3A0/converse", headers.Get(":path"))
|
||||
})
|
||||
|
||||
t.Run("pre-encoded model is not double encoded", func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
p.overwriteRequestPathHeader(headers, bedrockChatCompletionPath, preEncodedModel)
|
||||
assert.Equal(t, "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.amazon.nova-2-lite-v1%3A0/converse", headers.Get(":path"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateSignatureIgnoresQueryStringInCanonicalURI(t *testing.T) {
|
||||
p := &bedrockProvider{
|
||||
config: ProviderConfig{
|
||||
awsRegion: "ap-northeast-3",
|
||||
awsSecretKey: "test-secret",
|
||||
},
|
||||
}
|
||||
body := []byte(`{"messages":[{"role":"user","content":[{"text":"hello"}]}]}`)
|
||||
pathWithoutQuery := "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream"
|
||||
pathWithQuery := pathWithoutQuery + "?trace=1&foo=bar"
|
||||
|
||||
sigWithoutQuery := p.generateSignature(pathWithoutQuery, "20260312T142942Z", "20260312", body)
|
||||
sigWithQuery := p.generateSignature(pathWithQuery, "20260312T142942Z", "20260312", body)
|
||||
assert.Equal(t, sigWithoutQuery, sigWithQuery)
|
||||
}
|
||||
|
||||
func TestGenerateSignatureDiffersForRawAndPreEncodedModelPath(t *testing.T) {
|
||||
p := &bedrockProvider{
|
||||
config: ProviderConfig{
|
||||
awsRegion: "ap-northeast-3",
|
||||
awsSecretKey: "test-secret",
|
||||
},
|
||||
}
|
||||
body := []byte(`{"messages":[{"role":"user","content":[{"text":"hello"}]}]}`)
|
||||
rawPath := "/model/global.amazon.nova-2-lite-v1:0/converse-stream"
|
||||
preEncodedPath := "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream"
|
||||
|
||||
rawSignature := p.generateSignature(rawPath, "20260312T142942Z", "20260312", body)
|
||||
preEncodedSignature := p.generateSignature(preEncodedPath, "20260312T142942Z", "20260312", body)
|
||||
assert.NotEqual(t, rawSignature, preEncodedSignature)
|
||||
}
|
||||
Reference in New Issue
Block a user