snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

506
test/s3/sse/Makefile Normal file
View File

@@ -0,0 +1,506 @@
# Makefile for S3 SSE Integration Tests
# This Makefile provides targets for running comprehensive S3 Server-Side Encryption tests
# Default values
SEAWEEDFS_BINARY ?= weed
S3_PORT ?= 8333
FILER_PORT ?= 8888
VOLUME_PORT ?= 8080
MASTER_PORT ?= 9333
TEST_TIMEOUT ?= 15m
BUCKET_PREFIX ?= test-sse-
ACCESS_KEY ?= some_access_key1
SECRET_KEY ?= some_secret_key1
VOLUME_MAX_SIZE_MB ?= 50
VOLUME_MAX_COUNT ?= 100
# SSE-KMS configuration
KMS_KEY_ID ?= test-key-123
KMS_TYPE ?= local
OPENBAO_ADDR ?= http://127.0.0.1:8200
OPENBAO_TOKEN ?= root-token-for-testing
DOCKER_COMPOSE ?= docker-compose
# Test directory
TEST_DIR := $(shell pwd)
SEAWEEDFS_ROOT := $(shell cd ../../../ && pwd)
# Colors for output
RED := \033[0;31m
GREEN := \033[0;32m
YELLOW := \033[1;33m
NC := \033[0m # No Color
.PHONY: all test clean start-seaweedfs stop-seaweedfs stop-seaweedfs-safe start-seaweedfs-ci check-binary build-weed help help-extended test-with-server test-quick-with-server test-metadata-persistence setup-openbao test-with-kms test-ssekms-integration clean-kms start-full-stack stop-full-stack
all: test-basic
# Build SeaweedFS binary (GitHub Actions compatible)
build-weed:
@echo "Building SeaweedFS binary..."
@cd $(SEAWEEDFS_ROOT)/weed && go install -buildvcs=false
@echo "✅ SeaweedFS binary built successfully"
help:
@echo "SeaweedFS S3 SSE Integration Tests"
@echo ""
@echo "Available targets:"
@echo " test-basic - Run basic S3 put/get tests first"
@echo " test - Run all S3 SSE integration tests"
@echo " test-ssec - Run SSE-C tests only"
@echo " test-ssekms - Run SSE-KMS tests only"
@echo " test-copy - Run SSE copy operation tests"
@echo " test-multipart - Run SSE multipart upload tests"
@echo " test-errors - Run SSE error condition tests"
@echo " benchmark - Run SSE performance benchmarks"
@echo " KMS Integration:"
@echo " setup-openbao - Set up OpenBao KMS for testing"
@echo " test-with-kms - Run full SSE integration with real KMS"
@echo " test-ssekms-integration - Run SSE-KMS with OpenBao only"
@echo " start-full-stack - Start SeaweedFS + OpenBao with Docker"
@echo " stop-full-stack - Stop Docker services"
@echo " clean-kms - Clean up KMS test environment"
@echo " start-seaweedfs - Start SeaweedFS server for testing"
@echo " stop-seaweedfs - Stop SeaweedFS server"
@echo " clean - Clean up test artifacts"
@echo " check-binary - Check if SeaweedFS binary exists"
@echo ""
@echo "Configuration:"
@echo " SEAWEEDFS_BINARY=$(SEAWEEDFS_BINARY)"
@echo " S3_PORT=$(S3_PORT)"
@echo " FILER_PORT=$(FILER_PORT)"
@echo " VOLUME_PORT=$(VOLUME_PORT)"
@echo " MASTER_PORT=$(MASTER_PORT)"
@echo " TEST_TIMEOUT=$(TEST_TIMEOUT)"
@echo " VOLUME_MAX_SIZE_MB=$(VOLUME_MAX_SIZE_MB)"
check-binary:
@if ! command -v $(SEAWEEDFS_BINARY) > /dev/null 2>&1; then \
echo "$(RED)Error: SeaweedFS binary '$(SEAWEEDFS_BINARY)' not found in PATH$(NC)"; \
echo "Please build SeaweedFS first by running 'make' in the root directory"; \
exit 1; \
fi
@echo "$(GREEN)SeaweedFS binary found: $$(which $(SEAWEEDFS_BINARY))$(NC)"
start-seaweedfs: check-binary
@echo "$(YELLOW)Starting SeaweedFS server for SSE testing...$(NC)"
@# Use port-based cleanup for consistency and safety
@echo "Cleaning up any existing processes..."
@lsof -ti :$(MASTER_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(VOLUME_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(FILER_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(S3_PORT) | xargs -r kill -TERM || true
@sleep 2
# Create necessary directories
@mkdir -p /tmp/seaweedfs-test-sse
# Create S3 configuration with SSE-KMS support
@printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}],"kms":{"type":"%s","configs":{"keyId":"%s","encryptionContext":{},"bucketKey":false}}}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" "$(KMS_TYPE)" "$(KMS_KEY_ID)" > /tmp/seaweedfs-sse-s3.json
# Start weed mini (WEED_S3_SSE_KEY enables SSE-S3 encryption)
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) WEED_S3_SSE_KEY=test-sse-s3-key $(SEAWEEDFS_BINARY) mini \
-dir=/tmp/seaweedfs-test-sse \
-s3.port=$(S3_PORT) \
-s3.config=/tmp/seaweedfs-sse-s3.json \
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
@echo "Checking S3 service is ready..."
@for i in $$(seq 1 30); do \
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
echo "✅ S3 service is ready"; \
break; \
fi; \
sleep 1; \
done
stop-seaweedfs:
@echo "$(YELLOW)Stopping SeaweedFS server...$(NC)"
@# Use port-based cleanup for consistency and safety
@if [ -f /tmp/weed-mini.pid ]; then \
echo "Stopping weed mini..."; \
kill $$(cat /tmp/weed-mini.pid) || true; \
rm -f /tmp/weed-mini.pid; \
fi
@lsof -ti :$(MASTER_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(VOLUME_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(FILER_PORT) | xargs -r kill -TERM || true
@lsof -ti :$(S3_PORT) | xargs -r kill -TERM || true
@sleep 2
@echo "$(GREEN)SeaweedFS server stopped$(NC)"
# CI-safe server stop that's more conservative
stop-seaweedfs-safe:
@echo "$(YELLOW)Safely stopping SeaweedFS server...$(NC)"
@# Use port-based cleanup which is safer in CI
@if command -v lsof >/dev/null 2>&1; then \
echo "Using lsof for port-based cleanup..."; \
lsof -ti :$(MASTER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
lsof -ti :$(VOLUME_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
lsof -ti :$(FILER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
else \
echo "lsof not available, using netstat approach..."; \
netstat -tlnp 2>/dev/null | grep :$(MASTER_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
netstat -tlnp 2>/dev/null | grep :$(VOLUME_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
netstat -tlnp 2>/dev/null | grep :$(FILER_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
netstat -tlnp 2>/dev/null | grep :$(S3_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
fi
@sleep 2
@echo "$(GREEN)SeaweedFS server safely stopped$(NC)"
clean:
@echo "$(YELLOW)Cleaning up SSE test artifacts...$(NC)"
@rm -rf /tmp/seaweedfs-test-sse-*
@rm -f /tmp/seaweedfs-sse-*.log
@rm -f /tmp/seaweedfs-sse-s3.json
@echo "$(GREEN)SSE test cleanup completed$(NC)"
test-basic: check-binary
@echo "$(YELLOW)Running basic S3 SSE integration tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting basic SSE tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSECIntegrationBasic|TestSSEKMSIntegrationBasic" ./test/s3/sse || (echo "$(RED)Basic SSE tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)Basic SSE tests completed successfully!$(NC)"
test: test-basic
@echo "$(YELLOW)Running all S3 SSE integration tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting comprehensive SSE tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSE.*Integration" ./test/s3/sse || (echo "$(RED)SSE tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)All SSE integration tests completed successfully!$(NC)"
test-ssec: check-binary
@echo "$(YELLOW)Running SSE-C integration tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE-C tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSEC.*Integration" ./test/s3/sse || (echo "$(RED)SSE-C tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE-C tests completed successfully!$(NC)"
test-ssekms: check-binary
@echo "$(YELLOW)Running SSE-KMS integration tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE-KMS tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSEKMS.*Integration" ./test/s3/sse || (echo "$(RED)SSE-KMS tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE-KMS tests completed successfully!$(NC)"
test-copy: check-binary
@echo "$(YELLOW)Running SSE copy operation tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE copy tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run ".*CopyIntegration" ./test/s3/sse || (echo "$(RED)SSE copy tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE copy tests completed successfully!$(NC)"
test-multipart: check-binary
@echo "$(YELLOW)Running SSE multipart upload tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE multipart tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSEMultipartUploadIntegration" ./test/s3/sse || (echo "$(RED)SSE multipart tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE multipart tests completed successfully!$(NC)"
test-errors: check-binary
@echo "$(YELLOW)Running SSE error condition tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE error tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSEErrorConditions" ./test/s3/sse || (echo "$(RED)SSE error tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE error tests completed successfully!$(NC)"
test-quick: check-binary
@echo "$(YELLOW)Running quick SSE tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting quick SSE tests...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=5m -run "TestSSECIntegrationBasic|TestSSEKMSIntegrationBasic" ./test/s3/sse || (echo "$(RED)Quick SSE tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)Quick SSE tests completed successfully!$(NC)"
benchmark: check-binary
@echo "$(YELLOW)Running SSE performance benchmarks...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Starting SSE benchmarks...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=30m -bench=. -run=Benchmark ./test/s3/sse || (echo "$(RED)SSE benchmarks failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE benchmarks completed!$(NC)"
# Debug targets
debug-logs:
@echo "$(YELLOW)=== Master Log ===$(NC)"
@tail -n 50 /tmp/seaweedfs-sse-master.log || echo "No master log found"
@echo "$(YELLOW)=== Volume Log ===$(NC)"
@tail -n 50 /tmp/seaweedfs-sse-volume.log || echo "No volume log found"
@echo "$(YELLOW)=== Filer Log ===$(NC)"
@tail -n 50 /tmp/seaweedfs-sse-filer.log || echo "No filer log found"
@echo "$(YELLOW)=== S3 Log ===$(NC)"
@tail -n 50 /tmp/seaweedfs-sse-s3.log || echo "No S3 log found"
debug-status:
@echo "$(YELLOW)=== Process Status ===$(NC)"
@ps aux | grep -E "(weed|seaweedfs)" | grep -v grep || echo "No SeaweedFS processes found"
@echo "$(YELLOW)=== Port Status ===$(NC)"
@netstat -an | grep -E "($(MASTER_PORT)|$(VOLUME_PORT)|$(FILER_PORT)|$(S3_PORT))" || echo "No ports in use"
# Manual test targets for development
manual-start: start-seaweedfs
@echo "$(GREEN)SeaweedFS with SSE support is now running for manual testing$(NC)"
@echo "You can now run SSE tests manually or use S3 clients to test SSE functionality"
@echo "Run 'make manual-stop' when finished"
manual-stop: stop-seaweedfs clean
# CI/CD targets
ci-test: test-quick
# Stress test
stress: check-binary
@echo "$(YELLOW)Running SSE stress tests...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=60m -run="TestSSE.*Integration" -count=5 ./test/s3/sse || (echo "$(RED)SSE stress tests failed$(NC)" && $(MAKE) stop-seaweedfs-safe && exit 1)
@$(MAKE) stop-seaweedfs-safe
@echo "$(GREEN)SSE stress tests completed!$(NC)"
# Performance test with various data sizes
perf: check-binary
@echo "$(YELLOW)Running SSE performance tests with various data sizes...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=60m -run=".*VariousDataSizes" ./test/s3/sse || (echo "$(RED)SSE performance tests failed$(NC)" && $(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe && exit 1)
@$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe
@echo "$(GREEN)SSE performance tests completed!$(NC)"
# Test specific scenarios that would catch the metadata bug
test-metadata-persistence: check-binary
@echo "$(YELLOW)Running SSE metadata persistence tests (would catch filer metadata bugs)...$(NC)"
@$(MAKE) start-seaweedfs-ci
@sleep 5
@echo "$(GREEN)Testing that SSE metadata survives full PUT/GET cycle...$(NC)"
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSECIntegrationBasic" ./test/s3/sse || (echo "$(RED)SSE metadata persistence tests failed$(NC)" && $(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe && exit 1)
@$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe
@echo "$(GREEN)SSE metadata persistence tests completed successfully!$(NC)"
@echo "$(GREEN)✅ These tests would have caught the filer metadata storage bug!$(NC)"
# GitHub Actions compatible test-with-server target that handles server lifecycle
test-with-server: build-weed
@echo "🚀 Starting SSE integration tests with automated server management..."
@echo "Starting SeaweedFS cluster..."
@# Use the CI-safe startup directly without aggressive cleanup
@if $(MAKE) start-seaweedfs-ci > weed-test.log 2>&1; then \
echo "✅ SeaweedFS cluster started successfully"; \
echo "Running SSE integration tests..."; \
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
if [ -n "$(TEST_PATTERN)" ]; then \
echo "🔍 Running tests matching pattern: $(TEST_PATTERN)"; \
cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "$(TEST_PATTERN)" ./test/s3/sse || exit 1; \
else \
echo "🔍 Running all SSE integration tests"; \
cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSE.*Integration" ./test/s3/sse || exit 1; \
fi; \
echo "✅ All tests completed successfully"; \
$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true; \
else \
echo "❌ Failed to start SeaweedFS cluster"; \
echo "=== Server startup logs ==="; \
tail -100 weed-test.log 2>/dev/null || echo "No startup log available"; \
echo "=== System information ==="; \
ps aux | grep -E "weed|make" | grep -v grep || echo "No relevant processes found"; \
exit 1; \
fi
# CI-safe server startup that avoids process conflicts
start-seaweedfs-ci: check-binary
@echo "$(YELLOW)Starting SeaweedFS server for CI testing...$(NC)"
# Create necessary directories
@mkdir -p /tmp/seaweedfs-test-sse
# Clean up any old server logs
@rm -f /tmp/seaweedfs-sse-*.log || true
# Create S3 JSON configuration with KMS (Local provider) and basic identity for embedded S3
@sed -e 's/ACCESS_KEY_PLACEHOLDER/$(ACCESS_KEY)/g' \
-e 's/SECRET_KEY_PLACEHOLDER/$(SECRET_KEY)/g' \
s3-config-template.json > /tmp/seaweedfs-s3.json
# Start weed mini with embedded S3 using the JSON config (with verbose logging)
# WEED_S3_SSE_KEY enables SSE-S3 encryption for testing (KEK derived via HKDF)
@echo "Starting weed mini with embedded S3..."
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) WEED_S3_SSE_KEY=test-sse-s3-key GLOG_v=4 $(SEAWEEDFS_BINARY) mini \
-dir=/tmp/seaweedfs-test-sse \
-s3.port=$(S3_PORT) \
-s3.config=/tmp/seaweedfs-s3.json \
-ip=127.0.0.1 \
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
@echo "Checking S3 service is ready..."
@for i in $$(seq 1 30); do \
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
echo "✅ S3 service is ready"; \
break; \
fi; \
sleep 1; \
done
# GitHub Actions compatible quick test subset
test-quick-with-server: build-weed
@echo "🚀 Starting quick SSE tests with automated server management..."
@trap 'make stop-seaweedfs-safe || true' EXIT; \
echo "Starting SeaweedFS cluster..."; \
if make start-seaweedfs-ci > weed-test.log 2>&1; then \
echo "✅ SeaweedFS cluster started successfully"; \
echo "Running quick SSE integration tests..."; \
cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) -run "TestSSECIntegrationBasic|TestSSEKMSIntegrationBasic|TestSimpleSSECIntegration" ./test/s3/sse || exit 1; \
echo "✅ Quick tests completed successfully"; \
make stop-seaweedfs-safe || true; \
else \
echo "❌ Failed to start SeaweedFS cluster"; \
echo "=== Server startup logs ==="; \
tail -50 weed-test.log; \
exit 1; \
fi
# Help target - extended version
help-extended:
@echo "Available targets:"
@echo " test - Run all SSE integration tests (requires running server)"
@echo " test-with-server - Run all tests with automatic server management (GitHub Actions compatible)"
@echo " test-quick-with-server - Run quick tests with automatic server management"
@echo " test-ssec - Run only SSE-C tests"
@echo " test-ssekms - Run only SSE-KMS tests"
@echo " test-copy - Run only copy operation tests"
@echo " test-multipart - Run only multipart upload tests"
@echo " benchmark - Run performance benchmarks"
@echo " perf - Run performance tests with various data sizes"
@echo " test-metadata-persistence - Test metadata persistence (catches filer bugs)"
@echo " build-weed - Build SeaweedFS binary"
@echo " check-binary - Check if SeaweedFS binary exists"
@echo " start-seaweedfs - Start SeaweedFS cluster"
@echo " start-seaweedfs-ci - Start SeaweedFS cluster (CI-safe version)"
@echo " stop-seaweedfs - Stop SeaweedFS cluster"
@echo " stop-seaweedfs-safe - Stop SeaweedFS cluster (CI-safe version)"
@echo " clean - Clean up test artifacts"
@echo " debug-logs - Show recent logs from all services"
@echo ""
@echo "Environment Variables:"
@echo " ACCESS_KEY - S3 access key (default: some_access_key1)"
@echo " SECRET_KEY - S3 secret key (default: some_secret_key1)"
@echo " KMS_KEY_ID - KMS key ID for SSE-KMS (default: test-key-123)"
@echo " KMS_TYPE - KMS type (default: local)"
@echo " VOLUME_MAX_SIZE_MB - Volume maximum size in MB (default: 50)"
@echo " TEST_TIMEOUT - Test timeout (default: 15m)"
####################################################
# KMS Integration Testing with OpenBao
####################################################
setup-openbao:
@echo "$(YELLOW)Setting up OpenBao for SSE-KMS testing...$(NC)"
@$(DOCKER_COMPOSE) up -d openbao
@sleep 10
@echo "$(YELLOW)Configuring OpenBao...$(NC)"
@OPENBAO_ADDR=$(OPENBAO_ADDR) OPENBAO_TOKEN=$(OPENBAO_TOKEN) ./setup_openbao_sse.sh
@echo "$(GREEN)✅ OpenBao setup complete!$(NC)"
start-full-stack: setup-openbao
@echo "$(YELLOW)Starting full SeaweedFS + KMS stack...$(NC)"
@$(DOCKER_COMPOSE) up -d
@echo "$(YELLOW)Waiting for services to be ready...$(NC)"
@sleep 15
@echo "$(GREEN)✅ Full stack running!$(NC)"
@echo "OpenBao: $(OPENBAO_ADDR)"
@echo "S3 API: http://localhost:$(S3_PORT)"
stop-full-stack:
@echo "$(YELLOW)Stopping full stack...$(NC)"
@$(DOCKER_COMPOSE) down
@echo "$(GREEN)✅ Full stack stopped$(NC)"
test-with-kms: start-full-stack
@echo "$(YELLOW)Running SSE integration tests with real KMS...$(NC)"
@sleep 5 # Extra time for KMS initialization
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) ./test/s3/sse -run "SSE.*Integration" || (echo "$(RED)Tests failed$(NC)" && make stop-full-stack && exit 1)
@echo "$(GREEN)✅ All KMS integration tests passed!$(NC)"
@make stop-full-stack
test-ssekms-integration: start-full-stack
@echo "$(YELLOW)Running SSE-KMS integration tests with OpenBao...$(NC)"
@sleep 5 # Extra time for KMS initialization
@cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) ./test/s3/sse -run "TestSSEKMS.*Integration" || (echo "$(RED)SSE-KMS tests failed$(NC)" && make stop-full-stack && exit 1)
@echo "$(GREEN)✅ SSE-KMS integration tests passed!$(NC)"
@make stop-full-stack
clean-kms:
@echo "$(YELLOW)Cleaning up KMS test environment...$(NC)"
@$(DOCKER_COMPOSE) down -v --remove-orphans || true
@docker system prune -f || true
@echo "$(GREEN)✅ KMS environment cleaned up!$(NC)"
status-kms:
@echo "$(YELLOW)KMS Environment Status:$(NC)"
@$(DOCKER_COMPOSE) ps
@echo ""
@echo "$(YELLOW)OpenBao Health:$(NC)"
@curl -s $(OPENBAO_ADDR)/v1/sys/health | jq '.' || echo "OpenBao not accessible"
@echo ""
@echo "$(YELLOW)S3 API Status:$(NC)"
@curl -s http://localhost:$(S3_PORT) || echo "S3 API not accessible"
# Quick test with just basic KMS functionality
test-kms-quick: setup-openbao
@echo "$(YELLOW)Running quick KMS functionality test...$(NC)"
@cd ../../../test/kms && make dev-test
@echo "$(GREEN)✅ Quick KMS test passed!$(NC)"
# Development targets
dev-kms: setup-openbao
@echo "$(GREEN)Development environment ready$(NC)"
@echo "OpenBao: $(OPENBAO_ADDR)"
@echo "Token: $(OPENBAO_TOKEN)"
@echo "Use 'make test-ssekms-integration' to run tests"
# Volume encryption integration tests
test-volume-encryption: build-weed
@echo "🚀 Starting S3 volume encryption integration tests..."
@echo "Starting SeaweedFS cluster with volume encryption enabled..."
@# Start server with -s3.encryptVolumeData flag
@mkdir -p /tmp/seaweedfs-test-sse
@rm -f /tmp/seaweedfs-sse-*.log || true
@sed -e 's/ACCESS_KEY_PLACEHOLDER/$(ACCESS_KEY)/g' \
-e 's/SECRET_KEY_PLACEHOLDER/$(SECRET_KEY)/g' \
s3-config-template.json > /tmp/seaweedfs-s3.json
@echo "Starting weed mini with S3 volume encryption..."
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) WEED_S3_SSE_KEY=test-sse-s3-key GLOG_v=4 $(SEAWEEDFS_BINARY) mini \
-dir=/tmp/seaweedfs-test-sse \
-s3.port=$(S3_PORT) \
-s3.config=/tmp/seaweedfs-s3.json \
-s3.encryptVolumeData \
-ip=127.0.0.1 \
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
@echo "Checking S3 service is ready..."
@for i in $$(seq 1 30); do \
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
echo "✅ S3 service is ready"; \
break; \
fi; \
sleep 1; \
done
@echo "Running volume encryption integration tests..."
@trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
cd $(SEAWEEDFS_ROOT) && go test -v -tags=integration -timeout=10m -run "TestS3VolumeEncryption" ./test/s3/sse || exit 1; \
echo "✅ Volume encryption tests completed successfully"; \
$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true

253
test/s3/sse/README.md Normal file
View File

@@ -0,0 +1,253 @@
# S3 Server-Side Encryption (SSE) Integration Tests
This directory contains comprehensive integration tests for SeaweedFS S3 API Server-Side Encryption functionality. These tests validate the complete end-to-end encryption/decryption pipeline from S3 API requests through filer metadata storage.
## Overview
The SSE integration tests cover three main encryption methods:
- **SSE-C (Customer-Provided Keys)**: Client provides encryption keys via request headers
- **SSE-KMS (Key Management Service)**: Server manages encryption keys through a KMS provider
- **SSE-S3 (Server-Managed Keys)**: Server automatically manages encryption keys
### 🆕 Real KMS Integration
The tests now include **real KMS integration** with OpenBao, providing:
- ✅ Actual encryption/decryption operations (not mock keys)
- ✅ Multiple KMS keys for different security levels
- ✅ Per-bucket KMS configuration testing
- ✅ Performance benchmarking with real KMS operations
See [README_KMS.md](README_KMS.md) for detailed KMS integration documentation.
## Why Integration Tests Matter
These integration tests were created to address a **critical gap in test coverage** that previously existed. While the SeaweedFS codebase had comprehensive unit tests for SSE components, it lacked integration tests that validated the complete request flow:
```
Client Request → S3 API → Filer Storage → Metadata Persistence → Retrieval → Decryption
```
### The Bug These Tests Would Have Caught
A critical bug was discovered where:
- ✅ S3 API correctly encrypted data and sent metadata headers to the filer
- ❌ **Filer did not process SSE metadata headers**, losing all encryption metadata
- ❌ Objects could be encrypted but **never decrypted** (metadata was lost)
**Unit tests passed** because they tested components in isolation, but the **integration was broken**. These integration tests specifically validate that:
1. Encryption metadata is correctly sent to the filer
2. Filer properly processes and stores the metadata
3. Objects can be successfully retrieved and decrypted
4. Copy operations preserve encryption metadata
5. Multipart uploads maintain encryption consistency
## Test Structure
### Core Integration Tests
#### Basic Functionality
- `TestSSECIntegrationBasic` - Basic SSE-C PUT/GET cycle
- `TestSSEKMSIntegrationBasic` - Basic SSE-KMS PUT/GET cycle
#### Data Size Validation
- `TestSSECIntegrationVariousDataSizes` - SSE-C with various data sizes (0B to 1MB)
- `TestSSEKMSIntegrationVariousDataSizes` - SSE-KMS with various data sizes
#### Object Copy Operations
- `TestSSECObjectCopyIntegration` - SSE-C object copying (key rotation, encryption changes)
- `TestSSEKMSObjectCopyIntegration` - SSE-KMS object copying
#### Multipart Uploads
- `TestSSEMultipartUploadIntegration` - SSE multipart uploads for large objects
#### Error Conditions
- `TestSSEErrorConditions` - Invalid keys, malformed requests, error handling
### Performance Tests
- `BenchmarkSSECThroughput` - SSE-C performance benchmarking
- `BenchmarkSSEKMSThroughput` - SSE-KMS performance benchmarking
## Running Tests
### Prerequisites
1. **Build SeaweedFS**: Ensure the `weed` binary is built and available in PATH
```bash
cd /path/to/seaweedfs
make
```
2. **Dependencies**: Tests use AWS SDK Go v2 and testify - these are handled by Go modules
### Quick Test
Run basic SSE integration tests:
```bash
make test-basic
```
### Comprehensive Testing
Run all SSE integration tests:
```bash
make test
```
### Specific Test Categories
```bash
make test-ssec # SSE-C tests only
make test-ssekms # SSE-KMS tests only
make test-copy # Copy operation tests
make test-multipart # Multipart upload tests
make test-errors # Error condition tests
```
### Performance Testing
```bash
make benchmark # Performance benchmarks
make perf # Various data size performance tests
```
### KMS Integration Testing
```bash
make setup-openbao # Set up OpenBao KMS
make test-with-kms # Run all SSE tests with real KMS
make test-ssekms-integration # Run SSE-KMS with OpenBao only
make clean-kms # Clean up KMS environment
```
### Development Testing
```bash
make manual-start # Start SeaweedFS for manual testing
# ... run manual tests ...
make manual-stop # Stop and cleanup
```
## Test Configuration
### Default Configuration
The tests use these default settings:
- **S3 Endpoint**: `http://127.0.0.1:8333`
- **Access Key**: `some_access_key1`
- **Secret Key**: `some_secret_key1`
- **Region**: `us-east-1`
- **Bucket Prefix**: `test-sse-`
### Custom Configuration
Override defaults via environment variables:
```bash
S3_PORT=8444 FILER_PORT=8889 make test
```
### Test Environment
Each test run:
1. Starts a complete SeaweedFS cluster (master, volume, filer, s3)
2. Configures KMS support for SSE-KMS tests
3. Creates temporary buckets with unique names
4. Runs tests with real HTTP requests
5. Cleans up all test artifacts
## Test Data Coverage
### Data Sizes Tested
- **0 bytes**: Empty files (edge case)
- **1 byte**: Minimal data
- **16 bytes**: Single AES block
- **31 bytes**: Just under two blocks
- **32 bytes**: Exactly two blocks
- **100 bytes**: Small file
- **1 KB**: Small text file
- **8 KB**: Medium file
- **64 KB**: Large file
- **1 MB**: Very large file
### Encryption Key Scenarios
- **SSE-C**: Random 256-bit keys, key rotation, wrong keys
- **SSE-KMS**: Various key IDs, encryption contexts, bucket keys
- **Copy Operations**: Same key, different keys, encryption transitions
## Critical Test Scenarios
### Metadata Persistence Validation
The integration tests specifically validate scenarios that would catch metadata storage bugs:
```go
// 1. Upload with SSE-C
client.PutObject(..., SSECustomerKey: key) // ← Metadata sent to filer
// 2. Retrieve with SSE-C
client.GetObject(..., SSECustomerKey: key) // ← Metadata retrieved from filer
// 3. Verify decryption works
assert.Equal(originalData, decryptedData) // ← Would fail if metadata lost
```
### Content-Length Validation
Tests verify that Content-Length headers are correct, which would catch bugs related to IV handling:
```go
assert.Equal(int64(originalSize), resp.ContentLength) // ← Would catch IV-in-stream bugs
```
## Debugging
### View Logs
```bash
make debug-logs # Show recent log entries
make debug-status # Show process and port status
```
### Manual Testing
```bash
make manual-start # Start SeaweedFS
# Test with S3 clients, curl, etc.
make manual-stop # Cleanup
```
## Integration Test Benefits
These integration tests provide:
1. **End-to-End Validation**: Complete request pipeline testing
2. **Metadata Persistence**: Validates filer storage/retrieval of encryption metadata
3. **Real Network Communication**: Uses actual HTTP requests and responses
4. **Production-Like Environment**: Full SeaweedFS cluster with all components
5. **Regression Protection**: Prevents critical integration bugs
6. **Performance Baselines**: Benchmarking for performance monitoring
## Continuous Integration
For CI/CD pipelines, use:
```bash
make ci-test # Quick tests suitable for CI
make stress # Stress testing for stability validation
```
## Key Differences from Unit Tests
| Aspect | Unit Tests | Integration Tests |
|--------|------------|------------------|
| **Scope** | Individual functions | Complete request pipeline |
| **Dependencies** | Mocked/simulated | Real SeaweedFS cluster |
| **Network** | None | Real HTTP requests |
| **Storage** | In-memory | Real filer database |
| **Metadata** | Manual simulation | Actual storage/retrieval |
| **Speed** | Fast (milliseconds) | Slower (seconds) |
| **Coverage** | Component logic | System integration |
## Conclusion
These integration tests ensure that SeaweedFS SSE functionality works correctly in production-like environments. They complement the existing unit tests by validating that all components work together properly, providing confidence that encryption/decryption operations will succeed for real users.
**Most importantly**, these tests would have immediately caught the critical filer metadata storage bug that was previously undetected, demonstrating the crucial importance of integration testing for distributed systems.

245
test/s3/sse/README_KMS.md Normal file
View File

@@ -0,0 +1,245 @@
# SeaweedFS S3 SSE-KMS Integration with OpenBao
This directory contains comprehensive integration tests for SeaweedFS S3 Server-Side Encryption with Key Management Service (SSE-KMS) using OpenBao as the KMS provider.
## 🎯 Overview
The integration tests verify that SeaweedFS can:
- ✅ **Encrypt data** using real KMS operations (not mock keys)
- ✅ **Decrypt data** correctly with proper key management
- ✅ **Handle multiple KMS keys** for different security levels
- ✅ **Support various data sizes** (0 bytes to 1MB+)
- ✅ **Maintain data integrity** through encryption/decryption cycles
- ✅ **Work with per-bucket KMS configuration**
## 🏗️ Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ S3 Client │ │ SeaweedFS │ │ OpenBao │
│ │ │ S3 API │ │ KMS │
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
│ PUT /object │───▶│ SSE-KMS Handler │───▶│ GenerateDataKey │
│ SSEKMSKeyId: │ │ │ │ Encrypt │
│ "test-key-123" │ │ KMS Provider: │ │ Decrypt │
│ │ │ OpenBao │ │ Transit Engine │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
## 🚀 Quick Start
### 1. Set up OpenBao KMS
```bash
# Start OpenBao and create encryption keys
make setup-openbao
```
### 2. Run SSE-KMS Integration Tests
```bash
# Run all SSE-KMS tests with real KMS
make test-ssekms-integration
# Or run the full integration suite
make test-with-kms
```
### 3. Check KMS Status
```bash
# Verify OpenBao and SeaweedFS are running
make status-kms
```
## 📋 Available Test Targets
| Target | Description |
|--------|-------------|
| `setup-openbao` | Set up OpenBao KMS with test encryption keys |
| `test-with-kms` | Run all SSE tests with real KMS integration |
| `test-ssekms-integration` | Run only SSE-KMS tests with OpenBao |
| `start-full-stack` | Start SeaweedFS + OpenBao with Docker Compose |
| `stop-full-stack` | Stop all Docker services |
| `clean-kms` | Clean up KMS test environment |
| `status-kms` | Check status of KMS and S3 services |
| `dev-kms` | Set up development environment |
## 🔑 KMS Keys Created
The setup automatically creates these encryption keys in OpenBao:
| Key Name | Purpose |
|----------|---------|
| `test-key-123` | Basic SSE-KMS integration tests |
| `source-test-key-123` | Copy operation source key |
| `dest-test-key-456` | Copy operation destination key |
| `test-multipart-key` | Multipart upload tests |
| `test-kms-range-key` | Range request tests |
| `seaweedfs-test-key` | General SeaweedFS SSE tests |
| `bucket-default-key` | Default bucket encryption |
| `high-security-key` | High security scenarios |
| `performance-key` | Performance testing |
## 🧪 Test Coverage
### Basic SSE-KMS Operations
- ✅ PUT object with SSE-KMS encryption
- ✅ GET object with automatic decryption
- ✅ HEAD object metadata verification
- ✅ Multiple KMS key support
- ✅ Various data sizes (0B - 1MB)
### Advanced Scenarios
- ✅ Large file encryption (chunked)
- ✅ Range requests with encrypted data
- ✅ Per-bucket KMS configuration
- ✅ Error handling for invalid keys
- ⚠️ Object copy operations (known issue)
### Performance Testing
- ✅ KMS operation benchmarks
- ✅ Encryption/decryption latency
- ✅ Throughput with various data sizes
## ⚙️ Configuration
### S3 KMS Configuration (`s3_kms.json`)
```json
{
"kms": {
"default_provider": "openbao-test",
"providers": {
"openbao-test": {
"type": "openbao",
"address": "http://openbao:8200",
"token": "root-token-for-testing",
"transit_path": "transit"
}
},
"buckets": {
"test-sse-kms-basic": {
"provider": "openbao-test"
}
}
}
}
```
### Docker Compose Services
- **OpenBao**: KMS provider on port 8200
- **SeaweedFS Master**: Metadata management on port 9333
- **SeaweedFS Volume**: Data storage on port 8080
- **SeaweedFS Filer**: S3 API with KMS on port 8333
## 🎛️ Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `OPENBAO_ADDR` | `http://127.0.0.1:8200` | OpenBao server address |
| `OPENBAO_TOKEN` | `root-token-for-testing` | OpenBao root token |
| `S3_PORT` | `8333` | S3 API port |
| `TEST_TIMEOUT` | `15m` | Test timeout duration |
## 📊 Example Test Run
```bash
$ make test-ssekms-integration
Setting up OpenBao for SSE-KMS testing...
✅ OpenBao setup complete!
Starting full SeaweedFS + KMS stack...
✅ Full stack running!
Running SSE-KMS integration tests with OpenBao...
=== RUN TestSSEKMSIntegrationBasic
=== RUN TestSSEKMSOpenBaoIntegration
=== RUN TestSSEKMSOpenBaoAvailability
--- PASS: TestSSEKMSIntegrationBasic (0.26s)
--- PASS: TestSSEKMSOpenBaoIntegration (0.45s)
--- PASS: TestSSEKMSOpenBaoAvailability (0.12s)
✅ SSE-KMS integration tests passed!
```
## 🔍 Troubleshooting
### OpenBao Not Starting
```bash
# Check OpenBao logs
docker-compose logs openbao
# Verify port availability
lsof -ti :8200
```
### SeaweedFS KMS Not Working
```bash
# Check filer logs for KMS errors
docker-compose logs seaweedfs-filer
# Verify KMS configuration
curl http://localhost:8200/v1/sys/health
```
### Tests Failing
```bash
# Run specific test for debugging
cd ../../../ && go test -v -timeout=30s -run TestSSEKMSOpenBaoAvailability ./test/s3/sse
# Check service status
make status-kms
```
## 🚧 Known Issues
1. **Object Copy Operations**: Currently failing due to data corruption in copy logic (not KMS-related)
2. **Azure SDK Compatibility**: Azure KMS provider disabled due to SDK issues
3. **Network Timing**: Some tests may need longer startup delays in slow environments
## 🔄 Development Workflow
### 1. Development Setup
```bash
# Quick setup for development
make dev-kms
# Run specific test during development
go test -v -run TestSSEKMSOpenBaoAvailability ./test/s3/sse
```
### 2. Integration Testing
```bash
# Full integration test cycle
make clean-kms # Clean environment
make test-with-kms # Run comprehensive tests
make clean-kms # Clean up
```
### 3. Performance Testing
```bash
# Run KMS performance benchmarks
cd ../kms && make test-benchmark
```
## 📈 Performance Characteristics
From benchmark results:
- **GenerateDataKey**: ~55,886 ns/op (~18,000 ops/sec)
- **Decrypt**: ~48,009 ns/op (~21,000 ops/sec)
- **End-to-end encryption**: Sub-second for files up to 1MB
## 🔗 Related Documentation
- [SeaweedFS S3 API Documentation](https://github.com/seaweedfs/seaweedfs/wiki/Amazon-S3-API)
- [OpenBao Transit Secrets Engine](https://github.com/openbao/openbao/blob/main/website/content/docs/secrets/transit.md)
- [AWS S3 Server-Side Encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html)
## 🎉 Success Criteria
The integration is considered successful when:
- ✅ OpenBao KMS provider initializes correctly
- ✅ Encryption keys are created and accessible
- ✅ Data can be encrypted and decrypted reliably
- ✅ Multiple key types work independently
- ✅ Performance meets production requirements
- ✅ Error cases are handled gracefully
This integration demonstrates that SeaweedFS SSE-KMS is **production-ready** with real KMS providers! 🚀

View File

@@ -0,0 +1,100 @@
services:
# OpenBao server for KMS integration testing
openbao:
image: ghcr.io/openbao/openbao:latest
ports:
- "8200:8200"
environment:
- BAO_DEV_ROOT_TOKEN_ID=root-token-for-testing
- BAO_DEV_LISTEN_ADDRESS=0.0.0.0:8200
- BAO_LOCAL_CONFIG={"backend":{"file":{"path":"/bao/data"}},"default_lease_ttl":"168h","max_lease_ttl":"720h","ui":true,"disable_mlock":true}
command:
- bao
- server
- -dev
- -dev-root-token-id=root-token-for-testing
- -dev-listen-address=0.0.0.0:8200
volumes:
- openbao-data:/bao/data
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8200/v1/sys/health"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
networks:
- seaweedfs-sse-test
# SeaweedFS Master
seaweedfs-master:
image: chrislusf/seaweedfs:latest
ports:
- "9333:9333"
- "19333:19333"
command:
- master
- -ip=seaweedfs-master
- -port=9333
- -port.grpc=19333
- -volumeSizeLimitMB=50
- -mdir=/data
volumes:
- seaweedfs-master-data:/data
networks:
- seaweedfs-sse-test
# SeaweedFS Volume Server
seaweedfs-volume:
image: chrislusf/seaweedfs:latest
ports:
- "8080:8080"
command:
- volume
- -master=seaweedfs-master:9333
- -port=8080
- -ip=seaweedfs-volume
- -publicUrl=seaweedfs-volume:8080
- -dir=/data
- -max=100
depends_on:
- seaweedfs-master
volumes:
- seaweedfs-volume-data:/data
networks:
- seaweedfs-sse-test
# SeaweedFS Filer with S3 API and KMS configuration
seaweedfs-filer:
image: chrislusf/seaweedfs:latest
ports:
- "8888:8888" # Filer HTTP
- "18888:18888" # Filer gRPC
- "8333:8333" # S3 API
command:
- filer
- -master=seaweedfs-master:9333
- -port=8888
- -port.grpc=18888
- -ip=seaweedfs-filer
- -s3
- -s3.port=8333
- -s3.config=/etc/seaweedfs/s3.json
depends_on:
- seaweedfs-master
- seaweedfs-volume
- openbao
volumes:
- ./s3_kms.json:/etc/seaweedfs/s3.json
- seaweedfs-filer-data:/data
networks:
- seaweedfs-sse-test
volumes:
openbao-data:
seaweedfs-master-data:
seaweedfs-volume-data:
seaweedfs-filer-data:
networks:
seaweedfs-sse-test:
name: seaweedfs-sse-test

View File

@@ -0,0 +1,504 @@
package sse_test
import (
"bytes"
"context"
"fmt"
"io"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestGitHub7562CopyFromEncryptedToTempToEncrypted reproduces the exact scenario from
// GitHub issue #7562: copying from an encrypted bucket to a temp bucket, then to another
// encrypted bucket fails with InternalError.
//
// Reproduction steps:
// 1. Create source bucket with SSE-S3 encryption enabled
// 2. Upload object (automatically encrypted)
// 3. Create temp bucket (no encryption)
// 4. Copy object from source to temp (decrypts)
// 5. Delete source bucket
// 6. Create destination bucket with SSE-S3 encryption
// 7. Copy object from temp to dest (should re-encrypt) - THIS FAILS
func TestGitHub7562CopyFromEncryptedToTempToEncrypted(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
// Create three buckets
srcBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-src-")
require.NoError(t, err, "Failed to create source bucket")
tempBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-temp-")
require.NoError(t, err, "Failed to create temp bucket")
destBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-dest-")
require.NoError(t, err, "Failed to create destination bucket")
// Cleanup at the end
defer func() {
// Clean up in reverse order of creation
cleanupTestBucket(ctx, client, destBucket)
cleanupTestBucket(ctx, client, tempBucket)
// Note: srcBucket is deleted during the test
}()
testData := []byte("Test data for GitHub issue #7562 - copy from encrypted to temp to encrypted bucket")
objectKey := "demo-file.txt"
t.Logf("[1] Creating source bucket with SSE-S3 default encryption: %s", srcBucket)
// Step 1: Enable SSE-S3 default encryption on source bucket
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(srcBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set source bucket default encryption")
t.Log("[2] Uploading demo object to source bucket")
// Step 2: Upload object to source bucket (will be automatically encrypted)
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
// No encryption header - bucket default applies
})
require.NoError(t, err, "Failed to upload to source bucket")
// Verify source object is encrypted
srcHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD source object")
assert.Equal(t, types.ServerSideEncryptionAes256, srcHead.ServerSideEncryption,
"Source object should be SSE-S3 encrypted")
t.Logf("Source object encryption: %v", srcHead.ServerSideEncryption)
t.Logf("[3] Creating temp bucket (no encryption): %s", tempBucket)
// Temp bucket already created without encryption
t.Log("[4] Copying object from source to temp (should decrypt)")
// Step 4: Copy to temp bucket (no encryption = decrypts)
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", srcBucket, objectKey)),
// No encryption header - data stored unencrypted
})
require.NoError(t, err, "Failed to copy to temp bucket")
// Verify temp object is NOT encrypted
tempHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD temp object")
assert.Empty(t, tempHead.ServerSideEncryption, "Temp object should NOT be encrypted")
t.Logf("Temp object encryption: %v (should be empty)", tempHead.ServerSideEncryption)
// Verify temp object content
tempGet, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to GET temp object")
tempData, err := io.ReadAll(tempGet.Body)
tempGet.Body.Close()
require.NoError(t, err, "Failed to read temp object")
assertDataEqual(t, testData, tempData, "Temp object data should match original")
t.Log("[5] Deleting original source bucket")
// Step 5: Delete source bucket
err = cleanupTestBucket(ctx, client, srcBucket)
require.NoError(t, err, "Failed to delete source bucket")
t.Logf("[6] Creating destination bucket with SSE-S3 encryption: %s", destBucket)
// Step 6: Enable SSE-S3 default encryption on destination bucket
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(destBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set destination bucket default encryption")
t.Log("[7] Copying object from temp to dest (should re-encrypt) - THIS IS WHERE #7562 FAILS")
// Step 7: Copy from temp to dest bucket (should re-encrypt with SSE-S3)
// THIS IS THE STEP THAT FAILS IN GITHUB ISSUE #7562
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)),
// No encryption header - bucket default should apply
})
require.NoError(t, err, "GitHub #7562: Failed to copy from temp to encrypted dest bucket")
// Verify destination object is encrypted
destHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD destination object")
assert.Equal(t, types.ServerSideEncryptionAes256, destHead.ServerSideEncryption,
"Destination object should be SSE-S3 encrypted via bucket default")
t.Logf("Destination object encryption: %v", destHead.ServerSideEncryption)
// Verify destination object content is correct
destGet, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to GET destination object")
destData, err := io.ReadAll(destGet.Body)
destGet.Body.Close()
require.NoError(t, err, "Failed to read destination object")
assertDataEqual(t, testData, destData, "GitHub #7562: Destination object data mismatch after re-encryption")
t.Log("[done] GitHub #7562 reproduction test completed successfully!")
}
// TestGitHub7562SimpleScenario tests the simpler variant: just copy unencrypted to encrypted bucket
func TestGitHub7562SimpleScenario(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
// Create two buckets
srcBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-simple-src-")
require.NoError(t, err, "Failed to create source bucket")
defer cleanupTestBucket(ctx, client, srcBucket)
destBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-simple-dest-")
require.NoError(t, err, "Failed to create destination bucket")
defer cleanupTestBucket(ctx, client, destBucket)
testData := []byte("Simple test for unencrypted to encrypted copy")
objectKey := "test-object.txt"
t.Logf("Source bucket (no encryption): %s", srcBucket)
t.Logf("Dest bucket (SSE-S3 default): %s", destBucket)
// Upload to unencrypted source bucket
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
})
require.NoError(t, err, "Failed to upload to source bucket")
// Enable SSE-S3 on destination bucket
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(destBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set dest bucket encryption")
// Copy to encrypted bucket (should use bucket default encryption)
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", srcBucket, objectKey)),
})
require.NoError(t, err, "Failed to copy to encrypted bucket")
// Verify destination is encrypted
destHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD dest object")
assert.Equal(t, types.ServerSideEncryptionAes256, destHead.ServerSideEncryption,
"Object should be encrypted via bucket default")
// Verify content
destGet, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to GET dest object")
destData, err := io.ReadAll(destGet.Body)
destGet.Body.Close()
require.NoError(t, err, "Failed to read dest object")
assertDataEqual(t, testData, destData, "Data mismatch")
}
// TestGitHub7562DebugMetadata helps debug what metadata is present on objects at each step
func TestGitHub7562DebugMetadata(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
// Create three buckets
srcBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-debug-src-")
require.NoError(t, err, "Failed to create source bucket")
tempBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-debug-temp-")
require.NoError(t, err, "Failed to create temp bucket")
destBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-debug-dest-")
require.NoError(t, err, "Failed to create destination bucket")
defer func() {
cleanupTestBucket(ctx, client, destBucket)
cleanupTestBucket(ctx, client, tempBucket)
}()
testData := []byte("Debug metadata test for GitHub #7562")
objectKey := "debug-file.txt"
// Enable SSE-S3 on source
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(srcBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set source bucket encryption")
// Upload
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
})
require.NoError(t, err, "Failed to upload")
// Log source object headers
srcHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD source")
t.Logf("=== SOURCE OBJECT (encrypted) ===")
t.Logf("ServerSideEncryption: %v", srcHead.ServerSideEncryption)
t.Logf("Metadata: %v", srcHead.Metadata)
t.Logf("ContentLength: %d", aws.ToInt64(srcHead.ContentLength))
// Copy to temp
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", srcBucket, objectKey)),
})
require.NoError(t, err, "Failed to copy to temp")
// Log temp object headers
tempHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD temp")
t.Logf("=== TEMP OBJECT (should be unencrypted) ===")
t.Logf("ServerSideEncryption: %v (should be empty)", tempHead.ServerSideEncryption)
t.Logf("Metadata: %v", tempHead.Metadata)
t.Logf("ContentLength: %d", aws.ToInt64(tempHead.ContentLength))
// Verify temp is NOT encrypted
if tempHead.ServerSideEncryption != "" {
t.Logf("WARNING: Temp object unexpectedly has encryption: %v", tempHead.ServerSideEncryption)
}
// Delete source bucket
cleanupTestBucket(ctx, client, srcBucket)
// Enable SSE-S3 on dest
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(destBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set dest bucket encryption")
// Copy to dest - THIS IS WHERE #7562 FAILS
t.Log("=== COPYING TO ENCRYPTED DEST ===")
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)),
})
if err != nil {
t.Logf("!!! COPY FAILED (GitHub #7562): %v", err)
t.FailNow()
}
// Log dest object headers
destHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD dest")
t.Logf("=== DEST OBJECT (should be encrypted) ===")
t.Logf("ServerSideEncryption: %v", destHead.ServerSideEncryption)
t.Logf("Metadata: %v", destHead.Metadata)
t.Logf("ContentLength: %d", aws.ToInt64(destHead.ContentLength))
// Verify dest IS encrypted
assert.Equal(t, types.ServerSideEncryptionAes256, destHead.ServerSideEncryption,
"Dest object should be encrypted")
// Verify content is readable
destGet, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to GET dest")
destData, err := io.ReadAll(destGet.Body)
destGet.Body.Close()
require.NoError(t, err, "Failed to read dest")
assertDataEqual(t, testData, destData, "Data mismatch")
t.Log("=== DEBUG TEST PASSED ===")
}
// TestGitHub7562LargeFile tests the issue with larger files that might trigger multipart handling
func TestGitHub7562LargeFile(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
srcBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-large-src-")
require.NoError(t, err, "Failed to create source bucket")
tempBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-large-temp-")
require.NoError(t, err, "Failed to create temp bucket")
destBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"7562-large-dest-")
require.NoError(t, err, "Failed to create destination bucket")
defer func() {
cleanupTestBucket(ctx, client, destBucket)
cleanupTestBucket(ctx, client, tempBucket)
}()
// Use larger file to potentially trigger different code paths
testData := generateTestData(5 * 1024 * 1024) // 5MB
objectKey := "large-file.bin"
t.Logf("Testing with %d byte file", len(testData))
// Enable SSE-S3 on source
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(srcBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set source bucket encryption")
// Upload
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(srcBucket),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
})
require.NoError(t, err, "Failed to upload")
// Copy to temp (decrypt)
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(tempBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", srcBucket, objectKey)),
})
require.NoError(t, err, "Failed to copy to temp")
// Delete source
cleanupTestBucket(ctx, client, srcBucket)
// Enable SSE-S3 on dest
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(destBucket),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set dest bucket encryption")
// Copy to dest (re-encrypt) - GitHub #7562
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)),
})
require.NoError(t, err, "GitHub #7562: Large file copy to encrypted bucket failed")
// Verify
destHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to HEAD dest")
assert.Equal(t, types.ServerSideEncryptionAes256, destHead.ServerSideEncryption)
assert.Equal(t, int64(len(testData)), aws.ToInt64(destHead.ContentLength))
// Verify content
destGet, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(destBucket),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to GET dest")
destData, err := io.ReadAll(destGet.Body)
destGet.Body.Close()
require.NoError(t, err, "Failed to read dest")
assertDataEqual(t, testData, destData, "Large file data mismatch")
t.Log("Large file test passed!")
}

View File

@@ -0,0 +1,23 @@
{
"identities": [
{
"name": "admin",
"credentials": [
{
"accessKey": "ACCESS_KEY_PLACEHOLDER",
"secretKey": "SECRET_KEY_PLACEHOLDER"
}
],
"actions": ["Admin", "Read", "Write"]
}
],
"kms": {
"default_provider": "local-dev",
"providers": {
"local-dev": {
"type": "local",
"enableOnDemandCreate": true
}
}
}
}

41
test/s3/sse/s3_kms.json Normal file
View File

@@ -0,0 +1,41 @@
{
"identities": [
{
"name": "admin",
"credentials": [
{
"accessKey": "some_access_key1",
"secretKey": "some_secret_key1"
}
],
"actions": ["Admin", "Read", "Write"]
}
],
"kms": {
"default_provider": "openbao-test",
"providers": {
"openbao-test": {
"type": "openbao",
"address": "http://openbao:8200",
"token": "root-token-for-testing",
"transit_path": "transit",
"cache_enabled": true,
"cache_ttl": "1h"
}
},
"buckets": {
"test-sse-kms-basic": {
"provider": "openbao-test"
},
"test-sse-kms-multipart": {
"provider": "openbao-test"
},
"test-sse-kms-copy": {
"provider": "openbao-test"
},
"test-sse-kms-range": {
"provider": "openbao-test"
}
}
}
}

View File

@@ -0,0 +1,104 @@
package sse_test
import (
"bytes"
"context"
"fmt"
"io"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPlainObjectRangeAndHeadHeaders ensures non-SSE objects advertise correct
// Content-Length and Content-Range information for both HEAD and ranged GETs.
func TestPlainObjectRangeAndHeadHeaders(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"range-plain-")
require.NoError(t, err, "failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
// SeaweedFS S3 auto-chunks uploads at 8MiB (see chunkSize in putToFiler).
// Using 16MiB ensures at least two chunks without stressing CI resources.
const chunkSize = 8 * 1024 * 1024
const objectSize = 2 * chunkSize
objectKey := "plain-range-validation"
testData := generateTestData(objectSize)
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
})
require.NoError(t, err, "failed to upload test object")
t.Run("HeadObject reports accurate Content-Length", func(t *testing.T) {
resp, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
require.NoError(t, err, "HeadObject request failed")
assert.Equal(t, int64(objectSize), resp.ContentLength, "Content-Length mismatch on HEAD")
assert.Equal(t, "bytes", aws.ToString(resp.AcceptRanges), "Accept-Ranges should advertise bytes")
})
t.Run("Range request across chunk boundary", func(t *testing.T) {
// Test range that spans an 8MiB chunk boundary (chunkSize - 1KB to chunkSize + 3KB)
rangeStart := int64(chunkSize - 1024)
rangeEnd := rangeStart + 4096 - 1
rangeHeader := fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd)
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Range: aws.String(rangeHeader),
})
require.NoError(t, err, "GetObject range request failed")
defer resp.Body.Close()
expectedLen := rangeEnd - rangeStart + 1
assert.Equal(t, expectedLen, resp.ContentLength, "Content-Length must match requested range size")
assert.Equal(t,
fmt.Sprintf("bytes %d-%d/%d", rangeStart, rangeEnd, objectSize),
aws.ToString(resp.ContentRange),
"Content-Range header mismatch")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "failed to read range response body")
assert.Equal(t, int(expectedLen), len(body), "actual bytes read mismatch")
assert.Equal(t, testData[rangeStart:rangeEnd+1], body, "range payload mismatch")
})
t.Run("Suffix range request", func(t *testing.T) {
const suffixSize = 2048
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Range: aws.String(fmt.Sprintf("bytes=-%d", suffixSize)),
})
require.NoError(t, err, "GetObject suffix range request failed")
defer resp.Body.Close()
expectedStart := int64(objectSize - suffixSize)
expectedEnd := int64(objectSize - 1)
expectedLen := expectedEnd - expectedStart + 1
assert.Equal(t, expectedLen, resp.ContentLength, "suffix Content-Length mismatch")
assert.Equal(t,
fmt.Sprintf("bytes %d-%d/%d", expectedStart, expectedEnd, objectSize),
aws.ToString(resp.ContentRange),
"suffix Content-Range mismatch")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "failed to read suffix range response body")
assert.Equal(t, int(expectedLen), len(body), "suffix range byte count mismatch")
assert.Equal(t, testData[expectedStart:expectedEnd+1], body, "suffix range payload mismatch")
})
}

View File

@@ -0,0 +1,310 @@
package sse_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"sync"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
)
// TestSSES3ConcurrentMultipartDigestIntegration mirrors the production
// scenario reported in issue #8908: Docker Registry pushes several large
// container images in parallel, each push being itself a multipart upload of
// many ~5MB parts under bucket-default SSE-S3. Registry pulls fail with
// "Digest did not match" because the SHA-256 of the streamed GET body does
// not match the SHA-256 of what was uploaded.
//
// Coverage that the existing TestSSEMultipartManyChunksIntegration does NOT
// have:
// - bucket-default SSE-S3 (not explicit per-request headers)
// - multiple objects uploaded concurrently
// - parts within each object uploaded concurrently
// - both full-body GET (Docker Registry) and chunked range GETs (kubelet/CRI)
//
// If the SHA over the streamed body ever differs from the SHA over the
// uploaded bytes, the test fails with the exact bucket/key, expected SHA,
// and actual SHA — the same shape Docker Registry surfaces on pull.
//
// The function name ends in "Integration" so it is matched by the existing
// `TestSSE.*Integration` pattern in test/s3/sse/Makefile and the
// `.*Multipart.*Integration` pattern in .github/workflows/s3-sse-tests.yml,
// so the regression coverage runs automatically in CI.
func TestSSES3ConcurrentMultipartDigestIntegration(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-s3-concurrent-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(bucketName),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set bucket default SSE-S3 encryption")
const (
numObjects = 5 // mirrors "5 images at a time"
partsPerObject = 12 // 12 parts × 5MB = 60MB per blob; total 5×60=300MB
partSize = 5 * 1024 * 1024 // S3 minimum part size
uploadParallel = 8 // mirror typical S3 SDK transfer manager concurrency
iterations = 2 // run twice to flush out flakiness without bloating CI
)
type blob struct {
key string
parts [][]byte
full []byte
fullSHA [32]byte
}
makeBlobs := func(iter int) []blob {
blobs := make([]blob, numObjects)
for i := range blobs {
parts := make([][]byte, partsPerObject)
for j := range parts {
parts[j] = generateTestData(partSize)
}
full := bytes.Join(parts, nil)
blobs[i] = blob{
key: fmt.Sprintf("iter%02d-blob-%02d", iter, i),
parts: parts,
full: full,
fullSHA: sha256.Sum256(full),
}
}
return blobs
}
uploadOne := func(b blob) error {
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(b.key),
})
if err != nil {
return fmt.Errorf("create multipart for %s: %w", b.key, err)
}
uploadID := aws.ToString(createResp.UploadId)
completedParts := make([]types.CompletedPart, partsPerObject)
var (
wg sync.WaitGroup
partErr error
partMu sync.Mutex
)
sem := make(chan struct{}, uploadParallel)
for i := 0; i < partsPerObject; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
resp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(b.key),
PartNumber: aws.Int32(int32(i + 1)),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(b.parts[i]),
})
if err != nil {
partMu.Lock()
if partErr == nil {
partErr = fmt.Errorf("upload part %d of %s: %w", i+1, b.key, err)
}
partMu.Unlock()
return
}
completedParts[i] = types.CompletedPart{
ETag: resp.ETag,
PartNumber: aws.Int32(int32(i + 1)),
}
}(i)
}
wg.Wait()
if partErr != nil {
_, _ = client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{
Bucket: aws.String(bucketName), Key: aws.String(b.key), UploadId: aws.String(uploadID),
})
return partErr
}
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(b.key),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{Parts: completedParts},
})
if err != nil {
return fmt.Errorf("complete multipart for %s: %w", b.key, err)
}
return nil
}
type result struct {
key string
expected string
actual string
gotSize int64
wantSize int64
readErr error
}
verifyFullGET := func(b blob) result {
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(b.key),
})
if err != nil {
return result{key: "FULL " + b.key, readErr: fmt.Errorf("GetObject: %w", err)}
}
defer resp.Body.Close()
h := sha256.New()
n, copyErr := io.Copy(h, resp.Body)
var actual [32]byte
copy(actual[:], h.Sum(nil))
return result{
key: "FULL " + b.key,
expected: hex.EncodeToString(b.fullSHA[:]),
actual: hex.EncodeToString(actual[:]),
gotSize: n,
wantSize: int64(len(b.full)),
readErr: copyErr,
}
}
verifyRangeGET := func(b blob) result {
// 1MB windows. Includes a window that crosses the 8MB internal-chunk
// boundary (chunkSize in putToFiler) — the historical danger zone for
// SSE-S3 multipart range reads.
const window = 1024 * 1024
h := sha256.New()
var totalN int64
objectSize := int64(len(b.full))
for offset := int64(0); offset < objectSize; offset += window {
end := offset + window - 1
if end >= objectSize {
end = objectSize - 1
}
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(b.key),
Range: aws.String(fmt.Sprintf("bytes=%d-%d", offset, end)),
})
if err != nil {
return result{key: "RANGE " + b.key, readErr: fmt.Errorf("Range GetObject [%d-%d]: %w", offset, end, err)}
}
n, copyErr := io.Copy(h, resp.Body)
resp.Body.Close()
totalN += n
if copyErr != nil {
return result{key: "RANGE " + b.key, readErr: fmt.Errorf("Range copy [%d-%d]: %w", offset, end, copyErr)}
}
}
var actual [32]byte
copy(actual[:], h.Sum(nil))
return result{
key: "RANGE " + b.key,
expected: hex.EncodeToString(b.fullSHA[:]),
actual: hex.EncodeToString(actual[:]),
gotSize: totalN,
wantSize: objectSize,
}
}
var allFailures []string
for iter := 0; iter < iterations; iter++ {
blobs := makeBlobs(iter)
// Upload all N blobs in parallel.
{
var (
wg sync.WaitGroup
errMu sync.Mutex
errors []error
)
for i := range blobs {
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := uploadOne(blobs[i]); err != nil {
errMu.Lock()
errors = append(errors, err)
errMu.Unlock()
}
}(i)
}
wg.Wait()
require.Empty(t, errors, "iter %d: concurrent multipart uploads must succeed", iter)
}
// Two passes (full body + range), all blobs verified concurrently.
results := make([]result, 0, 2*numObjects)
var resultMu sync.Mutex
var wg sync.WaitGroup
for i := range blobs {
wg.Add(2)
go func(b blob) {
defer wg.Done()
r := verifyFullGET(b)
resultMu.Lock()
results = append(results, r)
resultMu.Unlock()
}(blobs[i])
go func(b blob) {
defer wg.Done()
r := verifyRangeGET(b)
resultMu.Lock()
results = append(results, r)
resultMu.Unlock()
}(blobs[i])
}
wg.Wait()
for _, r := range results {
if r.readErr != nil {
allFailures = append(allFailures, fmt.Sprintf("[iter %d %s] read error: %v (got %d / want %d bytes)",
iter, r.key, r.readErr, r.gotSize, r.wantSize))
continue
}
if r.expected != r.actual {
allFailures = append(allFailures, fmt.Sprintf(
"[iter %d %s] DIGEST MISMATCH: expected sha256:%s, got sha256:%s (got %d / want %d bytes)",
iter, r.key, r.expected, r.actual, r.gotSize, r.wantSize))
}
}
}
if len(allFailures) > 0 {
t.Fatalf("%d concurrent SSE-S3 multipart blob digest mismatches across %d iterations:\n %s",
len(allFailures), iterations, joinFailures(allFailures))
}
}
func joinFailures(ss []string) string {
out := ""
for i, s := range ss {
if i > 0 {
out += "\n "
}
out += s
}
return out
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,373 @@
package sse_test
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"io"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
)
// TestSSEMultipartCopy tests copying multipart encrypted objects
func TestSSEMultipartCopy(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-multipart-copy-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
// Generate test data for multipart upload (7.5MB)
originalData := generateTestData(7*1024*1024 + 512*1024)
originalMD5 := fmt.Sprintf("%x", md5.Sum(originalData))
t.Run("Copy SSE-C Multipart Object", func(t *testing.T) {
testSSECMultipartCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS Multipart Object", func(t *testing.T) {
testSSEKMSMultipartCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-C to SSE-KMS", func(t *testing.T) {
testSSECToSSEKMSCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS to SSE-C", func(t *testing.T) {
testSSEKMSToSSECCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-C to Unencrypted", func(t *testing.T) {
testSSECToUnencryptedCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS to Unencrypted", func(t *testing.T) {
testSSEKMSToUnencryptedCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
}
// testSSECMultipartCopy tests copying SSE-C multipart objects with same key
func testSSECMultipartCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-object"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy with same SSE-C key
destKey := "dest-ssec-multipart-object"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// Destination SSE-C headers (same key)
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err, "Failed to copy SSE-C multipart object")
// Verify copied object
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, sseKey, nil)
}
// testSSEKMSMultipartCopy tests copying SSE-KMS multipart objects with same key
func testSSEKMSMultipartCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-object"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy with same SSE-KMS key
destKey := "dest-ssekms-multipart-object"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String("test-multipart-key"),
BucketKeyEnabled: aws.Bool(false),
})
require.NoError(t, err, "Failed to copy SSE-KMS multipart object")
// Verify copied object
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, aws.String("test-multipart-key"))
}
// testSSECToSSEKMSCopy tests copying SSE-C multipart objects to SSE-KMS
func testSSECToSSEKMSCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-for-kms"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy to SSE-KMS
destKey := "dest-ssekms-from-ssec"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// Destination SSE-KMS headers
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String("test-multipart-key"),
BucketKeyEnabled: aws.Bool(false),
})
require.NoError(t, err, "Failed to copy SSE-C to SSE-KMS")
// Verify copied object as SSE-KMS
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, aws.String("test-multipart-key"))
}
// testSSEKMSToSSECCopy tests copying SSE-KMS multipart objects to SSE-C
func testSSEKMSToSSECCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-for-ssec"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy to SSE-C
destKey := "dest-ssec-from-ssekms"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Destination SSE-C headers
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err, "Failed to copy SSE-KMS to SSE-C")
// Verify copied object as SSE-C
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, sseKey, nil)
}
// testSSECToUnencryptedCopy tests copying SSE-C multipart objects to unencrypted
func testSSECToUnencryptedCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-for-plain"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy to unencrypted
destKey := "dest-plain-from-ssec"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// No destination encryption headers
})
require.NoError(t, err, "Failed to copy SSE-C to unencrypted")
// Verify copied object as unencrypted
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, nil)
}
// testSSEKMSToUnencryptedCopy tests copying SSE-KMS multipart objects to unencrypted
func testSSEKMSToUnencryptedCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-for-plain"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy to unencrypted
destKey := "dest-plain-from-ssekms"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// No destination encryption headers
})
require.NoError(t, err, "Failed to copy SSE-KMS to unencrypted")
// Verify copied object as unencrypted
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, nil)
}
// uploadMultipartSSECObject uploads a multipart SSE-C object
func uploadMultipartSSECObject(ctx context.Context, client *s3.Client, bucketName, objectKey string, data []byte, sseKey SSECKey) error {
// Create multipart upload
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
if err != nil {
return err
}
uploadID := aws.ToString(createResp.UploadId)
// Upload parts
partSize := 5 * 1024 * 1024 // 5MB
var completedParts []types.CompletedPart
for i := 0; i < len(data); i += partSize {
end := i + partSize
if end > len(data) {
end = len(data)
}
partNumber := int32(len(completedParts) + 1)
partResp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(data[i:end]),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
if err != nil {
return err
}
completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.ETag,
PartNumber: aws.Int32(partNumber),
})
}
// Complete multipart upload
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{
Parts: completedParts,
},
})
return err
}
// uploadMultipartSSEKMSObject uploads a multipart SSE-KMS object
func uploadMultipartSSEKMSObject(ctx context.Context, client *s3.Client, bucketName, objectKey, keyID string, data []byte) error {
// Create multipart upload
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(keyID),
BucketKeyEnabled: aws.Bool(false),
})
if err != nil {
return err
}
uploadID := aws.ToString(createResp.UploadId)
// Upload parts
partSize := 5 * 1024 * 1024 // 5MB
var completedParts []types.CompletedPart
for i := 0; i < len(data); i += partSize {
end := i + partSize
if end > len(data) {
end = len(data)
}
partNumber := int32(len(completedParts) + 1)
partResp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(data[i:end]),
})
if err != nil {
return err
}
completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.ETag,
PartNumber: aws.Int32(partNumber),
})
}
// Complete multipart upload
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{
Parts: completedParts,
},
})
return err
}
// verifyEncryptedObject verifies that a copied object can be retrieved and matches the original data
func verifyEncryptedObject(t *testing.T, ctx context.Context, client *s3.Client, bucketName, objectKey string, expectedData []byte, expectedMD5 string, sseKey *SSECKey, kmsKeyID *string) {
var getInput *s3.GetObjectInput
if sseKey != nil {
// SSE-C object
getInput = &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
}
} else {
// SSE-KMS or unencrypted object
getInput = &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
}
}
getResp, err := client.GetObject(ctx, getInput)
require.NoError(t, err, "Failed to retrieve copied object %s", objectKey)
defer getResp.Body.Close()
// Read and verify data
retrievedData, err := io.ReadAll(getResp.Body)
require.NoError(t, err, "Failed to read copied object data")
require.Equal(t, len(expectedData), len(retrievedData), "Data size mismatch for object %s", objectKey)
// Verify data using MD5
retrievedMD5 := fmt.Sprintf("%x", md5.Sum(retrievedData))
require.Equal(t, expectedMD5, retrievedMD5, "Data MD5 mismatch for object %s", objectKey)
// Verify encryption headers
if sseKey != nil {
require.Equal(t, "AES256", aws.ToString(getResp.SSECustomerAlgorithm), "SSE-C algorithm mismatch")
require.Equal(t, sseKey.KeyMD5, aws.ToString(getResp.SSECustomerKeyMD5), "SSE-C key MD5 mismatch")
} else if kmsKeyID != nil {
require.Equal(t, types.ServerSideEncryptionAwsKms, getResp.ServerSideEncryption, "SSE-KMS encryption mismatch")
require.Contains(t, aws.ToString(getResp.SSEKMSKeyId), *kmsKeyID, "SSE-KMS key ID mismatch")
}
t.Logf("Successfully verified copied object %s: %d bytes, MD5=%s", objectKey, len(retrievedData), retrievedMD5)
}

View File

@@ -0,0 +1,577 @@
package sse_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
smithyhttp "github.com/aws/smithy-go/transport/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// internalChunkSize mirrors the constant in weed/s3api/s3api_object_handlers_put.go
// (the size at which auto-chunking splits a single PUT or part body inside the
// volume-server layer). Range-read coverage that's interesting for SSE has to
// straddle this boundary, since each internal chunk is encrypted with its own
// adjusted IV (SSE-S3 / SSE-KMS) or its own random IV + PartOffset (SSE-C),
// and the read path has to stitch keystreams across chunks correctly.
const internalChunkSize = 8 * 1024 * 1024
// TestSSERangeReadIntegration is the canonical end-to-end coverage matrix
// for HTTP range GETs across SSE modes, object size classes, and range
// patterns. It supplements the per-SSE-mode TestSSExxxRangeRequests tests
// (which are scoped to small single-chunk objects, ≤1MB) by also exercising
// MEDIUM single-PUT objects that cross one internal 8MB chunk boundary AND
// LARGE multipart objects whose content spans many internal chunks. The
// many-chunk case is the path that broke in #8908 for full-object GETs;
// pinning range correctness here protects against any future regression in
// per-chunk IV / PartOffset plumbing for partial reads.
//
// The function name ends in "Integration" so it is matched by the existing
// `TestSSE.*Integration` pattern that the test/s3/sse Makefile and the
// .github/workflows/s3-sse-tests.yml CI flow use to discover SSE integration
// tests; both flows already start the server using s3-config-template.json,
// which configures the embedded `local` KMS provider with on-demand DEK
// creation, so the sse_kms subtests run end-to-end in CI.
//
// For ad-hoc local runs against a server without any KMS provider, the test
// probes once with a 1-byte SSE-KMS PUT and t.Skip's the sse_kms subtree
// with a clear message rather than producing a 5xx-storm in the logs.
func TestSSERangeReadIntegration(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"range-matrix-")
require.NoError(t, err, "create test bucket")
// MUST be t.Cleanup, not defer: the mode/size subtests below call
// t.Parallel(), which pauses them and yields back to this function. If
// we used defer, the for loop would finish scheduling, the function
// would return, and defer would fire BEFORE any parallel subtest body
// has run -- deleting the bucket out from under them. t.Cleanup waits
// until the test AND all its subtests complete.
t.Cleanup(func() { cleanupTestBucket(ctx, client, bucketName) })
modes := []sseRangeMode{
newRangeModeNone(),
newRangeModeSSEC(),
newRangeModeSSEKMS("test-range-coverage-key"),
newRangeModeSSES3(),
}
// Size classes. Sizes are chosen to stress specific boundaries:
// small : single internal chunk, no boundary
// medium : one internal chunk boundary (8MB+arbitrary tail)
// large : multipart with parts > 8MB, so each part itself spans
// multiple internal chunks AND the object spans multiple
// parts -- this is the shape the #8908 fix targets.
sizes := []sseRangeSize{
{
name: "small_256KB_single_chunk",
singlePutBytes: 256 * 1024,
},
{
name: "medium_12MB_one_internal_boundary",
singlePutBytes: internalChunkSize + 4*1024*1024,
},
{
name: "large_multipart_5x9MB_many_internal_boundaries",
// 5 parts of 9MB each: 45MB total. Every part exceeds the 8MB
// internal chunk size, so every part is split into 2 internal
// chunks (8MB + 1MB), giving ~10 internal chunks across the
// object. With AWS's 5MB minimum part size, this is the
// smallest realistic shape that exercises both inter-part
// stitching and intra-part chunk-boundary crossing.
multipartParts: []int{
9 * 1024 * 1024,
9 * 1024 * 1024,
9 * 1024 * 1024,
9 * 1024 * 1024,
9 * 1024 * 1024,
},
},
}
// Each (mode, size) pair uploads an independent object key under the
// shared bucket and exercises range reads against it. The four modes
// have no shared state (each one carries its own SSE-C key, KMS keyID,
// or none); within a mode each size class also writes a unique key.
// That makes both levels safe to t.Parallel(), which substantially cuts
// CI wall time on the matrix (~45MB of data per mode).
for _, mode := range modes {
mode := mode
t.Run(mode.name(), func(t *testing.T) {
t.Parallel()
if reason := mode.probe(t, ctx, client, bucketName); reason != "" {
t.Skipf("%s unsupported in this test environment: %s", mode.name(), reason)
}
for _, sz := range sizes {
sz := sz
t.Run(sz.name, func(t *testing.T) {
t.Parallel()
objectKey := fmt.Sprintf("%s/%s", mode.name(), sz.name)
var data []byte
if len(sz.multipartParts) > 0 {
parts := make([][]byte, len(sz.multipartParts))
for i, n := range sz.multipartParts {
parts[i] = generateTestData(n)
}
mode.uploadMultipart(t, ctx, client, bucketName, objectKey, parts)
data = bytes.Join(parts, nil)
} else {
data = generateTestData(sz.singlePutBytes)
mode.uploadSingle(t, ctx, client, bucketName, objectKey, data)
}
for _, rc := range rangeCasesFor(int64(len(data))) {
rc := rc
t.Run(rc.name, func(t *testing.T) {
verifyRangeRead(t, ctx, client, mode, bucketName, objectKey, data, rc)
})
}
})
}
})
}
}
// rangeCasesFor returns the set of range patterns to exercise on an object of
// the given total length. Some patterns are skipped automatically when the
// object is too small for them to be meaningful (e.g. the many-chunk-spanning
// case requires the object to actually span many internal chunks).
func rangeCasesFor(totalLen int64) []sseRangeCase {
cases := []sseRangeCase{
{
name: "single_byte_at_zero",
rangeHeader: "bytes=0-0",
start: 0,
end: 0,
},
{
name: "prefix_512_bytes",
rangeHeader: "bytes=0-511",
start: 0,
end: 511,
},
{
name: "single_byte_at_last",
rangeHeader: fmt.Sprintf("bytes=%d-%d", totalLen-1, totalLen-1),
start: totalLen - 1,
end: totalLen - 1,
},
{
name: "suffix_last_100_bytes",
rangeHeader: "bytes=-100",
start: totalLen - 100,
end: totalLen - 1,
},
{
name: "open_ended_from_middle",
rangeHeader: fmt.Sprintf("bytes=%d-", totalLen/2),
start: totalLen / 2,
end: totalLen - 1,
},
{
name: "whole_object_as_range",
rangeHeader: fmt.Sprintf("bytes=0-%d", totalLen-1),
start: 0,
end: totalLen - 1,
},
}
if totalLen >= 64 {
cases = append(cases, sseRangeCase{
// AES block-boundary stress: a 17-byte range starting at byte 15
// crosses the AES block boundary at byte 16. SSE-C historically
// has the most fragile offset arithmetic here, so this is worth
// pinning across all modes.
name: "mid_chunk_crosses_aes_block_boundary",
rangeHeader: "bytes=15-31",
start: 15,
end: 31,
})
}
if totalLen > internalChunkSize+128 {
// Range straddles one internal 8MB chunk boundary by 64 bytes on
// each side. Decryption has to fetch two distinct chunks and stitch
// the keystreams together correctly -- the path that exposed #8908's
// SSE-KMS double-IV bug (fixed in #9224 commit 4) and is the most
// sensitive single test for chunk-boundary stitching.
cases = append(cases, sseRangeCase{
name: "mid_straddles_one_internal_boundary",
rangeHeader: fmt.Sprintf("bytes=%d-%d", internalChunkSize-64, internalChunkSize+63),
start: internalChunkSize - 64,
end: internalChunkSize + 63,
})
}
if totalLen > 3*internalChunkSize+128 {
// Range that spans more than three internal 8MB chunks. This is
// the regression path for #8908's read-side issue: the eager
// multipart reader opened all chunks at once and could close
// later ones via keepalive while earlier ones were still being
// drained; range path uses the per-chunk view helpers (always
// lazy) but a generous-size cross-many-chunks range is still the
// best end-to-end pin that the per-chunk IV plumbing is correct
// across part and chunk boundaries.
start := int64(internalChunkSize/2) + 5
end := start + 3*internalChunkSize + 17
if end >= totalLen {
end = totalLen - 1
}
cases = append(cases, sseRangeCase{
name: "mid_spans_many_internal_boundaries",
rangeHeader: fmt.Sprintf("bytes=%d-%d", start, end),
start: start,
end: end,
})
}
return cases
}
// sseRangeCase is one (start,end) range to GET, with the literal Range header
// to send so we cover both `bytes=N-M`, `bytes=N-`, and `bytes=-N` forms.
type sseRangeCase struct {
name string
rangeHeader string
start, end int64 // inclusive byte offsets in the source data
}
type sseRangeSize struct {
name string
singlePutBytes int // if >0, upload via PutObject of this many random bytes
multipartParts []int // if non-empty, multipart upload with these part sizes (in bytes)
}
// sseRangeMode is the per-SSE-type behavior plug for the matrix: how to
// configure CreateBucket / PutObject / CreateMultipartUpload / UploadPart /
// GetObject, and what to assert on GET responses.
type sseRangeMode interface {
name() string
// probe attempts a 1-byte upload and returns "" on success or a short
// reason string if the test environment doesn't support this mode (used
// to t.Skip the SSE-KMS subtests when no KMS provider is configured).
probe(t *testing.T, ctx context.Context, client *s3.Client, bucket string) string
uploadSingle(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, data []byte)
uploadMultipart(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte)
configureGet(in *s3.GetObjectInput)
verifyGet(t *testing.T, resp *s3.GetObjectOutput)
}
func newRangeModeNone() sseRangeMode { return &rangeModeNone{} }
type rangeModeNone struct{}
func (m *rangeModeNone) name() string { return "no_sse" }
func (m *rangeModeNone) probe(t *testing.T, ctx context.Context, client *s3.Client, bucket string) string {
return ""
}
func (m *rangeModeNone) uploadSingle(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, data []byte) {
t.Helper()
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
})
require.NoError(t, err, "PutObject")
}
func (m *rangeModeNone) uploadMultipart(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte) {
t.Helper()
multipartUpload(t, ctx, client, bucket, key, parts, nil, nil)
}
func (m *rangeModeNone) configureGet(in *s3.GetObjectInput) {}
func (m *rangeModeNone) verifyGet(t *testing.T, resp *s3.GetObjectOutput) {
t.Helper()
assert.Empty(t, string(resp.ServerSideEncryption), "no SSE response header expected for plaintext object")
}
func newRangeModeSSEC() sseRangeMode {
return &rangeModeSSEC{key: generateSSECKey()}
}
type rangeModeSSEC struct {
key *SSECKey
}
func (m *rangeModeSSEC) name() string { return "sse_c" }
func (m *rangeModeSSEC) probe(t *testing.T, ctx context.Context, client *s3.Client, bucket string) string {
return ""
}
func (m *rangeModeSSEC) uploadSingle(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, data []byte) {
t.Helper()
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(m.key.KeyB64),
SSECustomerKeyMD5: aws.String(m.key.KeyMD5),
})
require.NoError(t, err, "PutObject SSE-C")
}
func (m *rangeModeSSEC) uploadMultipart(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte) {
t.Helper()
multipartUpload(t, ctx, client, bucket, key, parts,
func(in *s3.CreateMultipartUploadInput) {
in.SSECustomerAlgorithm = aws.String("AES256")
in.SSECustomerKey = aws.String(m.key.KeyB64)
in.SSECustomerKeyMD5 = aws.String(m.key.KeyMD5)
},
func(in *s3.UploadPartInput) {
in.SSECustomerAlgorithm = aws.String("AES256")
in.SSECustomerKey = aws.String(m.key.KeyB64)
in.SSECustomerKeyMD5 = aws.String(m.key.KeyMD5)
},
)
}
func (m *rangeModeSSEC) configureGet(in *s3.GetObjectInput) {
in.SSECustomerAlgorithm = aws.String("AES256")
in.SSECustomerKey = aws.String(m.key.KeyB64)
in.SSECustomerKeyMD5 = aws.String(m.key.KeyMD5)
}
func (m *rangeModeSSEC) verifyGet(t *testing.T, resp *s3.GetObjectOutput) {
t.Helper()
assert.Equal(t, "AES256", aws.ToString(resp.SSECustomerAlgorithm))
assert.Equal(t, m.key.KeyMD5, aws.ToString(resp.SSECustomerKeyMD5))
}
func newRangeModeSSEKMS(keyID string) sseRangeMode {
return &rangeModeSSEKMS{keyID: keyID}
}
type rangeModeSSEKMS struct {
keyID string
}
func (m *rangeModeSSEKMS) name() string { return "sse_kms" }
func (m *rangeModeSSEKMS) probe(t *testing.T, ctx context.Context, client *s3.Client, bucket string) string {
t.Helper()
probeKey := "__probe__" + m.name()
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(probeKey),
Body: bytes.NewReader([]byte{0}),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(m.keyID),
})
if err != nil {
// "KMS provider not configured" is the friendly diagnostic that
// causes the caller to t.Skip the SSE-KMS subtree. We use it for
// two narrow categories:
// 1. 5xx responses -- the s3api InternalError surface when no KMS
// provider is wired up at server start.
// 2. Specific error strings ("KMS.NotConfigured" /
// "NotImplemented" / "not configured") regardless of status,
// to catch other servers that may use a 4xx/501 to signal the
// same condition.
// We deliberately do NOT auto-skip on a generic 4xx: a real
// SSE-KMS misconfiguration in the test request itself (bad keyID
// format, missing header, etc.) also surfaces as a 400, and the
// CI-meaningful path -- where the server IS configured for KMS --
// must fail loudly in that case rather than silently skip the
// integration coverage.
var apiErr *smithyhttp.ResponseError
if errors.As(err, &apiErr) {
if code := apiErr.HTTPStatusCode(); code >= 500 {
return fmt.Sprintf("KMS provider not configured (PutObject returned %d)", code)
}
}
errMsg := err.Error()
if strings.Contains(errMsg, "KMS.NotConfigured") ||
strings.Contains(errMsg, "NotImplemented") ||
strings.Contains(errMsg, "not configured") {
return fmt.Sprintf("KMS provider not configured: %v", err)
}
return fmt.Sprintf("KMS PutObject probe failed: %v", err)
}
// Best-effort cleanup of the probe object.
_, _ = client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(probeKey),
})
return ""
}
func (m *rangeModeSSEKMS) uploadSingle(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, data []byte) {
t.Helper()
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(m.keyID),
})
require.NoError(t, err, "PutObject SSE-KMS")
}
func (m *rangeModeSSEKMS) uploadMultipart(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte) {
t.Helper()
multipartUpload(t, ctx, client, bucket, key, parts,
func(in *s3.CreateMultipartUploadInput) {
in.ServerSideEncryption = types.ServerSideEncryptionAwsKms
in.SSEKMSKeyId = aws.String(m.keyID)
},
// SSE-KMS does not require per-part headers (server reuses upload-init key).
nil,
)
}
func (m *rangeModeSSEKMS) configureGet(in *s3.GetObjectInput) {}
func (m *rangeModeSSEKMS) verifyGet(t *testing.T, resp *s3.GetObjectOutput) {
t.Helper()
assert.Equal(t, types.ServerSideEncryptionAwsKms, resp.ServerSideEncryption)
assert.Equal(t, m.keyID, aws.ToString(resp.SSEKMSKeyId))
}
func newRangeModeSSES3() sseRangeMode { return &rangeModeSSES3{} }
type rangeModeSSES3 struct{}
func (m *rangeModeSSES3) name() string { return "sse_s3" }
func (m *rangeModeSSES3) probe(t *testing.T, ctx context.Context, client *s3.Client, bucket string) string {
return ""
}
func (m *rangeModeSSES3) uploadSingle(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, data []byte) {
t.Helper()
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ServerSideEncryption: types.ServerSideEncryptionAes256,
})
require.NoError(t, err, "PutObject SSE-S3")
}
func (m *rangeModeSSES3) uploadMultipart(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte) {
t.Helper()
multipartUpload(t, ctx, client, bucket, key, parts,
func(in *s3.CreateMultipartUploadInput) {
in.ServerSideEncryption = types.ServerSideEncryptionAes256
},
// SSE-S3 multipart parts inherit encryption from the upload init.
nil,
)
}
func (m *rangeModeSSES3) configureGet(in *s3.GetObjectInput) {}
func (m *rangeModeSSES3) verifyGet(t *testing.T, resp *s3.GetObjectOutput) {
t.Helper()
assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption)
}
// multipartUpload is a small helper shared across SSE modes that need to
// assemble the test object via Create / UploadPart / Complete with optional
// per-mode header injection. It registers a t.Cleanup that aborts the upload
// if Complete didn't run successfully, so a test failure mid-way doesn't
// leave an orphan upload behind.
func multipartUpload(t *testing.T, ctx context.Context, client *s3.Client, bucket, key string, parts [][]byte,
configCreate func(*s3.CreateMultipartUploadInput),
configPart func(*s3.UploadPartInput)) {
t.Helper()
createIn := &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}
if configCreate != nil {
configCreate(createIn)
}
createResp, err := client.CreateMultipartUpload(ctx, createIn)
require.NoError(t, err, "CreateMultipartUpload")
uploadID := aws.ToString(createResp.UploadId)
completed := false
t.Cleanup(func() {
if completed {
return
}
_, _ = client.AbortMultipartUpload(context.Background(), &s3.AbortMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: aws.String(uploadID),
})
})
completedParts := make([]types.CompletedPart, 0, len(parts))
for i, part := range parts {
partNumber := int32(i + 1)
in := &s3.UploadPartInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(part),
}
if configPart != nil {
configPart(in)
}
resp, err := client.UploadPart(ctx, in)
require.NoError(t, err, "UploadPart %d", partNumber)
completedParts = append(completedParts, types.CompletedPart{
ETag: resp.ETag,
PartNumber: aws.Int32(partNumber),
})
}
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{Parts: completedParts},
})
require.NoError(t, err, "CompleteMultipartUpload")
completed = true
}
// verifyRangeRead does the actual GET + assertions for one (mode, object,
// range case). It checks: the body bytes match the source slice; the
// Content-Length header matches the range length; the Content-Range header
// matches the resolved byte range; the SSE response headers match the mode.
func verifyRangeRead(t *testing.T, ctx context.Context, client *s3.Client, mode sseRangeMode,
bucket, key string, source []byte, rc sseRangeCase) {
t.Helper()
totalLen := int64(len(source))
in := &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Range: aws.String(rc.rangeHeader),
}
mode.configureGet(in)
resp, err := client.GetObject(ctx, in)
require.NoError(t, err, "GetObject %s range=%s", key, rc.rangeHeader)
defer resp.Body.Close()
got, err := io.ReadAll(resp.Body)
require.NoError(t, err, "read range body")
expected := source[rc.start : rc.end+1]
expectedLen := rc.end - rc.start + 1
// Body-length check is `require` rather than `assert` because the bug
// fixed in #8908 surfaces as a fully-readable body that is shorter than
// requested -- a "truncation regression". Comparing different-length
// slices with assertDataEqual below would just produce a noisy byte-diff
// on top of the underlying truncation; bailing here keeps the failure
// log focused on the symptom that actually matters.
require.Equal(t, len(expected), len(got),
"body length mismatch for %s range=%s (source size=%d) — likely truncation regression", key, rc.rangeHeader, totalLen)
assert.Equal(t, expectedLen, aws.ToInt64(resp.ContentLength),
"Content-Length header mismatch for %s range=%s", key, rc.rangeHeader)
// Content-Range: bytes start-end/total
wantContentRange := fmt.Sprintf("bytes %d-%d/%d", rc.start, rc.end, totalLen)
assert.Equal(t, wantContentRange, aws.ToString(resp.ContentRange),
"Content-Range header mismatch for %s range=%s", key, rc.rangeHeader)
// Compare bytes with a hash-only assertion to keep failure output small
// (the actual byte content is random and unhelpful printed verbatim).
assertDataEqual(t, expected, got, "Range body mismatch for %s range=%s", key, rc.rangeHeader)
mode.verifyGet(t, resp)
}

View File

@@ -0,0 +1,445 @@
package sse_test
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"net/http"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// signRawHTTPRequest signs a raw HTTP request with AWS Signature V4
func signRawHTTPRequest(ctx context.Context, req *http.Request, cfg *S3SSETestConfig) error {
// Create credentials
creds := aws.Credentials{
AccessKeyID: cfg.AccessKey,
SecretAccessKey: cfg.SecretKey,
}
// Create signer
signer := v4.NewSigner()
// Calculate payload hash (empty for GET requests)
payloadHash := fmt.Sprintf("%x", sha256.Sum256([]byte{}))
// Sign the request
err := signer.SignHTTP(ctx, creds, req, payloadHash, "s3", cfg.Region, time.Now())
if err != nil {
return fmt.Errorf("failed to sign request: %w", err)
}
return nil
}
// TestSSECRangeRequestsServerBehavior tests that the server correctly handles Range requests
// for SSE-C encrypted objects by checking actual HTTP response (not SDK-processed response)
func TestSSECRangeRequestsServerBehavior(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssec-range-server-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
sseKey := generateSSECKey()
testData := generateTestData(2048) // 2KB test file
objectKey := "test-range-server-validation"
// Upload with SSE-C
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err, "Failed to upload SSE-C object")
// Test cases for range requests
testCases := []struct {
name string
rangeHeader string
expectedStart int64
expectedEnd int64
expectedTotal int64
}{
{
name: "First 100 bytes",
rangeHeader: "bytes=0-99",
expectedStart: 0,
expectedEnd: 99,
expectedTotal: 2048,
},
{
name: "Middle range",
rangeHeader: "bytes=500-699",
expectedStart: 500,
expectedEnd: 699,
expectedTotal: 2048,
},
{
name: "Last 100 bytes",
rangeHeader: "bytes=1948-2047",
expectedStart: 1948,
expectedEnd: 2047,
expectedTotal: 2048,
},
{
name: "Single byte",
rangeHeader: "bytes=1000-1000",
expectedStart: 1000,
expectedEnd: 1000,
expectedTotal: 2048,
},
{
name: "AES block boundary crossing",
rangeHeader: "bytes=15-17",
expectedStart: 15,
expectedEnd: 17,
expectedTotal: 2048,
},
{
name: "Open-ended range",
rangeHeader: "bytes=2000-",
expectedStart: 2000,
expectedEnd: 2047,
expectedTotal: 2048,
},
{
name: "Suffix range (last 100 bytes)",
rangeHeader: "bytes=-100",
expectedStart: 1948,
expectedEnd: 2047,
expectedTotal: 2048,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Build object URL (Endpoint already includes http://)
objectURL := fmt.Sprintf("%s/%s/%s",
defaultConfig.Endpoint,
bucketName,
objectKey,
)
// Create raw HTTP request
req, err := http.NewRequest("GET", objectURL, nil)
require.NoError(t, err, "Failed to create HTTP request")
// Add Range header
req.Header.Set("Range", tc.rangeHeader)
// Add SSE-C headers
req.Header.Set("x-amz-server-side-encryption-customer-algorithm", "AES256")
req.Header.Set("x-amz-server-side-encryption-customer-key", sseKey.KeyB64)
req.Header.Set("x-amz-server-side-encryption-customer-key-MD5", sseKey.KeyMD5)
// Sign the request with AWS Signature V4
err = signRawHTTPRequest(ctx, req, defaultConfig)
require.NoError(t, err, "Failed to sign HTTP request")
// Make request with raw HTTP client
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
require.NoError(t, err, "Failed to execute range request")
defer resp.Body.Close()
// CRITICAL CHECK 1: Status code must be 206 Partial Content
assert.Equal(t, http.StatusPartialContent, resp.StatusCode,
"Server must return 206 Partial Content for range request, got %d", resp.StatusCode)
// CRITICAL CHECK 2: Content-Range header must be present and correct
expectedContentRange := fmt.Sprintf("bytes %d-%d/%d",
tc.expectedStart, tc.expectedEnd, tc.expectedTotal)
actualContentRange := resp.Header.Get("Content-Range")
assert.Equal(t, expectedContentRange, actualContentRange,
"Content-Range header mismatch")
// CRITICAL CHECK 3: Content-Length must match requested range size
expectedLength := tc.expectedEnd - tc.expectedStart + 1
actualLength := resp.ContentLength
assert.Equal(t, expectedLength, actualLength,
"Content-Length mismatch: expected %d, got %d", expectedLength, actualLength)
// CRITICAL CHECK 4: Actual bytes received from network
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Failed to read response body")
assert.Equal(t, int(expectedLength), len(bodyBytes),
"Actual bytes received from server mismatch: expected %d, got %d",
expectedLength, len(bodyBytes))
// CRITICAL CHECK 5: Verify decrypted content matches expected range
expectedData := testData[tc.expectedStart : tc.expectedEnd+1]
assert.Equal(t, expectedData, bodyBytes,
"Decrypted range content doesn't match expected data")
// Verify SSE-C headers are present in response
assert.Equal(t, "AES256", resp.Header.Get("x-amz-server-side-encryption-customer-algorithm"),
"SSE-C algorithm header missing in range response")
assert.Equal(t, sseKey.KeyMD5, resp.Header.Get("x-amz-server-side-encryption-customer-key-MD5"),
"SSE-C key MD5 header missing in range response")
})
}
}
// TestSSEKMSRangeRequestsServerBehavior tests server-side Range handling for SSE-KMS
func TestSSEKMSRangeRequestsServerBehavior(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssekms-range-server-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
kmsKeyID := "test-range-key"
testData := generateTestData(4096) // 4KB test file
objectKey := "test-kms-range-server-validation"
// Upload with SSE-KMS
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: "aws:kms",
SSEKMSKeyId: aws.String(kmsKeyID),
})
require.NoError(t, err, "Failed to upload SSE-KMS object")
// Test various ranges
testCases := []struct {
name string
rangeHeader string
start int64
end int64
}{
{"First KB", "bytes=0-1023", 0, 1023},
{"Second KB", "bytes=1024-2047", 1024, 2047},
{"Last KB", "bytes=3072-4095", 3072, 4095},
{"Unaligned range", "bytes=100-299", 100, 299},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
objectURL := fmt.Sprintf("%s/%s/%s",
defaultConfig.Endpoint,
bucketName,
objectKey,
)
req, err := http.NewRequest("GET", objectURL, nil)
require.NoError(t, err)
req.Header.Set("Range", tc.rangeHeader)
// Sign the request with AWS Signature V4
err = signRawHTTPRequest(ctx, req, defaultConfig)
require.NoError(t, err, "Failed to sign HTTP request")
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify 206 status
assert.Equal(t, http.StatusPartialContent, resp.StatusCode,
"SSE-KMS range request must return 206, got %d", resp.StatusCode)
// Verify Content-Range
expectedContentRange := fmt.Sprintf("bytes %d-%d/%d", tc.start, tc.end, int64(len(testData)))
assert.Equal(t, expectedContentRange, resp.Header.Get("Content-Range"))
// Verify actual bytes received
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
expectedLength := tc.end - tc.start + 1
assert.Equal(t, int(expectedLength), len(bodyBytes),
"Actual network bytes mismatch")
// Verify content
expectedData := testData[tc.start : tc.end+1]
assert.Equal(t, expectedData, bodyBytes)
})
}
}
// TestSSES3RangeRequestsServerBehavior tests server-side Range handling for SSE-S3
func TestSSES3RangeRequestsServerBehavior(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, "sses3-range-server")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
testData := generateTestData(8192) // 8KB test file
objectKey := "test-s3-range-server-validation"
// Upload with SSE-S3
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: "AES256",
})
require.NoError(t, err, "Failed to upload SSE-S3 object")
// Test range request
objectURL := fmt.Sprintf("%s/%s/%s",
defaultConfig.Endpoint,
bucketName,
objectKey,
)
req, err := http.NewRequest("GET", objectURL, nil)
require.NoError(t, err)
req.Header.Set("Range", "bytes=1000-1999")
// Sign the request with AWS Signature V4
err = signRawHTTPRequest(ctx, req, defaultConfig)
require.NoError(t, err, "Failed to sign HTTP request")
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify server response
assert.Equal(t, http.StatusPartialContent, resp.StatusCode)
assert.Equal(t, "bytes 1000-1999/8192", resp.Header.Get("Content-Range"))
assert.Equal(t, int64(1000), resp.ContentLength)
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, 1000, len(bodyBytes))
assert.Equal(t, testData[1000:2000], bodyBytes)
}
// TestSSEMultipartRangeRequestsServerBehavior tests Range requests on multipart encrypted objects
func TestSSEMultipartRangeRequestsServerBehavior(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err)
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssec-mp-range-")
require.NoError(t, err)
defer cleanupTestBucket(ctx, client, bucketName)
sseKey := generateSSECKey()
objectKey := "test-multipart-range-server"
// Create 10MB test data (2 parts of 5MB each)
partSize := 5 * 1024 * 1024
part1Data := generateTestData(partSize)
part2Data := generateTestData(partSize)
fullData := append(part1Data, part2Data...)
// Initiate multipart upload
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err)
uploadID := aws.ToString(createResp.UploadId)
// Upload part 1
part1Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
PartNumber: aws.Int32(1),
Body: bytes.NewReader(part1Data),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err)
// Upload part 2
part2Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
PartNumber: aws.Int32(2),
Body: bytes.NewReader(part2Data),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err)
// Complete multipart upload
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &s3types.CompletedMultipartUpload{
Parts: []s3types.CompletedPart{
{PartNumber: aws.Int32(1), ETag: part1Resp.ETag},
{PartNumber: aws.Int32(2), ETag: part2Resp.ETag},
},
},
})
require.NoError(t, err)
// Test range that crosses part boundary
objectURL := fmt.Sprintf("%s/%s/%s",
defaultConfig.Endpoint,
bucketName,
objectKey,
)
// Range spanning across the part boundary
start := int64(partSize - 1000)
end := int64(partSize + 1000)
req, err := http.NewRequest("GET", objectURL, nil)
require.NoError(t, err)
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
req.Header.Set("x-amz-server-side-encryption-customer-algorithm", "AES256")
req.Header.Set("x-amz-server-side-encryption-customer-key", sseKey.KeyB64)
req.Header.Set("x-amz-server-side-encryption-customer-key-MD5", sseKey.KeyMD5)
// Sign the request with AWS Signature V4
err = signRawHTTPRequest(ctx, req, defaultConfig)
require.NoError(t, err, "Failed to sign HTTP request")
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify server behavior for cross-part range
assert.Equal(t, http.StatusPartialContent, resp.StatusCode,
"Multipart range request must return 206")
expectedLength := end - start + 1
assert.Equal(t, expectedLength, resp.ContentLength,
"Content-Length for cross-part range")
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, int(expectedLength), len(bodyBytes),
"Actual bytes for cross-part range")
// Verify content spans the part boundary correctly
expectedData := fullData[start : end+1]
assert.Equal(t, expectedData, bodyBytes,
"Cross-part range content must be correctly decrypted and assembled")
}

View File

@@ -0,0 +1,190 @@
package sse_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"strings"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
)
// TestSSES3MultipartUploadPartCopyIntegration pins the fix for issue #8908.
//
// Docker Registry's S3 storage driver finalizes blob uploads via a server-side
// "Move" pattern: a streaming PUT/multipart upload to a temporary key, then
// CreateMultipartUpload + UploadPartCopy(s) + CompleteMultipartUpload to put
// the bytes at the final blob path. Under bucket-default SSE-S3, every push
// goes through this UploadPartCopy step.
//
// Before the fix, copyChunksForRange did a raw byte copy that left the
// destination's part chunks SseType=NONE. Then completedMultipartChunk
// (PR #9224) saw NONE chunks in an SSE-S3 multipart upload and "backfilled"
// SSE-S3 metadata with IVs derived from the destination upload's baseIV. But
// the bytes on disk had been encrypted with the SOURCE upload's key+baseIV,
// so the read path decrypted with the wrong IV — yielding deterministic byte
// corruption on GET (the "Digest did not match" symptom kubelet surfaces).
//
// This test reproduces the exact shape: a 39MB plaintext source (single
// PutObject — auto-chunked into multiple internal SSE-S3 chunks on disk
// because of bucket-default SSE-S3), then a fresh multipart upload at a new
// destination key with two UploadPartCopy parts (32MB + 7MB) and Complete.
// The full GET must SHA back to what was uploaded.
//
// The function name contains both "Multipart" and "Integration" so it is matched
// by the `.*Multipart.*Integration` pattern in .github/workflows/s3-sse-tests.yml
// and the `TestSSE.*Integration` pattern in test/s3/sse/Makefile, ensuring this
// regression coverage runs in CI.
func TestSSES3MultipartUploadPartCopyIntegration(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-s3-uploadpartcopy-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
// Bucket-default SSE-S3 — same setup Docker Registry uses.
_, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
Bucket: aws.String(bucketName),
ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{
Rules: []types.ServerSideEncryptionRule{
{
ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{
SSEAlgorithm: types.ServerSideEncryptionAes256,
},
},
},
},
})
require.NoError(t, err, "Failed to set bucket default SSE-S3")
// Source: 39MB single PutObject (auto-chunked internally into 5 SSE-S3 chunks).
const sourceSize = 39 * 1024 * 1024
sourceData := generateTestData(sourceSize)
expectedSHA := sha256.Sum256(sourceData)
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String("source-blob"),
Body: bytes.NewReader(sourceData),
})
require.NoError(t, err, "Failed to upload source object")
// Sanity check: the source itself must round-trip correctly.
{
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String("source-blob"),
})
require.NoError(t, err, "Failed to GET source")
got, err := io.ReadAll(resp.Body)
resp.Body.Close()
require.NoError(t, err)
require.Equal(t, expectedSHA, sha256.Sum256(got), "source object must round-trip")
}
cases := []struct {
name string
// part definitions: each entry is (start, end) byte range to copy.
parts [][2]int64
}{
{
// Docker Registry's typical Move shape for blobs around 40MB:
// one 32MB part + one tail part. This is exactly the metadata
// shape the user reported (5 dst chunks across 2 multipart parts).
name: "DockerRegistry_32MB_Plus_Tail",
parts: [][2]int64{
{0, 32*1024*1024 - 1},
{32 * 1024 * 1024, sourceSize - 1},
},
},
{
// Single full-object UploadPartCopy.
name: "Single_Full_Object_Copy",
parts: [][2]int64{
{0, sourceSize - 1},
},
},
{
// Many small range-copies — exercises the per-part-local-offset
// IV math under varied chunk-overlap shapes.
name: "Many_5MB_Ranges",
parts: [][2]int64{
{0, 5*1024*1024 - 1},
{5 * 1024 * 1024, 10*1024*1024 - 1},
{10 * 1024 * 1024, 15*1024*1024 - 1},
{15 * 1024 * 1024, 20*1024*1024 - 1},
{20 * 1024 * 1024, sourceSize - 1},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dstKey := "dest-" + strings.ToLower(strings.ReplaceAll(tc.name, "_", "-"))
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(dstKey),
})
require.NoError(t, err, "CreateMultipartUpload")
uploadID := aws.ToString(createResp.UploadId)
completedParts := make([]types.CompletedPart, 0, len(tc.parts))
for i, rng := range tc.parts {
partNumber := int32(i + 1)
resp, err := client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: aws.String(bucketName),
Key: aws.String(dstKey),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
CopySource: aws.String(bucketName + "/source-blob"),
CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", rng[0], rng[1])),
})
require.NoErrorf(t, err, "UploadPartCopy part %d range=[%d,%d]", partNumber, rng[0], rng[1])
completedParts = append(completedParts, types.CompletedPart{
ETag: resp.CopyPartResult.ETag,
PartNumber: aws.Int32(partNumber),
})
}
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(dstKey),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{Parts: completedParts},
})
require.NoError(t, err, "CompleteMultipartUpload")
// Two-pass verification: full GET (Docker Registry / Kubelet shape).
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(dstKey),
})
require.NoError(t, err, "GetObject")
defer resp.Body.Close()
h := sha256.New()
n, err := io.Copy(h, resp.Body)
require.NoError(t, err, "stream GET body")
require.Equal(t, int64(sourceSize), n, "GET length")
var actual [32]byte
copy(actual[:], h.Sum(nil))
require.Equalf(t, expectedSHA, actual,
"UploadPartCopy SHA mismatch (#8908):\n expected sha256:%s\n got sha256:%s\n parts=%d size=%d",
hex.EncodeToString(expectedSHA[:]),
hex.EncodeToString(actual[:]),
len(tc.parts), n)
})
}
}

View File

@@ -0,0 +1,471 @@
//go:build integration
package sse
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
// TestS3VolumeEncryptionRoundtrip tests that data uploaded with encryptVolumeData
// enabled can be read back correctly. This requires the S3 server to be started with
// -encryptVolumeData=true for the test to verify encryption is working.
//
// To run this test:
// 1. Start SeaweedFS: weed server -s3 -s3.encryptVolumeData=true
// 2. Run: go test -v -run TestS3VolumeEncryptionRoundtrip
func TestS3VolumeEncryptionRoundtrip(t *testing.T) {
svc := getS3Client(t)
bucket := fmt.Sprintf("volume-encryption-test-%d", time.Now().Unix())
// Create bucket
_, err := svc.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
t.Fatalf("Failed to create bucket: %v", err)
}
defer cleanupBucket(t, svc, bucket)
testCases := []struct {
name string
key string
content string
binaryContent []byte
isBinary bool
rangeReq string // Optional range request
}{
{
name: "small file",
key: "small.txt",
content: "Hello, encrypted world!",
},
{
name: "medium file",
key: "medium.txt",
content: strings.Repeat("SeaweedFS volume encryption test content. ", 1000),
},
{
name: "binary content",
key: "binary.bin",
binaryContent: func() []byte {
// Create a 1KB binary file with predictable pattern including null bytes
data := make([]byte, 1024)
for i := range data {
data[i] = byte(i % 256)
}
return data
}(),
isBinary: true,
},
{
name: "range request",
key: "range-test.txt",
content: "0123456789ABCDEFGHIJ",
rangeReq: "bytes=5-10",
},
}
for _, tc := range testCases {
tc := tc // Capture range variable for closure
t.Run(tc.name, func(t *testing.T) {
// Add small delay to avoid rapid consecutive requests during volume encryption
time.Sleep(50 * time.Millisecond)
// Upload - use appropriate reader for binary vs text content
var uploadBody io.ReadSeeker
var expectedData []byte
if tc.isBinary {
uploadBody = bytes.NewReader(tc.binaryContent)
expectedData = tc.binaryContent
} else {
uploadBody = strings.NewReader(tc.content)
expectedData = []byte(tc.content)
}
_, err := svc.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(tc.key),
Body: uploadBody,
})
if err != nil {
t.Fatalf("PutObject failed: %v", err)
}
// Download
getInput := &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(tc.key),
}
if tc.rangeReq != "" {
getInput.Range = aws.String(tc.rangeReq)
}
result, err := svc.GetObject(getInput)
if err != nil {
t.Fatalf("GetObject failed: %v", err)
}
defer result.Body.Close()
data, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
// Verify content
expected := expectedData
if tc.rangeReq != "" {
// For "bytes=5-10", we expect bytes at positions 5-10 (inclusive)
expected = expectedData[5:11]
}
if !bytes.Equal(data, expected) {
t.Errorf("Content mismatch:\n expected: %v\n got: %v", expected, data)
} else {
t.Logf("Successfully uploaded and downloaded %s (%d bytes)", tc.key, len(data))
}
})
}
}
// TestS3VolumeEncryptionMultiChunk tests large files that span multiple chunks
// to ensure encryption works correctly across chunk boundaries.
func TestS3VolumeEncryptionMultiChunk(t *testing.T) {
svc := getS3Client(t)
bucket := fmt.Sprintf("volume-encryption-multichunk-%d", time.Now().Unix())
// Create bucket
_, err := svc.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
t.Fatalf("Failed to create bucket: %v", err)
}
defer cleanupBucket(t, svc, bucket)
// Create a file larger than default chunk size (8MB)
// Use 10MB to ensure multiple chunks
largeContent := make([]byte, 10*1024*1024)
for i := range largeContent {
largeContent[i] = byte(i % 256)
}
key := "large-file.bin"
// Upload
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(largeContent),
})
if err != nil {
t.Fatalf("PutObject failed for large file: %v", err)
}
// Download full file
result, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
t.Fatalf("GetObject failed: %v", err)
}
defer result.Body.Close()
downloadedData, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if len(downloadedData) != len(largeContent) {
t.Errorf("Size mismatch: expected %d, got %d", len(largeContent), len(downloadedData))
}
if !bytes.Equal(downloadedData, largeContent) {
t.Errorf("Content mismatch in multi-chunk file")
// Find first mismatch
for i := 0; i < len(downloadedData) && i < len(largeContent); i++ {
if downloadedData[i] != largeContent[i] {
t.Errorf("First mismatch at byte %d: expected %02x, got %02x", i, largeContent[i], downloadedData[i])
break
}
}
} else {
t.Logf("Successfully uploaded and downloaded %d byte multi-chunk file", len(downloadedData))
}
// Test range request spanning chunk boundary (around 8MB)
rangeStart := int64(8*1024*1024 - 1000)
rangeEnd := int64(8*1024*1024 + 1000)
rangeResult, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Range: aws.String(fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd)),
})
if err != nil {
t.Fatalf("Range GetObject failed: %v", err)
}
defer rangeResult.Body.Close()
rangeData, err := io.ReadAll(rangeResult.Body)
if err != nil {
t.Fatalf("Failed to read range response: %v", err)
}
expectedRange := largeContent[rangeStart : rangeEnd+1]
if !bytes.Equal(rangeData, expectedRange) {
t.Errorf("Range request content mismatch at chunk boundary")
} else {
t.Logf("Successfully retrieved range spanning chunk boundary (%d bytes)", len(rangeData))
}
}
// TestS3VolumeEncryptionMultiChunkRangeRead tests range reads that span multiple chunks:
// - Part of chunk 1
// - Whole chunk 2
// - Part of chunk 3
// This is critical for verifying that cipher decryption works correctly when
// reading across chunk boundaries with the cipherKey from each chunk.
func TestS3VolumeEncryptionMultiChunkRangeRead(t *testing.T) {
svc := getS3Client(t)
bucket := fmt.Sprintf("volume-encryption-multirange-%d", time.Now().Unix())
// Create bucket
_, err := svc.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
t.Fatalf("Failed to create bucket: %v", err)
}
defer cleanupBucket(t, svc, bucket)
// Default chunk size is 8MB. Create a file with 3+ chunks (25MB)
// to ensure we have multiple complete chunks
const chunkSize = 8 * 1024 * 1024 // 8MB
const fileSize = 25 * 1024 * 1024 // 25MB = 3 chunks + partial 4th
largeContent := make([]byte, fileSize)
for i := range largeContent {
// Use recognizable pattern: each byte encodes its position
largeContent[i] = byte(i % 256)
}
key := "multi-chunk-range-test.bin"
// Upload
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(largeContent),
})
if err != nil {
t.Fatalf("PutObject failed: %v", err)
}
t.Logf("Uploaded %d byte file (%d chunks of %d bytes)", fileSize, (fileSize+chunkSize-1)/chunkSize, chunkSize)
// Test cases for range reads spanning multiple chunks
testCases := []struct {
name string
rangeStart int64
rangeEnd int64
desc string
}{
{
name: "part_chunk1_whole_chunk2_part_chunk3",
rangeStart: chunkSize - 1000, // 1000 bytes before end of chunk 1
rangeEnd: 2*chunkSize + 1000, // 1000 bytes into chunk 3
desc: "Spans from end of chunk 1 through entire chunk 2 into beginning of chunk 3",
},
{
name: "last_byte_chunk1_through_first_byte_chunk3",
rangeStart: chunkSize - 1, // Last byte of chunk 1
rangeEnd: 2 * chunkSize, // First byte of chunk 3
desc: "Minimal span: last byte of chunk 1, entire chunk 2, first byte of chunk 3",
},
{
name: "middle_chunk1_to_middle_chunk3",
rangeStart: chunkSize / 2, // Middle of chunk 1
rangeEnd: 2*chunkSize + chunkSize/2, // Middle of chunk 3
desc: "From middle of chunk 1 to middle of chunk 3",
},
{
name: "half_chunk1_whole_chunk2_half_chunk3",
rangeStart: chunkSize / 2, // Half of chunk 1
rangeEnd: 2*chunkSize + chunkSize/2 - 1, // Half of chunk 3
desc: "Half of each boundary chunk, whole middle chunk",
},
{
name: "single_byte_each_boundary",
rangeStart: chunkSize - 1, // 1 byte from chunk 1
rangeEnd: 2 * chunkSize, // 1 byte from chunk 3
desc: "1 byte from chunk 1, entire chunk 2, 1 byte from chunk 3",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Logf("Test: %s", tc.desc)
t.Logf("Range: bytes=%d-%d (spanning bytes at offsets across chunk boundaries)", tc.rangeStart, tc.rangeEnd)
result, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Range: aws.String(fmt.Sprintf("bytes=%d-%d", tc.rangeStart, tc.rangeEnd)),
})
if err != nil {
t.Fatalf("Range GetObject failed: %v", err)
}
defer result.Body.Close()
rangeData, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("Failed to read range response: %v", err)
}
expectedLen := tc.rangeEnd - tc.rangeStart + 1
if int64(len(rangeData)) != expectedLen {
t.Errorf("Size mismatch: expected %d bytes, got %d", expectedLen, len(rangeData))
}
expectedRange := largeContent[tc.rangeStart : tc.rangeEnd+1]
if !bytes.Equal(rangeData, expectedRange) {
t.Errorf("Content mismatch in multi-chunk range read")
// Find first mismatch for debugging
for i := 0; i < len(rangeData) && i < len(expectedRange); i++ {
if rangeData[i] != expectedRange[i] {
globalOffset := tc.rangeStart + int64(i)
chunkNum := globalOffset / chunkSize
offsetInChunk := globalOffset % chunkSize
t.Errorf("First mismatch at byte %d (chunk %d, offset %d in chunk): expected %02x, got %02x",
i, chunkNum, offsetInChunk, expectedRange[i], rangeData[i])
break
}
}
} else {
t.Logf("✓ Successfully read %d bytes spanning chunks (offsets %d-%d)", len(rangeData), tc.rangeStart, tc.rangeEnd)
}
})
}
}
// TestS3VolumeEncryptionCopy tests that copying encrypted objects works correctly.
func TestS3VolumeEncryptionCopy(t *testing.T) {
svc := getS3Client(t)
bucket := fmt.Sprintf("volume-encryption-copy-%d", time.Now().Unix())
// Create bucket
_, err := svc.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
t.Fatalf("Failed to create bucket: %v", err)
}
defer cleanupBucket(t, svc, bucket)
srcKey := "source-object.txt"
dstKey := "copied-object.txt"
content := "Content to be copied with volume-level encryption"
// Upload source
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(srcKey),
Body: strings.NewReader(content),
})
if err != nil {
t.Fatalf("PutObject failed: %v", err)
}
// Copy object
_, err = svc.CopyObject(&s3.CopyObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucket, srcKey)),
})
if err != nil {
t.Fatalf("CopyObject failed: %v", err)
}
// Read copied object
result, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
})
if err != nil {
t.Fatalf("GetObject failed for copied object: %v", err)
}
defer result.Body.Close()
data, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("Failed to read copied object: %v", err)
}
if string(data) != content {
t.Errorf("Copied content mismatch:\n expected: %q\n got: %q", content, string(data))
} else {
t.Logf("Successfully copied encrypted object")
}
}
// Helper functions
func getS3Client(t *testing.T) *s3.S3 {
// Use credentials that match the Makefile configuration
// ACCESS_KEY ?= some_access_key1
// SECRET_KEY ?= some_secret_key1
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Endpoint: aws.String("http://localhost:8333"),
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials("some_access_key1", "some_secret_key1", ""),
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
return s3.New(sess)
}
func cleanupBucket(t *testing.T, svc *s3.S3, bucket string) {
ctx := context.Background()
_ = ctx
// List and delete all objects
listResult, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
})
if err != nil {
t.Logf("Warning: failed to list objects for cleanup: %v", err)
return
}
for _, obj := range listResult.Contents {
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: obj.Key,
})
if err != nil {
t.Logf("Warning: failed to delete object %s: %v", *obj.Key, err)
}
}
// Delete bucket
_, err = svc.DeleteBucket(&s3.DeleteBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
t.Logf("Warning: failed to delete bucket %s: %v", bucket, err)
}
}

146
test/s3/sse/setup_openbao_sse.sh Executable file
View File

@@ -0,0 +1,146 @@
#!/bin/bash
# Setup OpenBao for SSE Integration Testing
# This script configures OpenBao with encryption keys for S3 SSE testing
set -e
# Configuration
OPENBAO_ADDR="${OPENBAO_ADDR:-http://127.0.0.1:8200}"
OPENBAO_TOKEN="${OPENBAO_TOKEN:-root-token-for-testing}"
TRANSIT_PATH="${TRANSIT_PATH:-transit}"
echo "🚀 Setting up OpenBao for S3 SSE integration testing..."
echo "OpenBao Address: $OPENBAO_ADDR"
echo "Transit Path: $TRANSIT_PATH"
# Export for API calls
export VAULT_ADDR="$OPENBAO_ADDR"
export VAULT_TOKEN="$OPENBAO_TOKEN"
# Wait for OpenBao to be ready
echo "⏳ Waiting for OpenBao to be ready..."
for i in {1..30}; do
if curl -s "$OPENBAO_ADDR/v1/sys/health" > /dev/null 2>&1; then
echo "[OK] OpenBao is ready!"
break
fi
if [ $i -eq 30 ]; then
echo "[FAIL] OpenBao failed to start within 60 seconds"
exit 1
fi
sleep 2
done
# Enable transit secrets engine (ignore error if already enabled)
echo "🔧 Setting up transit secrets engine..."
curl -s -X POST \
-H "X-Vault-Token: $OPENBAO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"type\":\"transit\"}" \
"$OPENBAO_ADDR/v1/sys/mounts/$TRANSIT_PATH" || echo "Transit engine may already be enabled"
# Create encryption keys for S3 SSE testing
echo "🔑 Creating encryption keys for SSE testing..."
# Test keys that match the existing test expectations
declare -a keys=(
"test-key-123:SSE-KMS basic integration test key"
"source-test-key-123:SSE-KMS copy source key"
"dest-test-key-456:SSE-KMS copy destination key"
"test-multipart-key:SSE-KMS multipart upload test key"
"invalid-test-key:SSE-KMS error testing key"
"test-kms-range-key:SSE-KMS range request test key"
"seaweedfs-test-key:General SeaweedFS SSE test key"
"bucket-default-key:Default bucket encryption key"
"high-security-key:High security encryption key"
"performance-key:Performance testing key"
)
for key_info in "${keys[@]}"; do
IFS=':' read -r key_name description <<< "$key_info"
echo " Creating key: $key_name ($description)"
# Create key
response=$(curl -s -X POST \
-H "X-Vault-Token: $OPENBAO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"type\":\"aes256-gcm96\",\"description\":\"$description\"}" \
"$OPENBAO_ADDR/v1/$TRANSIT_PATH/keys/$key_name")
if echo "$response" | grep -q "errors"; then
echo " Warning: $response"
fi
# Verify key was created
verify_response=$(curl -s \
-H "X-Vault-Token: $OPENBAO_TOKEN" \
"$OPENBAO_ADDR/v1/$TRANSIT_PATH/keys/$key_name")
if echo "$verify_response" | grep -q "\"name\":\"$key_name\""; then
echo " [OK] Key $key_name created successfully"
else
echo " [FAIL] Failed to verify key $key_name"
echo " Response: $verify_response"
fi
done
# Test basic encryption/decryption functionality
echo "🧪 Testing basic encryption/decryption..."
test_plaintext="Hello, SeaweedFS SSE Integration!"
test_key="test-key-123"
# Encrypt
encrypt_response=$(curl -s -X POST \
-H "X-Vault-Token: $OPENBAO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"plaintext\":\"$(echo -n "$test_plaintext" | base64)\"}" \
"$OPENBAO_ADDR/v1/$TRANSIT_PATH/encrypt/$test_key")
if echo "$encrypt_response" | grep -q "ciphertext"; then
ciphertext=$(echo "$encrypt_response" | grep -o '"ciphertext":"[^"]*"' | cut -d'"' -f4)
echo " [OK] Encryption successful: ${ciphertext:0:50}..."
# Decrypt to verify
decrypt_response=$(curl -s -X POST \
-H "X-Vault-Token: $OPENBAO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"ciphertext\":\"$ciphertext\"}" \
"$OPENBAO_ADDR/v1/$TRANSIT_PATH/decrypt/$test_key")
if echo "$decrypt_response" | grep -q "plaintext"; then
decrypted_b64=$(echo "$decrypt_response" | grep -o '"plaintext":"[^"]*"' | cut -d'"' -f4)
decrypted=$(echo "$decrypted_b64" | base64 -d)
if [ "$decrypted" = "$test_plaintext" ]; then
echo " [OK] Decryption successful: $decrypted"
else
echo " [FAIL] Decryption failed: expected '$test_plaintext', got '$decrypted'"
fi
else
echo " [FAIL] Decryption failed: $decrypt_response"
fi
else
echo " [FAIL] Encryption failed: $encrypt_response"
fi
echo ""
echo "📊 OpenBao SSE setup summary:"
echo " Address: $OPENBAO_ADDR"
echo " Transit Path: $TRANSIT_PATH"
echo " Keys Created: ${#keys[@]}"
echo " Status: Ready for S3 SSE integration testing"
echo ""
echo "🎯 Ready to run S3 SSE integration tests!"
echo ""
echo "Usage:"
echo " # Run with Docker Compose"
echo " make test-with-kms"
echo ""
echo " # Run specific test suites"
echo " make test-ssekms-integration"
echo ""
echo " # Check status"
echo " curl $OPENBAO_ADDR/v1/sys/health"
echo ""
echo "[OK] OpenBao SSE setup complete!"

View File

@@ -0,0 +1,115 @@
package sse_test
import (
"bytes"
"context"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSimpleSSECIntegration tests basic SSE-C with a fixed bucket name
func TestSimpleSSECIntegration(t *testing.T) {
ctx := context.Background()
// Create S3 client
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: "http://127.0.0.1:8333",
HostnameImmutable: true,
}, nil
})
awsCfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("us-east-1"),
config.WithEndpointResolverWithOptions(customResolver),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
"some_access_key1",
"some_secret_key1",
"",
)),
)
require.NoError(t, err)
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = true
})
bucketName := "test-debug-bucket"
objectKey := fmt.Sprintf("test-object-prefixed-%d", time.Now().UnixNano())
// Generate SSE-C key
key := make([]byte, 32)
rand.Read(key)
keyB64 := base64.StdEncoding.EncodeToString(key)
keyMD5Hash := md5.Sum(key)
keyMD5 := base64.StdEncoding.EncodeToString(keyMD5Hash[:])
testData := []byte("Hello, simple SSE-C integration test!")
// Ensure bucket exists
_, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(bucketName),
})
if err != nil {
t.Logf("Bucket creation result: %v (might be OK if exists)", err)
}
// Wait a moment for bucket to be ready
time.Sleep(1 * time.Second)
t.Run("PUT with SSE-C", func(t *testing.T) {
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(keyB64),
SSECustomerKeyMD5: aws.String(keyMD5),
})
require.NoError(t, err, "Failed to upload SSE-C object")
t.Log("SSE-C PUT succeeded!")
})
t.Run("GET with SSE-C", func(t *testing.T) {
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(keyB64),
SSECustomerKeyMD5: aws.String(keyMD5),
})
require.NoError(t, err, "Failed to retrieve SSE-C object")
defer resp.Body.Close()
retrievedData, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Failed to read retrieved data")
assert.Equal(t, testData, retrievedData, "Retrieved data doesn't match original")
// Verify SSE-C headers
assert.Equal(t, "AES256", aws.ToString(resp.SSECustomerAlgorithm))
assert.Equal(t, keyMD5, aws.ToString(resp.SSECustomerKeyMD5))
t.Log("SSE-C GET succeeded and data matches!")
})
t.Run("GET without key should fail", func(t *testing.T) {
_, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
assert.Error(t, err, "Should fail to retrieve SSE-C object without key")
t.Log("GET without key correctly failed")
})
}

View File

@@ -0,0 +1,184 @@
package sse_test
import (
"bytes"
"context"
"io"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSSEKMSOpenBaoIntegration tests SSE-KMS with real OpenBao KMS provider
// This test verifies that SeaweedFS can successfully encrypt and decrypt data
// using actual KMS operations through OpenBao, not just mock key IDs
func TestSSEKMSOpenBaoIntegration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-kms-openbao-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
t.Run("Basic SSE-KMS with OpenBao", func(t *testing.T) {
testData := []byte("Hello, SSE-KMS with OpenBao integration!")
objectKey := "test-openbao-kms-object"
kmsKeyID := "test-key-123" // This key should exist in OpenBao
// Upload object with SSE-KMS
putResp, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(kmsKeyID),
})
require.NoError(t, err, "Failed to upload SSE-KMS object with OpenBao")
assert.NotEmpty(t, aws.ToString(putResp.ETag), "ETag should be present")
// Retrieve and verify object
getResp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to retrieve SSE-KMS object")
defer getResp.Body.Close()
// Verify content matches (this proves encryption/decryption worked)
retrievedData, err := io.ReadAll(getResp.Body)
require.NoError(t, err, "Failed to read retrieved data")
assert.Equal(t, testData, retrievedData, "Decrypted data should match original")
// Verify SSE-KMS headers are present
assert.Equal(t, types.ServerSideEncryptionAwsKms, getResp.ServerSideEncryption, "Should indicate KMS encryption")
assert.Equal(t, kmsKeyID, aws.ToString(getResp.SSEKMSKeyId), "Should return the KMS key ID used")
})
t.Run("Multiple KMS Keys with OpenBao", func(t *testing.T) {
testCases := []struct {
keyID string
data string
objectKey string
}{
{"test-key-123", "Data encrypted with test-key-123", "object-key-123"},
{"seaweedfs-test-key", "Data encrypted with seaweedfs-test-key", "object-seaweedfs-key"},
{"high-security-key", "Data encrypted with high-security-key", "object-security-key"},
}
for _, tc := range testCases {
t.Run("Key_"+tc.keyID, func(t *testing.T) {
testData := []byte(tc.data)
// Upload with specific KMS key
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(tc.objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(tc.keyID),
})
require.NoError(t, err, "Failed to upload with KMS key %s", tc.keyID)
// Retrieve and verify
getResp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(tc.objectKey),
})
require.NoError(t, err, "Failed to retrieve object encrypted with key %s", tc.keyID)
defer getResp.Body.Close()
retrievedData, err := io.ReadAll(getResp.Body)
require.NoError(t, err, "Failed to read data for key %s", tc.keyID)
// Verify data integrity (proves real encryption/decryption occurred)
assert.Equal(t, testData, retrievedData, "Data should match for key %s", tc.keyID)
assert.Equal(t, tc.keyID, aws.ToString(getResp.SSEKMSKeyId), "Should return correct key ID")
})
}
})
t.Run("Large Data with OpenBao KMS", func(t *testing.T) {
// Test with larger data to ensure chunked encryption works
testData := generateTestData(64 * 1024) // 64KB
objectKey := "large-openbao-kms-object"
kmsKeyID := "performance-key"
// Upload large object with SSE-KMS
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(kmsKeyID),
})
require.NoError(t, err, "Failed to upload large SSE-KMS object")
// Retrieve and verify large object
getResp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to retrieve large SSE-KMS object")
defer getResp.Body.Close()
retrievedData, err := io.ReadAll(getResp.Body)
require.NoError(t, err, "Failed to read large data")
// Use MD5 comparison for large data
assertDataEqual(t, testData, retrievedData, "Large encrypted data should match original")
assert.Equal(t, kmsKeyID, aws.ToString(getResp.SSEKMSKeyId), "Should return performance key ID")
})
}
// TestSSEKMSOpenBaoAvailability checks if OpenBao KMS is available for testing
// This test can be run separately to verify the KMS setup
func TestSSEKMSOpenBaoAvailability(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-kms-availability-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
// Try a simple KMS operation to verify availability
testData := []byte("KMS availability test")
objectKey := "kms-availability-test"
kmsKeyID := "test-key-123"
// This should succeed if KMS is properly configured
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(testData),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(kmsKeyID),
})
if err != nil {
t.Skipf("OpenBao KMS not available for testing: %v", err)
}
t.Logf("OpenBao KMS is available and working")
// Verify we can retrieve the object
getResp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
require.NoError(t, err, "Failed to retrieve KMS test object")
defer getResp.Body.Close()
assert.Equal(t, types.ServerSideEncryptionAwsKms, getResp.ServerSideEncryption)
t.Logf("KMS encryption/decryption working correctly")
}

View File

@@ -0,0 +1 @@
Test data for single object SSE-C