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>
This commit is contained in:
@@ -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
|
||||
|
||||
284
weed/s3api/auth_signature_v2_test.go
Normal file
284
weed/s3api/auth_signature_v2_test.go
Normal file
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user