From e088b494f67765a976f477a0e5d175e0904ec335 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 1 Apr 2026 21:28:32 -0700 Subject: [PATCH] fix(s3): replace mock presigned URL signature with HMAC-SHA256 signing The presigned URL manager used generateMockSignature() which computed a plain SHA256 hash without any secret key, allowing anyone to forge valid presigned URL signatures. Replace with signPresignedURL() that implements AWS SigV4 HMAC-SHA256 signing: derives a signing key via the standard HMAC chain (date -> region -> service -> aws4_request), then signs a canonical request built from the HTTP method, path, query, and expiration. NewS3PresignedURLManager now accepts an optional signingKey variadic parameter for backward compatibility. --- weed/s3api/s3_presigned_url_iam.go | 55 ++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/weed/s3api/s3_presigned_url_iam.go b/weed/s3api/s3_presigned_url_iam.go index b731b1634..45b77bfbf 100644 --- a/weed/s3api/s3_presigned_url_iam.go +++ b/weed/s3api/s3_presigned_url_iam.go @@ -2,6 +2,7 @@ package s3api import ( "context" + "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" @@ -18,14 +19,20 @@ import ( // S3PresignedURLManager handles IAM integration for presigned URLs type S3PresignedURLManager struct { - s3iam *S3IAMIntegration + s3iam *S3IAMIntegration + signingKey string // secret key used for HMAC-SHA256 presigned URL signatures } -// NewS3PresignedURLManager creates a new presigned URL manager with IAM integration -func NewS3PresignedURLManager(s3iam *S3IAMIntegration) *S3PresignedURLManager { - return &S3PresignedURLManager{ +// NewS3PresignedURLManager creates a new presigned URL manager with IAM integration. +// signingKey is the server-side secret used to sign presigned URLs. +func NewS3PresignedURLManager(s3iam *S3IAMIntegration, signingKey ...string) *S3PresignedURLManager { + pm := &S3PresignedURLManager{ s3iam: s3iam, } + if len(signingKey) > 0 { + pm.signingKey = signingKey[0] + } + return pm } // PresignedURLRequest represents a request to generate a presigned URL @@ -163,10 +170,9 @@ func (pm *S3PresignedURLManager) generatePresignedURL(req *PresignedURLRequest, // Build canonical query string canonicalQuery := buildCanonicalQuery(queryParams) - // For now, we'll create a mock signature - // In production, this would use proper AWS signature v4 signing - mockSignature := generateMockSignature(req.Method, urlPath, canonicalQuery, identity.SessionToken) - queryParams["X-Amz-Signature"] = mockSignature + // Sign using HMAC-SHA256 with the server signing key (AWS SigV4-style) + signature := pm.signPresignedURL(req.Method, urlPath, canonicalQuery, expiresAt) + queryParams["X-Amz-Signature"] = signature // Build final URL finalQuery := buildCanonicalQuery(queryParams) @@ -262,13 +268,32 @@ func buildCanonicalQuery(params map[string]string) string { return strings.Join(parts, "&") } -// generateMockSignature generates a mock signature for testing purposes -func generateMockSignature(method, path, query, sessionToken string) string { - // This is a simplified signature for demonstration - // In production, use proper AWS signature v4 calculation - data := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, query, sessionToken) - hash := sha256.Sum256([]byte(data)) - return hex.EncodeToString(hash[:])[:16] // Truncate for readability +// signPresignedURL produces an HMAC-SHA256 signature following AWS SigV4 +// conventions: signingKey is derived from the server secret, date, region, +// and service, then used to sign the string-to-sign. +func (pm *S3PresignedURLManager) signPresignedURL(method, path, query string, date time.Time) string { + dateStamp := date.Format("20060102") + // Derive signing key: HMAC chain of date / region / service / "aws4_request" + kDate := hmacSHA256([]byte("AWS4"+pm.signingKey), []byte(dateStamp)) + kRegion := hmacSHA256(kDate, []byte("us-east-1")) + kService := hmacSHA256(kRegion, []byte("s3")) + kSigning := hmacSHA256(kService, []byte("aws4_request")) + + // Build string to sign + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\nhost\nhost\nUNSIGNED-PAYLOAD", method, path, query) + canonicalHash := sha256.Sum256([]byte(canonicalRequest)) + scope := fmt.Sprintf("%s/us-east-1/s3/aws4_request", dateStamp) + stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", + date.Format("20060102T150405Z"), scope, hex.EncodeToString(canonicalHash[:])) + + sig := hmacSHA256(kSigning, []byte(stringToSign)) + return hex.EncodeToString(sig) +} + +func hmacSHA256(key, data []byte) []byte { + h := hmac.New(sha256.New, key) + h.Write(data) + return h.Sum(nil) } // ValidatePresignedURLExpiration validates that a presigned URL hasn't expired