mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-08-20 08:47:00 +08:00
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:
parent
b01b5e0f34
commit
c6d9756933
@ -240,7 +240,7 @@ func (iam *IdentityAccessManagement) verifySignatureWithPath(extractedSignedHead
|
|||||||
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
|
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate signature.
|
||||||
newSignature := getSignature(signingKey, stringToSign)
|
newSignature := getSignature(signingKey, stringToSign)
|
||||||
@ -262,7 +262,7 @@ func (iam *IdentityAccessManagement) verifyPresignedSignatureWithPath(extractedS
|
|||||||
stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope())
|
stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope())
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate expected signature.
|
||||||
expectedSignature := getSignature(signingKey, stringToSign)
|
expectedSignature := getSignature(signingKey, stringToSign)
|
||||||
@ -485,7 +485,7 @@ func (iam *IdentityAccessManagement) doesPolicySignatureV4Match(formValues http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get signing key.
|
// 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.
|
// Get signature.
|
||||||
newSignature := getSignature(signingKey, formValues.Get("Policy"))
|
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.
|
// 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{
|
scope := strings.Join([]string{
|
||||||
t.Format(yyyymmdd),
|
t.Format(yyyymmdd),
|
||||||
region,
|
region,
|
||||||
"s3",
|
service,
|
||||||
"aws4_request",
|
"aws4_request",
|
||||||
}, "/")
|
}, "/")
|
||||||
return scope
|
return scope
|
||||||
|
|||||||
@ -1198,6 +1198,109 @@ func TestGitHubIssue7080Scenario(t *testing.T) {
|
|||||||
assert.Equal(t, testPayload, string(bodyBytes))
|
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)
|
// Test that large IAM request bodies are truncated for security (DoS prevention)
|
||||||
func TestIAMLargeBodySecurityLimit(t *testing.T) {
|
func TestIAMLargeBodySecurityLimit(t *testing.T) {
|
||||||
// Create test IAM instance
|
// Create test IAM instance
|
||||||
|
|||||||
@ -46,7 +46,7 @@ import (
|
|||||||
//
|
//
|
||||||
// returns signature, error otherwise if the signature mismatches or any other
|
// returns signature, error otherwise if the signature mismatches or any other
|
||||||
// error while parsing and validating.
|
// 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.
|
// Copy request.
|
||||||
req := *r
|
req := *r
|
||||||
@ -57,7 +57,7 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
// Parse signature version '4' header.
|
// Parse signature version '4' header.
|
||||||
signV4Values, errCode := parseSignV4(v4Auth)
|
signV4Values, errCode := parseSignV4(v4Auth)
|
||||||
if errCode != s3err.ErrNone {
|
if errCode != s3err.ErrNone {
|
||||||
return nil, "", "", time.Time{}, errCode
|
return nil, "", "", "", time.Time{}, errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
contentSha256Header := req.Header.Get("X-Amz-Content-Sha256")
|
contentSha256Header := req.Header.Get("X-Amz-Content-Sha256")
|
||||||
@ -69,7 +69,7 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
case streamingUnsignedPayload:
|
case streamingUnsignedPayload:
|
||||||
glog.V(3).Infof("streaming unsigned payload")
|
glog.V(3).Infof("streaming unsigned payload")
|
||||||
default:
|
default:
|
||||||
return nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch
|
return nil, "", "", "", time.Time{}, s3err.ErrContentSHA256Mismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload streaming.
|
// Payload streaming.
|
||||||
@ -78,12 +78,12 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
// Extract all the signed headers along with its values.
|
// Extract all the signed headers along with its values.
|
||||||
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
||||||
if errCode != s3err.ErrNone {
|
if errCode != s3err.ErrNone {
|
||||||
return nil, "", "", time.Time{}, errCode
|
return nil, "", "", "", time.Time{}, errCode
|
||||||
}
|
}
|
||||||
// Verify if the access key id matches.
|
// Verify if the access key id matches.
|
||||||
identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey)
|
identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, "", "", time.Time{}, s3err.ErrInvalidAccessKeyID
|
return nil, "", "", "", time.Time{}, s3err.ErrInvalidAccessKeyID
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
bucket, object := s3_constants.GetBucketAndObject(r)
|
||||||
@ -99,14 +99,14 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
var dateStr string
|
var dateStr string
|
||||||
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
|
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
|
||||||
if dateStr = r.Header.Get("Date"); dateStr == "" {
|
if dateStr = r.Header.Get("Date"); dateStr == "" {
|
||||||
return nil, "", "", time.Time{}, s3err.ErrMissingDateHeader
|
return nil, "", "", "", time.Time{}, s3err.ErrMissingDateHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse date header.
|
// Parse date header.
|
||||||
date, err := time.Parse(iso8601Format, dateStr)
|
date, err := time.Parse(iso8601Format, dateStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", "", time.Time{}, s3err.ErrMalformedDate
|
return nil, "", "", "", time.Time{}, s3err.ErrMalformedDate
|
||||||
}
|
}
|
||||||
// Query string.
|
// Query string.
|
||||||
queryStr := req.URL.Query().Encode()
|
queryStr := req.URL.Query().Encode()
|
||||||
@ -118,18 +118,18 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope())
|
stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope())
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate signature.
|
||||||
newSignature := getSignature(signingKey, stringToSign)
|
newSignature := getSignature(signingKey, stringToSign)
|
||||||
|
|
||||||
// Verify if signature match.
|
// Verify if signature match.
|
||||||
if !compareSignatureV4(newSignature, signV4Values.Signature) {
|
if !compareSignatureV4(newSignature, signV4Values.Signature) {
|
||||||
return nil, "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch
|
return nil, "", "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return calculated signature.
|
// 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
|
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")
|
authorizationHeader := req.Header.Get("Authorization")
|
||||||
|
|
||||||
var ident *Credential
|
var ident *Credential
|
||||||
var seedSignature, region string
|
var seedSignature, region, service string
|
||||||
var seedDate time.Time
|
var seedDate time.Time
|
||||||
var errCode s3err.ErrorCode
|
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'
|
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
||||||
case streamingContentSHA256:
|
case streamingContentSHA256:
|
||||||
glog.V(3).Infof("streaming content sha256")
|
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 {
|
if errCode != s3err.ErrNone {
|
||||||
return nil, errCode
|
return nil, errCode
|
||||||
}
|
}
|
||||||
@ -167,7 +167,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea
|
|||||||
if authorizationHeader != "" {
|
if authorizationHeader != "" {
|
||||||
// We do not need to pass the seed signature to the Reader as each chunk is not signed,
|
// 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.
|
// 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 {
|
if errCode != s3err.ErrNone {
|
||||||
return nil, errCode
|
return nil, errCode
|
||||||
}
|
}
|
||||||
@ -191,6 +191,7 @@ func (iam *IdentityAccessManagement) newChunkedReader(req *http.Request) (io.Rea
|
|||||||
seedSignature: seedSignature,
|
seedSignature: seedSignature,
|
||||||
seedDate: seedDate,
|
seedDate: seedDate,
|
||||||
region: region,
|
region: region,
|
||||||
|
service: service,
|
||||||
chunkSHA256Writer: sha256.New(),
|
chunkSHA256Writer: sha256.New(),
|
||||||
checkSumAlgorithm: checksumAlgorithm.String(),
|
checkSumAlgorithm: checksumAlgorithm.String(),
|
||||||
checkSumWriter: checkSumWriter,
|
checkSumWriter: checkSumWriter,
|
||||||
@ -227,6 +228,7 @@ type s3ChunkedReader struct {
|
|||||||
seedSignature string
|
seedSignature string
|
||||||
seedDate time.Time
|
seedDate time.Time
|
||||||
region string
|
region string
|
||||||
|
service string // Service from credential scope (e.g., "s3", "iam")
|
||||||
state chunkState
|
state chunkState
|
||||||
lastChunk bool
|
lastChunk bool
|
||||||
chunkSignature string // Empty string if unsigned streaming upload.
|
chunkSignature string // Empty string if unsigned streaming upload.
|
||||||
@ -467,13 +469,13 @@ func (cr *s3ChunkedReader) getChunkSignature(hashedChunk string) string {
|
|||||||
// Calculate string to sign.
|
// Calculate string to sign.
|
||||||
stringToSign := signV4Algorithm + "-PAYLOAD" + "\n" +
|
stringToSign := signV4Algorithm + "-PAYLOAD" + "\n" +
|
||||||
cr.seedDate.Format(iso8601Format) + "\n" +
|
cr.seedDate.Format(iso8601Format) + "\n" +
|
||||||
getScope(cr.seedDate, cr.region) + "\n" +
|
getScope(cr.seedDate, cr.region, cr.service) + "\n" +
|
||||||
cr.seedSignature + "\n" +
|
cr.seedSignature + "\n" +
|
||||||
emptySHA256 + "\n" +
|
emptySHA256 + "\n" +
|
||||||
hashedChunk
|
hashedChunk
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate and return signature.
|
||||||
return getSignature(signingKey, stringToSign)
|
return getSignature(signingKey, stringToSign)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user