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
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:
506
test/s3/sse/Makefile
Normal file
506
test/s3/sse/Makefile
Normal 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
253
test/s3/sse/README.md
Normal 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
245
test/s3/sse/README_KMS.md
Normal 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! 🚀
|
||||
100
test/s3/sse/docker-compose.yml
Normal file
100
test/s3/sse/docker-compose.yml
Normal 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
|
||||
504
test/s3/sse/github_7562_copy_test.go
Normal file
504
test/s3/sse/github_7562_copy_test.go
Normal 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!")
|
||||
}
|
||||
23
test/s3/sse/s3-config-template.json
Normal file
23
test/s3/sse/s3-config-template.json
Normal 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
41
test/s3/sse/s3_kms.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
test/s3/sse/s3_range_headers_test.go
Normal file
104
test/s3/sse/s3_range_headers_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
310
test/s3/sse/s3_sse_concurrent_repro_test.go
Normal file
310
test/s3/sse/s3_sse_concurrent_repro_test.go
Normal 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
|
||||
}
|
||||
2843
test/s3/sse/s3_sse_integration_test.go
Normal file
2843
test/s3/sse/s3_sse_integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
373
test/s3/sse/s3_sse_multipart_copy_test.go
Normal file
373
test/s3/sse/s3_sse_multipart_copy_test.go
Normal 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)
|
||||
}
|
||||
577
test/s3/sse/s3_sse_range_coverage_test.go
Normal file
577
test/s3/sse/s3_sse_range_coverage_test.go
Normal 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)
|
||||
}
|
||||
445
test/s3/sse/s3_sse_range_server_test.go
Normal file
445
test/s3/sse/s3_sse_range_server_test.go
Normal 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")
|
||||
}
|
||||
190
test/s3/sse/s3_sse_uploadpartcopy_integration_test.go
Normal file
190
test/s3/sse/s3_sse_uploadpartcopy_integration_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
471
test/s3/sse/s3_volume_encryption_test.go
Normal file
471
test/s3/sse/s3_volume_encryption_test.go
Normal 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
146
test/s3/sse/setup_openbao_sse.sh
Executable 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!"
|
||||
115
test/s3/sse/simple_sse_test.go
Normal file
115
test/s3/sse/simple_sse_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
184
test/s3/sse/sse_kms_openbao_test.go
Normal file
184
test/s3/sse/sse_kms_openbao_test.go
Normal 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")
|
||||
}
|
||||
1
test/s3/sse/test_single_ssec.txt
Normal file
1
test/s3/sse/test_single_ssec.txt
Normal file
@@ -0,0 +1 @@
|
||||
Test data for single object SSE-C
|
||||
Reference in New Issue
Block a user