diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 74ecc8207..d534377a5 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -240,7 +240,7 @@ func (iam *IdentityAccessManagement) verifySignatureWithPath(extractedSignedHead stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) // Get hmac signing key. - signingKey := getSigningKey(secretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, "s3") + signingKey := getSigningKey(secretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, signV4Values.Credential.scope.service) // Calculate signature. newSignature := getSignature(signingKey, stringToSign) @@ -262,7 +262,7 @@ func (iam *IdentityAccessManagement) verifyPresignedSignatureWithPath(extractedS stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope()) // Get hmac signing key. - signingKey := getSigningKey(secretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3") + signingKey := getSigningKey(secretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, credHeader.scope.service) // Calculate expected signature. expectedSignature := getSignature(signingKey, stringToSign) @@ -485,7 +485,7 @@ func (iam *IdentityAccessManagement) doesPolicySignatureV4Match(formValues http. } // Get signing key. - signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3") + signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, credHeader.scope.service) // Get signature. newSignature := getSignature(signingKey, formValues.Get("Policy")) @@ -552,11 +552,11 @@ func extractHostHeader(r *http.Request) string { } // getScope generate a string of a specific date, an AWS region, and a service. -func getScope(t time.Time, region string) string { +func getScope(t time.Time, region string, service string) string { scope := strings.Join([]string{ t.Format(yyyymmdd), region, - "s3", + service, "aws4_request", }, "/") return scope diff --git a/weed/s3api/auto_signature_v4_test.go b/weed/s3api/auto_signature_v4_test.go index 29b6df968..7a9599583 100644 --- a/weed/s3api/auto_signature_v4_test.go +++ b/weed/s3api/auto_signature_v4_test.go @@ -1198,6 +1198,109 @@ func TestGitHubIssue7080Scenario(t *testing.T) { assert.Equal(t, testPayload, string(bodyBytes)) } +// TestIAMSignatureServiceMatching tests that IAM requests use the correct service in signature computation +// This reproduces the bug described in GitHub issue #7080 where the service was hardcoded to "s3" +func TestIAMSignatureServiceMatching(t *testing.T) { + // Create test IAM instance + iam := &IdentityAccessManagement{} + + // Load test configuration with credentials that match the logs + err := iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{ + Identities: []*iam_pb.Identity{ + { + Name: "power_user", + Credentials: []*iam_pb.Credential{ + { + AccessKey: "power_user_key", + SecretKey: "power_user_secret", + }, + }, + Actions: []string{"Admin"}, + }, + }, + }) + assert.NoError(t, err) + + // Use the exact payload and headers from the failing logs + testPayload := "Action=CreateAccessKey&UserName=admin&Version=2010-05-08" + + // Create request exactly as shown in logs + req, err := http.NewRequest("POST", "http://localhost:8111/", strings.NewReader(testPayload)) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") + req.Header.Set("Host", "localhost:8111") + req.Header.Set("X-Amz-Date", "20250805T082934Z") + + // Calculate the expected signature using the correct IAM service + // This simulates what botocore/AWS SDK would calculate + credentialScope := "20250805/us-east-1/iam/aws4_request" + + // Calculate the actual payload hash for our test payload + actualPayloadHash := getSHA256Hash([]byte(testPayload)) + + // Build the canonical request with the actual payload hash + canonicalRequest := "POST\n/\n\ncontent-type:application/x-www-form-urlencoded; charset=utf-8\nhost:localhost:8111\nx-amz-date:20250805T082934Z\n\ncontent-type;host;x-amz-date\n" + actualPayloadHash + + // Calculate the canonical request hash + canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest)) + + // Build the string to sign + stringToSign := "AWS4-HMAC-SHA256\n20250805T082934Z\n" + credentialScope + "\n" + canonicalRequestHash + + // Calculate expected signature using IAM service (what client sends) + expectedSigningKey := getSigningKey("power_user_secret", "20250805", "us-east-1", "iam") + expectedSignature := getSignature(expectedSigningKey, stringToSign) + + // Create authorization header with the correct signature + authHeader := "AWS4-HMAC-SHA256 Credential=power_user_key/" + credentialScope + + ", SignedHeaders=content-type;host;x-amz-date, Signature=" + expectedSignature + req.Header.Set("Authorization", authHeader) + + // Now test that SeaweedFS computes the same signature with our fix + identity, errCode := iam.doesSignatureMatch(actualPayloadHash, req) + + // With the fix, the signatures should match and we should get a successful authentication + assert.Equal(t, s3err.ErrNone, errCode) + assert.NotNil(t, identity) + assert.Equal(t, "power_user", identity.Name) +} + +// TestStreamingSignatureServiceField tests that the s3ChunkedReader struct correctly stores the service +// This verifies the fix for streaming uploads where getChunkSignature was hardcoding "s3" +func TestStreamingSignatureServiceField(t *testing.T) { + // Test that the s3ChunkedReader correctly uses the service field + // Create a mock s3ChunkedReader with IAM service + chunkedReader := &s3ChunkedReader{ + seedDate: time.Now(), + region: "us-east-1", + service: "iam", // This should be used instead of hardcoded "s3" + seedSignature: "testsignature", + cred: &Credential{ + AccessKey: "testkey", + SecretKey: "testsecret", + }, + } + + // Test that getScope is called with the correct service + scope := getScope(chunkedReader.seedDate, chunkedReader.region, chunkedReader.service) + assert.Contains(t, scope, "/iam/aws4_request") + assert.NotContains(t, scope, "/s3/aws4_request") + + // Test that getSigningKey would be called with the correct service + signingKey := getSigningKey( + chunkedReader.cred.SecretKey, + chunkedReader.seedDate.Format(yyyymmdd), + chunkedReader.region, + chunkedReader.service, + ) + assert.NotNil(t, signingKey) + + // The main point is that chunkedReader.service is "iam" and gets used correctly + // This ensures that IAM streaming uploads will use "iam" service instead of hardcoded "s3" + assert.Equal(t, "iam", chunkedReader.service) +} + // Test that large IAM request bodies are truncated for security (DoS prevention) func TestIAMLargeBodySecurityLimit(t *testing.T) { // Create test IAM instance diff --git a/weed/s3api/chunked_reader_v4.go b/weed/s3api/chunked_reader_v4.go index 53ea8e768..5973cbcc2 100644 --- a/weed/s3api/chunked_reader_v4.go +++ b/weed/s3api/chunked_reader_v4.go @@ -46,7 +46,7 @@ import ( // // returns signature, error otherwise if the signature mismatches or any other // error while parsing and validating. -func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cred *Credential, signature string, region string, date time.Time, errCode s3err.ErrorCode) { +func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cred *Credential, signature string, region string, service string, date time.Time, errCode s3err.ErrorCode) { // Copy request. req := *r @@ -57,7 +57,7 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr // Parse signature version '4' header. signV4Values, errCode := parseSignV4(v4Auth) if errCode != s3err.ErrNone { - return nil, "", "", time.Time{}, errCode + return nil, "", "", "", time.Time{}, errCode } contentSha256Header := req.Header.Get("X-Amz-Content-Sha256") @@ -69,7 +69,7 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr case streamingUnsignedPayload: glog.V(3).Infof("streaming unsigned payload") default: - return nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch + return nil, "", "", "", time.Time{}, s3err.ErrContentSHA256Mismatch } // Payload streaming. @@ -78,12 +78,12 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr // Extract all the signed headers along with its values. extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r) if errCode != s3err.ErrNone { - return nil, "", "", time.Time{}, errCode + return nil, "", "", "", time.Time{}, errCode } // Verify if the access key id matches. identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey) if !found { - return nil, "", "", time.Time{}, s3err.ErrInvalidAccessKeyID + return nil, "", "", "", time.Time{}, s3err.ErrInvalidAccessKeyID } bucket, object := s3_constants.GetBucketAndObject(r) @@ -99,14 +99,14 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr var dateStr string if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" { if dateStr = r.Header.Get("Date"); dateStr == "" { - return nil, "", "", time.Time{}, s3err.ErrMissingDateHeader + return nil, "", "", "", time.Time{}, s3err.ErrMissingDateHeader } } // Parse date header. date, err := time.Parse(iso8601Format, dateStr) if err != nil { - return nil, "", "", time.Time{}, s3err.ErrMalformedDate + return nil, "", "", "", time.Time{}, s3err.ErrMalformedDate } // Query string. queryStr := req.URL.Query().Encode() @@ -118,18 +118,18 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope()) // Get hmac signing key. - signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), region, "s3") + signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), region, signV4Values.Credential.scope.service) // Calculate signature. newSignature := getSignature(signingKey, stringToSign) // Verify if signature match. if !compareSignatureV4(newSignature, signV4Values.Signature) { - return nil, "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch + return nil, "", "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch } // Return calculated signature. - return cred, newSignature, region, date, s3err.ErrNone + return cred, newSignature, region, signV4Values.Credential.scope.service, date, s3err.ErrNone } const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB @@ -150,7 +150,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea authorizationHeader := req.Header.Get("Authorization") var ident *Credential - var seedSignature, region string + var seedSignature, region, service string var seedDate time.Time var errCode s3err.ErrorCode @@ -158,7 +158,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea // Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD' case streamingContentSHA256: glog.V(3).Infof("streaming content sha256") - ident, seedSignature, region, seedDate, errCode = iam.calculateSeedSignature(req) + ident, seedSignature, region, service, seedDate, errCode = iam.calculateSeedSignature(req) if errCode != s3err.ErrNone { return nil, errCode } @@ -167,7 +167,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea if authorizationHeader != "" { // We do not need to pass the seed signature to the Reader as each chunk is not signed, // but we do compute it to verify the caller has the correct permissions. - _, _, _, _, errCode = iam.calculateSeedSignature(req) + _, _, _, _, _, errCode = iam.calculateSeedSignature(req) if errCode != s3err.ErrNone { return nil, errCode } @@ -191,6 +191,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea seedSignature: seedSignature, seedDate: seedDate, region: region, + service: service, chunkSHA256Writer: sha256.New(), checkSumAlgorithm: checksumAlgorithm.String(), checkSumWriter: checkSumWriter, @@ -227,6 +228,7 @@ type s3ChunkedReader struct { seedSignature string seedDate time.Time region string + service string // Service from credential scope (e.g., "s3", "iam") state chunkState lastChunk bool chunkSignature string // Empty string if unsigned streaming upload. @@ -467,13 +469,13 @@ func (cr *s3ChunkedReader) getChunkSignature(hashedChunk string) string { // Calculate string to sign. stringToSign := signV4Algorithm + "-PAYLOAD" + "\n" + cr.seedDate.Format(iso8601Format) + "\n" + - getScope(cr.seedDate, cr.region) + "\n" + + getScope(cr.seedDate, cr.region, cr.service) + "\n" + cr.seedSignature + "\n" + emptySHA256 + "\n" + hashedChunk // Get hmac signing key. - signingKey := getSigningKey(cr.cred.SecretKey, cr.seedDate.Format(yyyymmdd), cr.region, "s3") + signingKey := getSigningKey(cr.cred.SecretKey, cr.seedDate.Format(yyyymmdd), cr.region, cr.service) // Calculate and return signature. return getSignature(signingKey, stringToSign)