mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-19 22:42:20 +00:00
44d575100a
* fix(s3api): preserve requested AES256 copy encryption Problem CopyObject metadata processing ignored an explicit x-amz-server-side-encryption: AES256 request header. A destination copy could lose the requested SSE-S3 metadata even though KMS requests were handled. Root cause processMetadataBytes only wrote the destination SSE header when the requested algorithm was aws:kms. Any other explicit SSE algorithm fell through to the source-preservation branch. Fix Write the requested SSE algorithm whenever x-amz-server-side-encryption is present, and keep KMS-specific metadata handling limited to aws:kms. Co-authored-by: Codex <noreply@openai.com> * fix(s3api): reject unsupported copy encryption algorithms A mistyped or unsupported x-amz-server-side-encryption value on a copy request slipped past validation and got persisted as the destination's algorithm header, advertising encryption that was never applied. Reject anything other than AES256 or aws:kms up front. * fix(s3api): write SSE key metadata for empty encrypted copies A zero-byte source copied with an explicit SSE request took the no-content branch and never ran the encryption path, leaving the object with a bare algorithm header but no key. HEAD then advertised SSE while the encryption-state machine saw the header as orphaned. Run the inline encryption path when the destination requests encryption so the key metadata is written too. * s3api: use SSEAlgorithmKMS constant in copy metadata handling * test(s3api): cover source SSE preservation on copy * test(iam): allow the local client's real source IP in SourceIp tests The aws:SourceIp allow policies hardcoded the loopback CIDRs, but a CI runner reaching the server over localhost can be observed with one of the host's RFC1918 addresses (the S3 endpoint is advertised on a 10.x interface), so the positive-condition PutObject was denied and the allow assertion flaked while the deny path passed trivially. Broaden the allow list to loopback plus private ranges via a shared helper, and log the denial on each failed attempt so any residual failure is diagnosable. --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
326 lines
9.4 KiB
Go
326 lines
9.4 KiB
Go
package s3api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// CopyValidationError represents validation errors during copy operations
|
|
type CopyValidationError struct {
|
|
Code s3err.ErrorCode
|
|
Message string
|
|
}
|
|
|
|
func (e *CopyValidationError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// ValidateCopyEncryption performs comprehensive validation of copy encryption parameters
|
|
func ValidateCopyEncryption(srcMetadata map[string][]byte, headers http.Header) error {
|
|
// Validate SSE-C copy requirements
|
|
if err := validateSSECCopyRequirements(srcMetadata, headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate SSE-KMS copy requirements
|
|
if err := validateSSEKMSCopyRequirements(srcMetadata, headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate incompatible encryption combinations
|
|
if err := validateEncryptionCompatibility(headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSECCopyRequirements validates SSE-C copy header requirements
|
|
func validateSSECCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error {
|
|
srcIsSSEC := IsSSECEncrypted(srcMetadata)
|
|
hasCopyHeaders := hasSSECCopyHeaders(headers)
|
|
hasSSECHeaders := hasSSECHeaders(headers)
|
|
|
|
// If source is SSE-C encrypted, copy headers are required
|
|
if srcIsSSEC && !hasCopyHeaders {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C encrypted source requires copy source encryption headers",
|
|
}
|
|
}
|
|
|
|
// If copy headers are provided, source must be SSE-C encrypted
|
|
if hasCopyHeaders && !srcIsSSEC {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy headers provided but source is not SSE-C encrypted",
|
|
}
|
|
}
|
|
|
|
// Validate copy header completeness
|
|
if hasCopyHeaders {
|
|
if err := validateSSECCopyHeaderCompleteness(headers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate destination SSE-C headers if present
|
|
if hasSSECHeaders {
|
|
if err := validateSSECHeaderCompleteness(headers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSEKMSCopyRequirements validates SSE-KMS copy requirements
|
|
func validateSSEKMSCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error {
|
|
dstIsSSEKMS := IsSSEKMSRequest(&http.Request{Header: headers})
|
|
|
|
// Validate KMS key ID format if provided
|
|
if dstIsSSEKMS {
|
|
keyID := headers.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
|
|
if keyID != "" && !isValidKMSKeyID(keyID) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrKMSKeyNotFound,
|
|
Message: fmt.Sprintf("Invalid KMS key ID format: %s", keyID),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate encryption context format if provided
|
|
if contextHeader := headers.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" {
|
|
if !dstIsSSEKMS {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Encryption context can only be used with SSE-KMS",
|
|
}
|
|
}
|
|
|
|
// Validate base64 encoding and JSON format
|
|
if err := validateEncryptionContext(contextHeader); err != nil {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Invalid encryption context: %v", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateEncryptionCompatibility validates that encryption methods are not conflicting
|
|
func validateEncryptionCompatibility(headers http.Header) error {
|
|
sseAlgorithm := headers.Get(s3_constants.AmzServerSideEncryption)
|
|
hasSSEC := hasSSECHeaders(headers)
|
|
hasSSEKMS := sseAlgorithm == s3_constants.SSEAlgorithmKMS
|
|
hasSSES3 := sseAlgorithm == s3_constants.SSEAlgorithmAES256
|
|
|
|
// Reject unsupported algorithms so they are never persisted as a bogus
|
|
// destination header advertising encryption that was never applied.
|
|
if sseAlgorithm != "" && !hasSSEKMS && !hasSSES3 {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidEncryptionAlgorithm,
|
|
Message: fmt.Sprintf("Unsupported server-side encryption algorithm: %s", sseAlgorithm),
|
|
}
|
|
}
|
|
|
|
// Count how many encryption methods are specified
|
|
encryptionCount := 0
|
|
if hasSSEC {
|
|
encryptionCount++
|
|
}
|
|
if hasSSEKMS {
|
|
encryptionCount++
|
|
}
|
|
if hasSSES3 {
|
|
encryptionCount++
|
|
}
|
|
|
|
// Only one encryption method should be specified
|
|
if encryptionCount > 1 {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Multiple encryption methods specified - only one is allowed",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSECCopyHeaderCompleteness validates that all required SSE-C copy headers are present
|
|
func validateSSECCopyHeaderCompleteness(headers http.Header) error {
|
|
algorithm := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm)
|
|
key := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey)
|
|
keyMD5 := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5)
|
|
|
|
if algorithm == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer algorithm header is required",
|
|
}
|
|
}
|
|
|
|
if key == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer key header is required",
|
|
}
|
|
}
|
|
|
|
if keyMD5 == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer key MD5 header is required",
|
|
}
|
|
}
|
|
|
|
// Validate algorithm
|
|
if algorithm != "AES256" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSECHeaderCompleteness validates that all required SSE-C headers are present
|
|
func validateSSECHeaderCompleteness(headers http.Header) error {
|
|
algorithm := headers.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
|
|
key := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKey)
|
|
keyMD5 := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
|
|
|
|
if algorithm == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer algorithm header is required",
|
|
}
|
|
}
|
|
|
|
if key == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer key header is required",
|
|
}
|
|
}
|
|
|
|
if keyMD5 == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer key MD5 header is required",
|
|
}
|
|
}
|
|
|
|
// Validate algorithm
|
|
if algorithm != "AES256" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions for header detection
|
|
func hasSSECCopyHeaders(headers http.Header) bool {
|
|
return headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm) != "" ||
|
|
headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey) != "" ||
|
|
headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5) != ""
|
|
}
|
|
|
|
func hasSSECHeaders(headers http.Header) bool {
|
|
return headers.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" ||
|
|
headers.Get(s3_constants.AmzServerSideEncryptionCustomerKey) != "" ||
|
|
headers.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5) != ""
|
|
}
|
|
|
|
// validateEncryptionContext validates the encryption context header format
|
|
func validateEncryptionContext(contextHeader string) error {
|
|
// This would validate base64 encoding and JSON format
|
|
// Implementation would decode base64 and parse JSON
|
|
// For now, just check it's not empty
|
|
if contextHeader == "" {
|
|
return fmt.Errorf("encryption context cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateCopySource validates the copy source path.
|
|
func ValidateCopySource(copySource string, srcBucket, srcObject string) error {
|
|
return validateCopySource(copySource, srcBucket, srcObject, "")
|
|
}
|
|
|
|
func validateCopySource(copySource string, srcBucket, srcObject, srcVersionId string) error {
|
|
if copySource == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source header is required",
|
|
}
|
|
}
|
|
|
|
if srcBucket == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Source bucket cannot be empty",
|
|
}
|
|
}
|
|
|
|
if srcObject == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Source object cannot be empty",
|
|
}
|
|
}
|
|
|
|
// `.`/`..` segments are collapsed by the filer's path join; reject them as
|
|
// IsValidObjectKey does for the request URL so the source stays in-bucket.
|
|
if !s3_constants.IsValidBucketName(srcBucket) || !s3_constants.IsValidObjectKey(srcObject) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source contains invalid path segments",
|
|
}
|
|
}
|
|
if !isValidVersionID(srcVersionId) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source contains an invalid version ID",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateCopyDestination validates the copy destination
|
|
func ValidateCopyDestination(dstBucket, dstObject string) error {
|
|
if dstBucket == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Destination bucket cannot be empty",
|
|
}
|
|
}
|
|
|
|
if dstObject == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Destination object cannot be empty",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MapCopyValidationError maps validation errors to appropriate S3 error codes
|
|
func MapCopyValidationError(err error) s3err.ErrorCode {
|
|
if validationErr, ok := err.(*CopyValidationError); ok {
|
|
return validationErr.Code
|
|
}
|
|
return s3err.ErrInvalidRequest
|
|
}
|