aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorqzh <zhihao.qu@ly.com>2025-11-27 04:24:02 +0800
committerGitHub <noreply@github.com>2025-11-26 12:24:02 -0800
commit3ab26e39ff7ed952dd33826a00b77bb03ab20427 (patch)
treeaa617993887f8d23aeeef2ac2ed9ee3220e95640
parentedf0ef7a80e58333a11cf8fff5915d7a5d8ea15a (diff)
downloadseaweedfs-3ab26e39ff7ed952dd33826a00b77bb03ab20427.tar.xz
seaweedfs-3ab26e39ff7ed952dd33826a00b77bb03ab20427.zip
fix(s3api): fix AWS Signature V2 format and validation (#7488)
* fix(s3api): fix AWS Signature V2 format and validation * fix(s3api): Skip space after "AWS" prefix (+1 offset) * test(s3api): add unit tests for Signature V2 authentication fix * fix(s3api): simply comparing signatures * validation for the colon extraction in expectedAuth --------- Co-authored-by: chrislu <chris.lu@gmail.com>
-rw-r--r--weed/s3api/auth_signature_v2.go25
-rw-r--r--weed/s3api/auth_signature_v2_test.go284
2 files changed, 306 insertions, 3 deletions
diff --git a/weed/s3api/auth_signature_v2.go b/weed/s3api/auth_signature_v2.go
index 77d04e1e0..5e226ec4e 100644
--- a/weed/s3api/auth_signature_v2.go
+++ b/weed/s3api/auth_signature_v2.go
@@ -134,7 +134,26 @@ func (iam *IdentityAccessManagement) doesSignV2Match(r *http.Request) (*Identity
}
expectedAuth := signatureV2(cred, r.Method, r.URL.Path, r.URL.Query().Encode(), r.Header)
- if !compareSignatureV2(v2Auth, expectedAuth) {
+
+ // Extract signatures from both auth headers
+ v2Signature := ""
+ expectedV2Signature := ""
+
+ // Extract signature from request header
+ if idx := strings.LastIndex(v2Auth, ":"); idx != -1 {
+ v2Signature = v2Auth[idx+1:]
+ }
+
+ // Extract signature from expected auth header
+ // This should always succeed if signatureV2 is working correctly
+ if idx := strings.LastIndex(expectedAuth, ":"); idx != -1 {
+ expectedV2Signature = expectedAuth[idx+1:]
+ } else {
+ // This indicates a bug in signatureV2 function
+ return nil, s3err.ErrSignatureDoesNotMatch
+ }
+
+ if !compareSignatureV2(v2Signature, expectedV2Signature) {
return nil, s3err.ErrSignatureDoesNotMatch
}
return identity, s3err.ErrNone
@@ -204,7 +223,7 @@ func validateV2AuthHeader(v2Auth string) (accessKey string, errCode s3err.ErrorC
}
// Strip off the Algorithm prefix.
- v2Auth = v2Auth[len(signV2Algorithm):]
+ v2Auth = v2Auth[len(signV2Algorithm)+1:]
authFields := strings.Split(v2Auth, ":")
if len(authFields) != 2 {
return "", s3err.ErrMissingFields
@@ -227,7 +246,7 @@ func validateV2AuthHeader(v2Auth string) (accessKey string, errCode s3err.ErrorC
func signatureV2(cred *Credential, method string, encodedResource string, encodedQuery string, headers http.Header) string {
stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, "")
signature := calculateSignatureV2(stringToSign, cred.SecretKey)
- return signV2Algorithm + cred.AccessKey + ":" + signature
+ return signV2Algorithm + " " + cred.AccessKey + ":" + signature
}
// getStringToSignV2 - string to sign in accordance with
diff --git a/weed/s3api/auth_signature_v2_test.go b/weed/s3api/auth_signature_v2_test.go
new file mode 100644
index 000000000..d876c5abe
--- /dev/null
+++ b/weed/s3api/auth_signature_v2_test.go
@@ -0,0 +1,284 @@
+package s3api
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
+)
+
+func setupTestIAMForV2Auth() *IdentityAccessManagement {
+ iam := &IdentityAccessManagement{
+ identities: []*Identity{},
+ accessKeyIdent: make(map[string]*Identity),
+ }
+
+ testCred := &Credential{
+ AccessKey: "AKIAIOSFODNN7EXAMPLE",
+ SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+ }
+
+ testIdentity := &Identity{
+ Name: "testUser",
+ Account: &AccountAdmin,
+ Credentials: []*Credential{testCred},
+ Actions: []Action{
+ s3_constants.ACTION_ADMIN,
+ },
+ }
+
+ iam.identities = append(iam.identities, testIdentity)
+ iam.accessKeyIdent[testCred.AccessKey] = testIdentity
+
+ return iam
+}
+
+func TestValidateV2AuthHeader(t *testing.T) {
+ tests := []struct {
+ name string
+ authHeader string
+ expectedAccessKey string
+ expectedError s3err.ErrorCode
+ }{
+ {
+ name: "valid auth header with space",
+ authHeader: "AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg=",
+ expectedAccessKey: "AKIAIOSFODNN7EXAMPLE",
+ expectedError: s3err.ErrNone,
+ },
+ {
+ name: "empty auth header",
+ authHeader: "",
+ expectedError: s3err.ErrAuthHeaderEmpty,
+ },
+ {
+ name: "wrong algorithm prefix",
+ authHeader: "HMAC AKIAIOSFODNN7EXAMPLE:signature",
+ expectedError: s3err.ErrSignatureVersionNotSupported,
+ },
+ {
+ name: "missing colon separator",
+ authHeader: "AWS AKIAIOSFODNN7EXAMPLE",
+ expectedError: s3err.ErrMissingFields,
+ },
+ {
+ name: "empty access key",
+ authHeader: "AWS :signature",
+ expectedError: s3err.ErrInvalidAccessKeyID,
+ },
+ {
+ name: "empty signature",
+ authHeader: "AWS AKIAIOSFODNN7EXAMPLE:",
+ expectedError: s3err.ErrMissingFields,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ accessKey, errCode := validateV2AuthHeader(tt.authHeader)
+
+ if errCode != tt.expectedError {
+ t.Errorf("validateV2AuthHeader() error = %v, want %v", errCode, tt.expectedError)
+ }
+
+ if errCode == s3err.ErrNone && accessKey != tt.expectedAccessKey {
+ t.Errorf("validateV2AuthHeader() accessKey = %q, want %q", accessKey, tt.expectedAccessKey)
+ }
+ })
+ }
+}
+
+func TestSignatureV2Format(t *testing.T) {
+ cred := &Credential{
+ AccessKey: "AKIAIOSFODNN7EXAMPLE",
+ SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+ }
+
+ headers := http.Header{}
+ headers.Set("Date", "Mon, 09 Sep 2011 23:36:00 GMT")
+
+ signature := signatureV2(cred, "GET", "/bucket/object", "", headers)
+
+ // Verify format: "AWS <AccessKey>:<Signature>" with space after AWS
+ expectedPrefix := "AWS " + cred.AccessKey + ":"
+ if len(signature) < len(expectedPrefix) {
+ t.Fatalf("Signature too short: %s", signature)
+ }
+
+ actualPrefix := signature[:len(expectedPrefix)]
+ if actualPrefix != expectedPrefix {
+ t.Errorf("Signature prefix = %q, want %q", actualPrefix, expectedPrefix)
+ }
+}
+
+func TestDoesSignV2Match(t *testing.T) {
+ iam := setupTestIAMForV2Auth()
+ cred := &Credential{
+ AccessKey: "AKIAIOSFODNN7EXAMPLE",
+ SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+ }
+
+ tests := []struct {
+ name string
+ method string
+ path string
+ query string
+ headers map[string]string
+ authOverride string
+ expectedError s3err.ErrorCode
+ expectIdent bool
+ }{
+ {
+ name: "valid GET request",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ expectedError: s3err.ErrNone,
+ expectIdent: true,
+ },
+ {
+ name: "valid PUT request with content headers",
+ method: "PUT",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{
+ "Date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ "Content-Type": "text/plain",
+ "Content-Md5": "c8fdb181845a4ca6b8fec737b3581d76",
+ },
+ expectedError: s3err.ErrNone,
+ expectIdent: true,
+ },
+ {
+ name: "request with query parameters",
+ method: "GET",
+ path: "/bucket/object",
+ query: "acl&versionId=123",
+ headers: map[string]string{
+ "Date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ expectedError: s3err.ErrNone,
+ expectIdent: true,
+ },
+ {
+ name: "request with x-amz headers",
+ method: "PUT",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{
+ "Date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ "x-amz-storage-class": "REDUCED_REDUNDANCY",
+ "x-amz-meta-custom": "value",
+ },
+ expectedError: s3err.ErrNone,
+ expectIdent: true,
+ },
+ {
+ name: "invalid signature",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "AWS AKIAIOSFODNN7EXAMPLE:invalidSignature123456==",
+ expectedError: s3err.ErrSignatureDoesNotMatch,
+ expectIdent: false,
+ },
+ {
+ name: "non-existent access key",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "AWS NONEXISTENTKEY:signature==",
+ expectedError: s3err.ErrInvalidAccessKeyID,
+ expectIdent: false,
+ },
+ {
+ name: "empty authorization header",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "",
+ expectedError: s3err.ErrAuthHeaderEmpty,
+ expectIdent: false,
+ },
+ {
+ name: "malformed auth - missing signature",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "AWS AKIAIOSFODNN7EXAMPLE",
+ expectedError: s3err.ErrMissingFields,
+ expectIdent: false,
+ },
+ {
+ name: "malformed auth - wrong prefix",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "HMAC AKIAIOSFODNN7EXAMPLE:sig",
+ expectedError: s3err.ErrSignatureVersionNotSupported,
+ expectIdent: false,
+ },
+ {
+ name: "malformed auth - no space after AWS",
+ method: "GET",
+ path: "/bucket/object",
+ query: "",
+ headers: map[string]string{"Date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ authOverride: "AWSAKIAIOSFODNN7EXAMPLE:signature==",
+ expectedError: s3err.ErrInvalidAccessKeyID,
+ expectIdent: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ url := "http://example.com" + tt.path
+ if tt.query != "" {
+ url += "?" + tt.query
+ }
+ req, err := http.NewRequest(tt.method, url, nil)
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ for key, value := range tt.headers {
+ req.Header.Set(key, value)
+ }
+
+ var authHeader string
+ if tt.authOverride != "" {
+ authHeader = tt.authOverride
+ } else {
+ authHeader = signatureV2(cred, req.Method, req.URL.Path, req.URL.Query().Encode(), req.Header)
+ }
+ if tt.name != "empty authorization header" {
+ req.Header.Set("Authorization", authHeader)
+ }
+
+ identity, errCode := iam.doesSignV2Match(req)
+
+ if errCode != tt.expectedError {
+ t.Errorf("doesSignV2Match() error = %v, want %v", errCode, tt.expectedError)
+ }
+
+ if tt.expectIdent && identity == nil {
+ t.Error("Expected non-nil identity")
+ }
+
+ if !tt.expectIdent && identity != nil {
+ t.Error("Expected nil identity")
+ }
+
+ if identity != nil && identity.Name != "testUser" {
+ t.Errorf("Identity name = %q, want %q", identity.Name, "testUser")
+ }
+ })
+ }
+}