fix signature hashing for iam (#7100)

* fix signature hashing for iam

* add tests

* address comments

* Update weed/s3api/auto_signature_v4_test.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* indention

* fix test

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Chris Lu
2025-08-05 22:54:54 -07:00
committed by GitHub
parent b01b5e0f34
commit c6d9756933
3 changed files with 125 additions and 20 deletions

View File

@@ -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