diff --git a/test/s3/iam/s3_sts_credential_prefix_test.go b/test/s3/iam/s3_sts_credential_prefix_test.go new file mode 100644 index 000000000..b8736d21f --- /dev/null +++ b/test/s3/iam/s3_sts_credential_prefix_test.go @@ -0,0 +1,82 @@ +package iam + +import ( + "encoding/xml" + "io" + "net/http" + "net/url" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSTSTemporaryCredentialPrefix verifies that STS temporary credentials use ASIA prefix +// This test ensures AWS compatibility - temporary credentials should use ASIA, not AKIA +func TestSTSTemporaryCredentialPrefix(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + if !isSTSEndpointRunning(t) { + t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint) + } + + // Use test credentials from environment or fall back to defaults + accessKey := os.Getenv("STS_TEST_ACCESS_KEY") + if accessKey == "" { + accessKey = "admin" + } + secretKey := os.Getenv("STS_TEST_SECRET_KEY") + if secretKey == "" { + secretKey = "admin" + } + + t.Run("assume_role_returns_asia_prefix", func(t *testing.T) { + resp, err := callSTSAPIWithSigV4(t, url.Values{ + "Action": {"AssumeRole"}, + "Version": {"2011-06-15"}, + "RoleArn": {"arn:aws:iam::role/admin"}, + "RoleSessionName": {"asia-prefix-test"}, + }, accessKey, secretKey) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + if resp.StatusCode != http.StatusOK { + t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) + t.Skip("AssumeRole not fully implemented yet") + } + + var stsResp AssumeRoleTestResponse + err = xml.Unmarshal(body, &stsResp) + require.NoError(t, err, "Failed to parse response: %s", string(body)) + + creds := stsResp.Result.Credentials + require.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should not be empty") + + // Verify ASIA prefix for temporary credentials + assert.True(t, strings.HasPrefix(creds.AccessKeyId, "ASIA"), + "Temporary credentials must use ASIA prefix (not AKIA for permanent keys), got: %s", creds.AccessKeyId) + + // Verify it's NOT using AKIA (permanent credentials) + assert.False(t, strings.HasPrefix(creds.AccessKeyId, "AKIA"), + "Temporary credentials must NOT use AKIA prefix (that's for permanent IAM keys), got: %s", creds.AccessKeyId) + + // Verify format: ASIA + 16 hex characters = 20 chars total + assert.Equal(t, 20, len(creds.AccessKeyId), + "Access key ID should be 20 characters (ASIA + 16 hex chars), got: %s", creds.AccessKeyId) + + t.Logf("✓ Temporary credentials correctly use ASIA prefix: %s", creds.AccessKeyId) + }) + + t.Run("assume_role_with_web_identity_returns_asia_prefix", func(t *testing.T) { + // This test would require OIDC setup, so we'll skip it for now + // but the same ASIA prefix validation should apply + t.Skip("AssumeRoleWithWebIdentity requires OIDC provider setup") + }) +} diff --git a/weed/iam/sts/credential_prefix_test.go b/weed/iam/sts/credential_prefix_test.go new file mode 100644 index 000000000..b3bd0d331 --- /dev/null +++ b/weed/iam/sts/credential_prefix_test.go @@ -0,0 +1,68 @@ +package sts + +import ( + "encoding/base64" + "encoding/hex" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TestTemporaryCredentialPrefix verifies that temporary credentials use ASIA prefix +// (not AKIA which is for permanent IAM user credentials) +func TestTemporaryCredentialPrefix(t *testing.T) { + sessionId := "test-session-for-prefix" + expiration := time.Now().Add(time.Hour) + + credGen := NewCredentialGenerator() + cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration) + + assert.NoError(t, err) + assert.NotNil(t, cred) + + // Verify ASIA prefix for temporary credentials + assert.True(t, strings.HasPrefix(cred.AccessKeyId, "ASIA"), + "Temporary credentials must use ASIA prefix, got: %s", cred.AccessKeyId) + + // Verify it's NOT using AKIA (permanent credentials) + assert.False(t, strings.HasPrefix(cred.AccessKeyId, "AKIA"), + "Temporary credentials must NOT use AKIA prefix (that's for permanent IAM keys)") +} + +// TestTemporaryCredentialFormat verifies the full format of temporary credentials +func TestTemporaryCredentialFormat(t *testing.T) { + sessionId := "format-test-session" + expiration := time.Now().Add(time.Hour) + + credGen := NewCredentialGenerator() + cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration) + + assert.NoError(t, err) + assert.NotNil(t, cred) + + // AWS temporary access key format: ASIA + 16 hex characters = 20 chars total + assert.Equal(t, 20, len(cred.AccessKeyId), + "Access key ID should be 20 characters (ASIA + 16 hex chars)") + + // Verify it starts with ASIA + assert.True(t, strings.HasPrefix(cred.AccessKeyId, "ASIA"), + "Access key must start with ASIA prefix") + + // Verify the rest is hex (after ASIA prefix) + hexPart := cred.AccessKeyId[4:] + assert.Equal(t, 16, len(hexPart), "Hex part should be 16 characters") + _, err = hex.DecodeString(hexPart) + assert.NoError(t, err, "The part after ASIA prefix should be valid hex") + + // Verify secret key is not empty and is a valid base64-encoded SHA256 hash + assert.NotEmpty(t, cred.SecretAccessKey) + assert.Equal(t, 44, len(cred.SecretAccessKey), + "SecretAccessKey should be 44 characters for a base64-encoded 32-byte hash") + _, err = base64.StdEncoding.DecodeString(cred.SecretAccessKey) + assert.NoError(t, err, "SecretAccessKey should be a valid base64 string") + + // Verify session token is not empty + assert.NotEmpty(t, cred.SessionToken) +} diff --git a/weed/iam/sts/token_utils.go b/weed/iam/sts/token_utils.go index a788287d8..69ab170ed 100644 --- a/weed/iam/sts/token_utils.go +++ b/weed/iam/sts/token_utils.go @@ -170,7 +170,7 @@ func NewCredentialGenerator() *CredentialGenerator { // GenerateTemporaryCredentials creates temporary AWS credentials func (c *CredentialGenerator) GenerateTemporaryCredentials(sessionId string, expiration time.Time) (*Credentials, error) { - accessKeyId, err := c.generateAccessKeyId(sessionId) + accessKeyId, err := c.generateTemporaryAccessKeyId(sessionId) if err != nil { return nil, fmt.Errorf("failed to generate access key ID: %w", err) } @@ -193,11 +193,12 @@ func (c *CredentialGenerator) GenerateTemporaryCredentials(sessionId string, exp }, nil } -// generateAccessKeyId generates an AWS-style access key ID -func (c *CredentialGenerator) generateAccessKeyId(sessionId string) (string, error) { +// generateTemporaryAccessKeyId generates an AWS-style access key ID for temporary STS credentials +func (c *CredentialGenerator) generateTemporaryAccessKeyId(sessionId string) (string, error) { // Create a deterministic but unique access key ID based on session hash := sha256.Sum256([]byte("access-key:" + sessionId)) - return "AKIA" + hex.EncodeToString(hash[:8]), nil // AWS format: AKIA + 16 chars + // Use ASIA prefix for temporary credentials (STS), not AKIA (permanent IAM keys) + return "ASIA" + hex.EncodeToString(hash[:8]), nil // AWS format: ASIA + 16 chars } // generateSecretAccessKey generates a deterministic secret access key based on sessionId