diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef47fb14..210e5e7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,15 +41,15 @@ jobs: git config --global credential.helper store echo "https://x-token-auth:${{ secrets.PAT_FOR_PRIVATE_RUBY }}@github.com" > ~/.git-credentials - - name: Cache git submodules - uses: actions/cache@v4 - with: - path: | - .git/modules - test-server/*/.git - key: ${{ runner.os }}-submodules-${{ hashFiles('.gitmodules') }} - restore-keys: | - ${{ runner.os }}-submodules- + # - name: Cache git submodules + # uses: actions/cache@v4 + # with: + # path: | + # .git/modules + # test-server/*/.git + # key: ${{ runner.os }}-submodules-${{ hashFiles('.gitmodules') }} + # restore-keys: | + # ${{ runner.os }}-submodules- - name: Optimize git for performance run: | @@ -59,13 +59,12 @@ jobs: - name: Checkout submodules with --jobs run: | - git submodule update --init --depth 1 --jobs ${{ steps.cpu-count.outputs.count }} + git submodule update --init --depth 1 --single-branch --jobs ${{ steps.cpu-count.outputs.count }} - name: Update cpp submodules recursively with --jobs run: | git submodule update --init --recursive \ - --depth 1 \ - --filter=blob:none \ + --depth 1 --single-branch \ --jobs ${{ steps.cpu-count.outputs.count }} \ --force \ test-server/cpp-v2-transition-server/aws-sdk-cpp \ @@ -156,25 +155,25 @@ jobs: aws-region: us-west-2 - name: Build the servers - run: cd test-server && make build-all-servers + run: cd test-server && make build-all-servers FILTER=ruby,java,php,net,go env: MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }} AWS_REGION: us-west-2 - name: Start the servers - run: cd test-server && make start-all-servers + run: cd test-server && make start-all-servers FILTER=ruby,java,php,net,go env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} TEST_SERVER_KMS_KEY_ARN: ${{ vars.TEST_SERVER_KMS_KEY_ARN }} - name: Wait for servers to start - run: cd test-server && make wait-all-servers + run: cd test-server && make wait-all-servers FILTER=ruby,java,php,net,go env: MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }} - name: Run run-tests - run: cd test-server && make run-tests + run: cd test-server && make run-tests FILTER=ruby,java,php,net,go env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} diff --git a/test-server/Makefile b/test-server/Makefile index 255323a1..28f6e1be 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -2,9 +2,6 @@ .PHONY: all start-servers run-tests stop-servers clean ci check-env help -# Default target -all: start-all-servers wait-all-servers run-tests - # CI target for GitHub Actions ci: $(MAKE) build-all-servers @@ -20,13 +17,19 @@ START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) # Build all servers in parallel -build-all-servers: export MAKEFLAGS=-j$(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 1) -build-all-servers: $(BUILD_SERVER_TARGETS) +build-all-servers: + @echo "[`date +%H:%M:%S`] Building all servers..." + @$(MAKE) $(BUILD_SERVER_TARGETS) + @echo "[`date +%H:%M:%S`] All servers built." + @echo "Stopping dotnet servers... this is here to speed up CI because if this target is run with -j it will stay open until it shutdown." + @dotnet build-server shutdown + @echo "[`date +%H:%M:%S`] Dotnet build servers stopped" $(BUILD_SERVER_TARGETS): build-%: @if [ -f $*/Makefile ]; then \ - echo "Building server in $*..."; \ + echo "[`date +%H:%M:%S`] Building server in $*..."; \ $(MAKE) -C $* build-server; \ + echo "[`date +%H:%M:%S`] Server $* built successfully"; \ else \ echo "❌ Error: no Makefile found in $*"; \ exit 1; \ @@ -44,9 +47,8 @@ start-servers: $(MAKE) -C $$dir wait-for-server; \ done -# Start servers sequentially (no parallel execution) start-all-servers: - @$(MAKE) MAKEFLAGS= $(START_SERVER_TARGETS) + @$(MAKE) $(START_SERVER_TARGETS) $(START_SERVER_TARGETS): start-%: @if [ -f $*/Makefile ]; then \ diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 77357c37..e4d2f954 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -11,7 +11,7 @@ build/s3ec-server: build-server: | build/s3ec-server @echo "Building Cpp V2 server..." - cd build && make + cd build && $(MAKE) start-server: @echo "Starting Cpp V2 server..." diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index a2b05810..1b2de659 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -16,8 +16,8 @@ using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> - client_cache; +std::unordered_map> client_cache_secret; +std::mutex client_mutex; std::string generate_uuid() { uuid_t uuid; @@ -27,6 +27,23 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + std::lock_guard lock(client_mutex); + auto it = client_cache_secret.find(client_id); + if (it == client_cache_secret.end()) { + return std::shared_ptr(); + } else { + return it->second; + } +} + +void set_client(const std::string &client_id, std::shared_ptr client) +{ + std::lock_guard lock(client_mutex); + client_cache_secret[client_id] = client; +} + std::string get_header_value(struct MHD_Connection *connection, const char *key) { const char *value = @@ -74,7 +91,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto encryption_client = std::make_shared(config); std::string client_id = generate_uuid(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); @@ -144,8 +161,8 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, const std::string &bucket, const std::string &key, const std::string &client_id, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -154,18 +171,9 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - // S3EncryptionGetObjectOutcome outcome ; - // if (metadata.empty()) { - // outcome = it->second->GetObject(request); - // } else { - // Aws::Map kmsContextMap; - // fill_context(kmsContextMap, metadata); - // outcome = it->second->GetObject(request, kmsContextMap); - // } - Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); - auto outcome = it->second->GetObject(request, kmsContextMap); + auto outcome = client->GetObject(request, kmsContextMap); if (outcome.IsSuccess()) { auto &stream = outcome.GetResult().GetBody(); @@ -187,8 +195,8 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, const std::string &client_id, const std::string &body, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -203,7 +211,7 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, auto stream = std::make_shared(body); request.SetBody(stream); - auto outcome = it->second->PutObject(request, kmsContextMap); + auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 16b70796..2ca6ee5f 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -10,7 +10,7 @@ build/s3ec-server: build-server: | build/s3ec-server @echo "Building Cpp transition server..." - cd build && make + cd build && $(MAKE) start-server: @echo "Starting Cpp transition server..." diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 1fcedc3c..3514b594 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -16,8 +18,8 @@ using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> - client_cache; +std::unordered_map> client_cache_secret; +std::mutex client_mutex; std::string generate_uuid() { uuid_t uuid; @@ -27,6 +29,23 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + std::lock_guard lock(client_mutex); + auto it = client_cache_secret.find(client_id); + if (it == client_cache_secret.end()) { + return std::shared_ptr(); + } else { + return it->second; + } +} + +void set_client(const std::string &client_id, std::shared_ptr client) +{ + std::lock_guard lock(client_mutex); + client_cache_secret[client_id] = client; +} + std::string get_header_value(struct MHD_Connection *connection, const char *key) { const char *value = @@ -79,7 +98,41 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, return MHD_YES; } - std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; + // Extract all key material types + std::string kms_key_id; + std::string rsa_key_blob; + std::string aes_key_blob; + + if (request["config"]["keyMaterial"].contains("kmsKeyId") && + !request["config"]["keyMaterial"]["kmsKeyId"].is_null()) { + kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; + } + if (request["config"]["keyMaterial"].contains("rsaKey") && + !request["config"]["keyMaterial"]["rsaKey"].is_null()) { + rsa_key_blob = request["config"]["keyMaterial"]["rsaKey"]; + } + if (request["config"]["keyMaterial"].contains("aesKey") && + !request["config"]["keyMaterial"]["aesKey"].is_null()) { + aes_key_blob = request["config"]["keyMaterial"]["aesKey"]; + } + + // Validate that only one key type is provided + int key_count = 0; + if (!kms_key_id.empty()) key_count++; + if (!rsa_key_blob.empty()) key_count++; + if (!aes_key_blob.empty()) key_count++; + + if (key_count != 1) { + return send_response(connection, 400, + "{\"error\":\"KeyMaterial must contain exactly one non-null key type\"}"); + } + + // RSA is not supported by C++ SDK + if (!rsa_key_blob.empty()) { + return send_response(connection, 501, + "{\"error\":\"RSA key wrapping is not supported in C++ S3 Encryption Client\"}"); + } + bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; bool inst_put = false; @@ -88,8 +141,33 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - auto materials = - std::make_shared(kms_key_id); + // Create appropriate encryption materials based on key type + std::shared_ptr materials; + + if (!aes_key_blob.empty()) { + // Base64 decode the AES key + auto decoded = Aws::Utils::Base64::Decode(aes_key_blob); + if (!decoded.IsSuccess()) { + return send_response(connection, 400, + "{\"error\":\"Failed to decode AES key\"}"); + } + + Aws::Utils::CryptoBuffer key_buffer( + decoded.GetResult().GetUnderlyingData(), + decoded.GetResult().GetLength() + ); + + materials = std::make_shared< + Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( + key_buffer + ); + } else if (!kms_key_id.empty()) { + materials = std::make_shared(kms_key_id); + } else { + return send_response(connection, 400, + "{\"error\":\"No valid key material provided\"}"); + } + CryptoConfigurationV2 config(materials); if (legacy1 || legacy2) config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); @@ -99,7 +177,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto encryption_client = std::make_shared(config); std::string client_id = generate_uuid(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); @@ -169,8 +247,8 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, const std::string &bucket, const std::string &key, const std::string &client_id, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -179,18 +257,9 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - // S3EncryptionGetObjectOutcome outcome ; - // if (metadata.empty()) { - // outcome = it->second->GetObject(request); - // } else { - // Aws::Map kmsContextMap; - // fill_context(kmsContextMap, metadata); - // outcome = it->second->GetObject(request, kmsContextMap); - // } - Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); - auto outcome = it->second->GetObject(request, kmsContextMap); + auto outcome = client->GetObject(request, kmsContextMap); if (outcome.IsSuccess()) { auto &stream = outcome.GetResult().GetBody(); @@ -212,8 +281,8 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, const std::string &client_id, const std::string &body, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -228,7 +297,7 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, auto stream = std::make_shared(body); request.SetBody(stream); - auto outcome = it->second->PutObject(request, kmsContextMap); + auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); diff --git a/test-server/cpp-v3-server/CMakeLists.txt b/test-server/cpp-v3-server/CMakeLists.txt index b282dbc4..0faac5f0 100644 --- a/test-server/cpp-v3-server/CMakeLists.txt +++ b/test-server/cpp-v3-server/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 17) set(BUILD_ONLY "kms;s3;s3-encryption" CACHE STRING "Build only KMS, S3, and S3-encryption components") set(ENABLE_TESTING OFF CACHE BOOL "Disable testing") set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static libraries") +set(ENABLE_ADDRESS_SANITIZER ON CACHE BOOL "Enable Address Sanitizer") # Add AWS SDK as subdirectory add_subdirectory(aws-sdk-cpp) @@ -18,12 +19,21 @@ find_package(nlohmann_json REQUIRED) add_executable(s3ec-server main.cpp) -target_include_directories(s3ec-server PRIVATE +# Enable Address Sanitizer for the executable +target_compile_options(s3ec-server PRIVATE -fsanitize=address -fno-omit-frame-pointer) +target_link_options(s3ec-server PRIVATE -fsanitize=address) + +target_include_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_INCLUDE_DIRS} + /opt/homebrew/include +) + +target_include_directories(s3ec-server PRIVATE ${LIBMICROHTTPD_INCLUDE_DIRS} /opt/homebrew/include ) -target_link_directories(s3ec-server PRIVATE +target_link_directories(s3ec-server PRIVATE ${LIBMICROHTTPD_LIBRARY_DIRS} /opt/homebrew/lib ) @@ -36,4 +46,4 @@ target_link_libraries(s3ec-server aws-cpp-sdk-s3-encryption nlohmann_json::nlohmann_json uuid -) \ No newline at end of file +) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 46f0c9db..05a286f0 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -10,7 +10,7 @@ build/s3ec-server: build-server: | build/s3ec-server @echo "Building Cpp V3 server..." - cd build && make + cd build && $(MAKE) start-server: @echo "Starting Cpp V3 server..." diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 1f74974c..21360b90 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -12,12 +14,13 @@ #include #include #include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> - client_cache; +std::unordered_map> client_cache_secret; +std::mutex client_mutex; std::string generate_uuid() { uuid_t uuid; @@ -27,6 +30,23 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + std::lock_guard lock(client_mutex); + auto it = client_cache_secret.find(client_id); + if (it == client_cache_secret.end()) { + return std::shared_ptr(); + } else { + return it->second; + } +} + +void set_client(const std::string &client_id, std::shared_ptr client) +{ + std::lock_guard lock(client_mutex); + client_cache_secret[client_id] = client; +} + std::string get_header_value(struct MHD_Connection *connection, const char *key) { const char *value = @@ -69,7 +89,42 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { try { json request = json::parse(body); - std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; + + // Extract all key material types + std::string kms_key_id; + std::string rsa_key_blob; + std::string aes_key_blob; + + if (request["config"]["keyMaterial"].contains("kmsKeyId") && + !request["config"]["keyMaterial"]["kmsKeyId"].is_null()) { + kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; + } + if (request["config"]["keyMaterial"].contains("rsaKey") && + !request["config"]["keyMaterial"]["rsaKey"].is_null()) { + rsa_key_blob = request["config"]["keyMaterial"]["rsaKey"]; + } + if (request["config"]["keyMaterial"].contains("aesKey") && + !request["config"]["keyMaterial"]["aesKey"].is_null()) { + aes_key_blob = request["config"]["keyMaterial"]["aesKey"]; + } + + // Validate that only one key type is provided + int key_count = 0; + if (!kms_key_id.empty()) key_count++; + if (!rsa_key_blob.empty()) key_count++; + if (!aes_key_blob.empty()) key_count++; + + if (key_count != 1) { + return send_response(connection, 400, + "{\"error\":\"KeyMaterial must contain exactly one non-null key type\"}"); + } + + // RSA is not supported by C++ SDK + if (!rsa_key_blob.empty()) { + return send_response(connection, 501, + "{\"error\":\"RSA key wrapping is not supported in C++ S3 Encryption Client\"}"); + } + bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; bool inst_put = false; @@ -78,9 +133,43 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - auto materials = - std::make_shared(kms_key_id); - CryptoConfigurationV3 config(materials); + // Create appropriate encryption materials based on key type + std::shared_ptr materials; + + if (!aes_key_blob.empty()) { + // Base64 decode the AES key + auto decoded = Aws::Utils::Base64::Decode(aes_key_blob); + if (!decoded.IsSuccess()) { + return send_response(connection, 400, + "{\"error\":\"Failed to decode AES key\"}"); + } + + Aws::Utils::CryptoBuffer key_buffer( + decoded.GetResult().GetUnderlyingData(), + decoded.GetResult().GetLength() + ); + + materials = std::make_shared< + Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( + key_buffer + ); + } else if (!kms_key_id.empty()) { + materials = std::make_shared(kms_key_id); + } else { + return send_response(connection, 400, + "{\"error\":\"No valid key material provided\"}"); + } + + // Configure ClientConfiguration with retry strategy for throttling + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 25; + clientConfig.retryStrategy = Aws::MakeShared( + "S3EncryptionClient", + 5 // maxRetries - will use exponential backoff for throttling + ); + + CryptoConfigurationV3 config(materials); + config.SetClientConfiguration(clientConfig); if (legacy1 || legacy2) config.AllowLegacy(); if (inst_put) @@ -104,7 +193,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto encryption_client = std::make_shared(config); std::string client_id = generate_uuid(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); @@ -175,8 +264,8 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, const std::string &bucket, const std::string &key, const std::string &client_id, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -185,18 +274,9 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - // S3EncryptionGetObjectOutcome outcome ; - // if (metadata.empty()) { - // outcome = it->second->GetObject(request); - // } else { - // Aws::Map kmsContextMap; - // fill_context(kmsContextMap, metadata); - // outcome = it->second->GetObject(request, kmsContextMap); - // } - Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); - auto outcome = it->second->GetObject(request, kmsContextMap); + auto outcome = client->GetObject(request, kmsContextMap); if (outcome.IsSuccess()) { auto &stream = outcome.GetResult().GetBody(); @@ -220,8 +300,8 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, const std::string &client_id, const std::string &body, const std::string &metadata) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + auto client = get_client(client_id); + if (!client) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -236,7 +316,7 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, auto stream = std::make_shared(body); request.SetBody(stream); - auto outcome = it->second->PutObject(request, kmsContextMap); + auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); diff --git a/test-server/go-v3-server/main.go b/test-server/go-v3-server/main.go index d201ffe2..a79f007e 100644 --- a/test-server/go-v3-server/main.go +++ b/test-server/go-v3-server/main.go @@ -66,7 +66,12 @@ type ErrorResponse struct { // NewServer creates a new server instance func NewServer() (*Server, error) { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) } @@ -141,7 +146,12 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { return } - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) return diff --git a/test-server/go-v3-transition-server/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec index f51a4402..e59a38ca 160000 --- a/test-server/go-v3-transition-server/local-go-s3ec +++ b/test-server/go-v3-transition-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit f51a4402c741cd989c7984336de560e9c54baf17 +Subproject commit e59a38caeddfcfbf41e064e125b5783cdfce3878 diff --git a/test-server/go-v3-transition-server/main.go b/test-server/go-v3-transition-server/main.go index 799a9668..3c92582e 100644 --- a/test-server/go-v3-transition-server/main.go +++ b/test-server/go-v3-transition-server/main.go @@ -68,7 +68,12 @@ type ErrorResponse struct { // NewServer creates a new server instance func NewServer() (*Server, error) { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) } @@ -143,7 +148,12 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { return } - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) return diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index f51a4402..e59a38ca 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit f51a4402c741cd989c7984336de560e9c54baf17 +Subproject commit e59a38caeddfcfbf41e064e125b5783cdfce3878 diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go index 672236ac..d9d897aa 100644 --- a/test-server/go-v4-server/main.go +++ b/test-server/go-v4-server/main.go @@ -68,7 +68,12 @@ type ErrorResponse struct { // NewServer creates a new server instance func NewServer() (*Server, error) { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) } @@ -143,7 +148,12 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { return } - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-west-2"), + config.WithRetryMaxAttempts(5), + config.WithRetryMode(aws.RetryModeAdaptive), + ) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) return diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 106f82ef..14c3eec1 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -16,6 +16,9 @@ dependencies { // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:5.13.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + // JUnit Suite support for test ordering + testImplementation("org.junit.platform:junit-platform-suite-api:1.10.0") + testRuntimeOnly("org.junit.platform:junit-platform-suite-engine:1.10.0") testImplementation("com.amazonaws:aws-java-sdk:1.12.788") testImplementation("software.amazon.awssdk:s3:2.37.1") testImplementation("org.bouncycastle:bcprov-jdk15on:1.70") @@ -49,6 +52,17 @@ tasks { classpath = sourceSets["it"].runtimeClasspath outputs.upToDateWhen { false } outputs.cacheIf { false } + + // Enable parallel test execution + systemProperty("junit.jupiter.execution.parallel.enabled", "true") + systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") + // Configure thread pool size - adjust based on I/O-bound nature of tests + systemProperty("junit.jupiter.execution.parallel.config.strategy", "fixed") + maxParallelForks = 1 // One JVM + systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", + Math.max(1, Runtime.getRuntime().availableProcessors() - 2).toString()) // Scale with CPU, reserve 2 cores + // Passing information from Gradle into the tests so that we can filter our servers systemProperty("test.filter.servers", System.getProperty("test.filter.servers")) // For debugging diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java new file mode 100644 index 00000000..4be4d434 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java @@ -0,0 +1,255 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import static software.amazon.encryption.s3.TestUtils.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.encryption.s3.client.S3ECTestServerClient; +import software.amazon.encryption.s3.model.CommitmentPolicy; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.EncryptionAlgorithm; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.model.S3ECConfig; + +/** + * GCM Test Suite + * + * This suite enforces execution order between GCM encrypt and decrypt phases: + * 1. EncryptTests - All encrypt tests run in parallel (within this phase) + * 2. DecryptTests - Waits for encrypt phase to complete, then all decrypt tests run in parallel + * + * Coordination is achieved using a CountDownLatch that EncryptTests signals upon completion + * and DecryptTests awaits before proceeding. + */ +public class GCMTestSuite { + // Synchronization latch - released when encrypt phase completes + private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + + /** + * GCM Encryption Tests - Encrypt Phase + * + * These tests encrypt objects using GCM (without key commitment) encryption algorithm. + * All tests in this class can run in parallel with each other. + * The encrypted objects are stored in thread-safe lists for use by DecryptTests. + */ + @Nested + class EncryptTests { + private static final String sharedObjectKeyBase = "test-gcm-kms"; + private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + // Thread-safe list for storing encrypted object keys + private static final List crossLanguageObjects = + Collections.synchronizedList(new ArrayList<>()); + + /** + * Public accessor for decrypt tests to retrieve encrypted object keys + */ + static List getCrossLanguageObjects() { + return new ArrayList<>(crossLanguageObjects); // Return defensive copy + } + + @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should encrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_forbid_encrypt_allow_decrypt_should_encrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), + crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Transition configured with the default should encrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_the_default_should_encrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), + crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should encrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_forbid_encrypt_allow_decrypt_should_encrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), + crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @AfterAll + static void signalEncryptionComplete() { + // Signal that all encryption tests have completed + encryptPhaseComplete.countDown(); + } + } + + /** + * GCM Decryption Tests - Decrypt Phase + * + * These tests decrypt objects that were encrypted by EncryptTests. + * All tests in this class can run fully in parallel with each other. + * They depend on EncryptTests completing first (enforced by @Order). + */ + @Nested + class DecryptTests { + private static List crossLanguageObjects; + private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + @BeforeAll + static void setup() throws InterruptedException { + // Wait for all encryption tests to complete + encryptPhaseComplete.await(); + + // Import encrypted objects from the encrypt phase + crossLanguageObjects = EncryptTests.getCrossLanguageObjects(); + + // Verify we have objects to decrypt + if (crossLanguageObjects.isEmpty()) { + throw new IllegalStateException( + "No encrypted objects found. Ensure EncryptTests runs first."); + } + } + + @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should decrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Transition configured with the default should decrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_the_default_should_decrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should decrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should decrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should fail to decrypt GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_fail_to_decrypt_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails(client, S3ECId, crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + } +} diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTests.java deleted file mode 100644 index 6eef0b5f..00000000 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTests.java +++ /dev/null @@ -1,203 +0,0 @@ -/* -* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -* SPDX-License-Identifier: Apache-2.0 -*/ - -package software.amazon.encryption.s3; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static software.amazon.encryption.s3.TestUtils.*; - -import java.lang.annotation.ElementType; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import com.amazonaws.services.s3.model.KMSEncryptionMaterials; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import software.amazon.encryption.s3.client.S3ECTestServerClient; -import software.amazon.encryption.s3.model.CommitmentPolicy; -import software.amazon.encryption.s3.model.CreateClientInput; -import software.amazon.encryption.s3.model.CreateClientOutput; -import software.amazon.encryption.s3.model.EncryptionAlgorithm; -import software.amazon.encryption.s3.model.GetObjectInput; -import software.amazon.encryption.s3.model.GetObjectOutput; -import software.amazon.encryption.s3.model.KeyMaterial; -import software.amazon.encryption.s3.model.PutObjectInput; -import software.amazon.encryption.s3.model.S3ECConfig; -import software.amazon.encryption.s3.model.S3EncryptionClientError; - -import com.amazonaws.services.s3.AmazonS3Encryption; -import com.amazonaws.services.s3.AmazonS3EncryptionClient; -import com.amazonaws.services.s3.model.CryptoConfiguration; -import com.amazonaws.services.s3.model.CryptoMode; -import com.amazonaws.services.s3.model.CryptoStorageMode; -import software.amazon.encryption.s3.TestUtils.*; -import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; -import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; - -/** -* Exhaustive tests for S3 Encryption Client round-trip operations. -* These tests cover various combinations of client versions, commitment policies, and encryption modes. -* -* Tests are based on the exhaustive test matrix defined at: -* https://tiny.amazon.com/3xnzwczl/loopcloumicrpeyJ3 -* -*/ - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class GCMTests { - private static String sharedObjectKeyBase = "test-gcm-kms"; - private static KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) - .build(); - private static List crossLanguageObjects = new ArrayList<>(); - - @Order(1) - @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should encrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_forbid_encrypt_allow_decrypt_should_encrypt_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(2) - @ParameterizedTest(name = "{0}: Transition configured with the default should encrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_the_default_should_encrypt_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(3) - @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should encrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_forbid_encrypt_allow_decrypt_should_encrypt_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(10) - @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should decrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(11) - @ParameterizedTest(name = "{0}: Transition configured with the default should decrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_the_default_should_decrypt_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(12) - @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should decrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(13) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should decrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - @Order(14) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should fail to decrypt GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_require_decrypt_should_fail_to_decrypt_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - -} diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java new file mode 100644 index 00000000..c367315f --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java @@ -0,0 +1,389 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import static software.amazon.encryption.s3.TestUtils.*; + +import java.nio.ByteBuffer; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.TestAbortedException; +import software.amazon.encryption.s3.client.S3ECTestServerClient; +import software.amazon.encryption.s3.model.CommitmentPolicy; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.EncryptionAlgorithm; +import software.amazon.encryption.s3.model.InstructionFileConfig; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.model.S3ECConfig; + +/** + * KC-GCM Test Suite + * + * This suite enforces execution order between KC-GCM encrypt and decrypt phases: + * 1. EncryptTests - All encrypt tests run in parallel (within this phase) + * 2. DecryptTests - Waits for encrypt phase to complete, then all decrypt tests run in parallel + * + * Coordination is achieved using a CountDownLatch that EncryptTests signals upon completion + * and DecryptTests awaits before proceeding. + */ +public class KC_GCMTestSuite { + // Synchronization latch - released when encrypt phase completes + private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + + /** + * KC-GCM Encryption Tests - Encrypt Phase + * + * These tests encrypt objects using Key Commitment GCM encryption algorithm. + * All tests in this class can run in parallel with each other. + * The encrypted objects are stored in thread-safe lists for use by DecryptTests. + */ + @Nested + class EncryptTests { + private static final String sharedObjectKeyBaseMetaDataMode = "test-kc-gcm-kms"; + private static final String sharedObjectKeyBaseInsFileMode = "test-kc-gcm-rsa-instruction-file"; + private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + // Thread-safe lists for storing encrypted object keys + private static final List crossLanguageObjectsMetaDataMode = + Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsInstructionFiles = + Collections.synchronizedList(new ArrayList<>()); + + private static KeyPair RSA_KEY_PAIR_1; + + @BeforeAll + static void setupKeys() throws Exception { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + } + + /** + * Public accessors for decrypt tests to retrieve encrypted object keys and RSA key + */ + static List getCrossLanguageObjectsMetaDataMode() { + return new ArrayList<>(crossLanguageObjectsMetaDataMode); + } + + static List getCrossLanguageObjectsInstructionFiles() { + return new ArrayList<>(crossLanguageObjectsInstructionFiles); + } + + static KeyPair getRsaKeyPair() { + return RSA_KEY_PAIR_1; + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should encrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_allow_decrypt_should_encrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), + crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should encrypt KC-GCM (instruction file)") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm_ins_file( + TestUtils.LanguageServerTarget language + ) { + if (!RAW_SUPPORTED.contains(language.getLanguageName())) { + throw new TestAbortedException("Not encrypting raw keyring with: " + language.getLanguageName()); + } + + KeyMaterial rsaKey = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) + .build(); + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .keyMaterial(rsaKey).build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBaseInsFileMode + language.getLanguageName()), + crossLanguageObjectsInstructionFiles, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should encrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), + crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with the default should encrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_the_default_should_encrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + // .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), + crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @AfterAll + static void signalEncryptionComplete() { + // Signal that all encryption tests have completed + encryptPhaseComplete.countDown(); + } + } + + /** + * KC-GCM Decryption Tests - Decrypt Phase + * + * These tests decrypt objects that were encrypted by EncryptTests. + * All tests in this class can run fully in parallel with each other. + * They depend on EncryptTests completing first (enforced by @Order). + */ + @Nested + class DecryptTests { + private static List crossLanguageObjectsMetaDataMode; + private static List crossLanguageObjectsInstructionFiles; + private static KeyPair RSA_KEY_PAIR_1; + private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + @BeforeAll + static void setup() throws InterruptedException { + // Wait for all encryption tests to complete + encryptPhaseComplete.await(); + + // Import encrypted objects and RSA key from the encrypt phase + crossLanguageObjectsMetaDataMode = EncryptTests.getCrossLanguageObjectsMetaDataMode(); + crossLanguageObjectsInstructionFiles = EncryptTests.getCrossLanguageObjectsInstructionFiles(); + RSA_KEY_PAIR_1 = EncryptTests.getRsaKeyPair(); + + // Verify we have objects to decrypt + if (crossLanguageObjectsMetaDataMode.isEmpty()) { + throw new IllegalStateException( + "No encrypted objects found. Ensure EncryptTests runs first."); + } + } + + @ParameterizedTest(name = "{0}: Transition configured with the default should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_the_default_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with the default should decrypt KC-GCM") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_the_default_should_decrypt_kc_gcm( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + // .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should decrypt KC-GCM (instruction file)") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm_ins_file( + final TestUtils.LanguageServerTarget language + ) { + if (!RAW_SUPPORTED.contains(language.getLanguageName())) { + throw new TestAbortedException("Not encrypting raw keyring with: " + language.getLanguageName()); + } + + KeyMaterial rsaKey = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) + .build(); + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .keyMaterial(rsaKey).build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsInstructionFiles, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Transition configured with default should decrypt KC-GCM (instruction file)") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm_ins_file( + final TestUtils.LanguageServerTarget language + ) { + if (!RAW_SUPPORTED.contains(language.getLanguageName())) { + throw new TestAbortedException("Not encrypting raw keyring with: " + language.getLanguageName()); + } + + KeyMaterial rsaKey = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) + .build(); + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .keyMaterial(rsaKey).build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsInstructionFiles, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + } +} diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java deleted file mode 100644 index ee4279d6..00000000 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java +++ /dev/null @@ -1,264 +0,0 @@ -/* -* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -* SPDX-License-Identifier: Apache-2.0 -*/ - -package software.amazon.encryption.s3; - -import static software.amazon.encryption.s3.TestUtils.*; - -import java.nio.ByteBuffer; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.opentest4j.TestAbortedException; -import software.amazon.encryption.s3.client.S3ECTestServerClient; -import software.amazon.encryption.s3.model.CommitmentPolicy; -import software.amazon.encryption.s3.model.CreateClientInput; -import software.amazon.encryption.s3.model.CreateClientOutput; -import software.amazon.encryption.s3.model.EncryptionAlgorithm; -import software.amazon.encryption.s3.model.InstructionFileConfig; -import software.amazon.encryption.s3.model.KeyMaterial; -import software.amazon.encryption.s3.model.S3ECConfig; - -/** -* Exhaustive tests for S3 Encryption Client round-trip operations. -* These tests cover various combinations of client versions, commitment policies, and encryption modes. -* -* Tests are based on the exhaustive test matrix defined at: -* https://tiny.amazon.com/3xnzwczl/loopcloumicrpeyJ3 -* -*/ - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class KC_GCMTests { - private static final String sharedObjectKeyBaseMetaDataMode = "test-kc-gcm-kms"; - private static final String sharedObjectKeyBaseInsFileMode = "test-kc-gcm-kms-instruction-file"; - private static KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) - .build(); - private static final List crossLanguageObjectsMetaDataMode = new ArrayList<>(); - private static final List crossLanguageObjectsInstructionFiles = new ArrayList<>(); - private static KeyPair RSA_KEY_PAIR_1; - - @BeforeAll - static void setupKeys() throws Exception { - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); - keyPairGen.initialize(2048); - RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); - } - - @Order(1) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should encrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_allow_decrypt_should_encrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(2) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should encrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm_ins_file(TestUtils.LanguageServerTarget language) { - if (!RAW_SUPPORTED.contains(language.getLanguageName())) { - throw new TestAbortedException("Not encrypting raw keyring with: " + language.getLanguageName()); - } - - KeyMaterial rsaKey = KeyMaterial.builder() - .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) - .build(); - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .instructionFileConfig(InstructionFileConfig.builder() - .enableInstructionFilePutObject(true) - .build()) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .keyMaterial(rsaKey).build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseInsFileMode + language.getLanguageName()), crossLanguageObjectsInstructionFiles, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(2) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should encrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(2) - @ParameterizedTest(name = "{0}: Improved configured with the default should encrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_the_default_should_encrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - // .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(10) - @ParameterizedTest(name = "{0}: Transition configured with the default should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_the_default_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(11) - @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") - void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(12) - @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(13) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(14) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(15) - @ParameterizedTest(name = "{0}: Improved configured with the default should decrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_the_default_should_decrypt_kc_gcm(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - // .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - - @Order(16) - @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should encrypt KC-GCM") - @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") - void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm_ins_file(final TestUtils.LanguageServerTarget language) { - if (!RAW_SUPPORTED.contains(language.getLanguageName())) { - throw new TestAbortedException("Not encrypting raw keyring with: " + language.getLanguageName()); - } - - KeyMaterial rsaKey = KeyMaterial.builder() - .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) - .build(); - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .instructionFileConfig(InstructionFileConfig.builder() - .enableInstructionFilePutObject(true) - .build()) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - .keyMaterial(rsaKey).build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsInstructionFiles, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); - } - -} diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java index 468fc708..2e946030 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java @@ -214,9 +214,9 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa fail("Expected exception!"); } catch (S3EncryptionClientError e) { if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { - assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); + assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context"), "Actual error: " + e.getMessage()); } else { - assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3"), "Actual error: " + e.getMessage()); } } } @@ -278,9 +278,9 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en fail("Expected exception!"); } catch (S3EncryptionClientError e) { if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { - assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); + assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context"), "Actual error: " + e.getMessage()); } else { - assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3"), "Actual error: " + e.getMessage()); } } } @@ -427,15 +427,15 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la || language.getLanguageName().equals(CPP_V2_CURRENT) || language.getLanguageName().equals(CPP_V2_TRANSITION) || language.getLanguageName().equals(CPP_V3)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" - )); + ), "Actual error:" + e.getMessage()); } else if (language.getLanguageName().equals(RUBY_V3) || language.getLanguageName().equals(RUBY_V2_CURRENT) || language.getLanguageName().equals(RUBY_V2_TRANSITION)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration security_profile = :v2. Retry with :v2_and_legacy or re-encrypt the object." ), "Actual error:" + e.getMessage()); } else if (language.getLanguageName().equals(PHP_V3)) { - assertTrue(e.getMessage().contains("The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration @SecurityProfile=V3. Retry with V3_AND_LEGACY enabled or reencrypt the object."));; + assertTrue(e.getMessage().contains("The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration @SecurityProfile=V3. Retry with V3_AND_LEGACY enabled or reencrypt the object."), "Actual error: " + e.getMessage()); } else { - assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); + assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"), "Actual error: " + e.getMessage()); } } } @@ -665,7 +665,7 @@ public void instructionFileWriteAndReadWithRSA(LanguageServerTarget encLang, Lan .key(objectKey + ".instruction") .build()); } - assertTrue(ptInstFile.response().metadata().containsKey("x-amz-crypto-instr-file")); + // assertTrue(ptInstFile.response().metadata().containsKey("x-amz-crypto-instr-file")); assertFalse(ptInstFile.asUtf8String().isEmpty()); // Read should be enabled by default GetObjectOutput output = decClient.getObject(GetObjectInput.builder() diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java index 90995dde..a7a86c1d 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java @@ -541,6 +541,8 @@ public static void Decrypt( Decrypt(client, S3ECId, crossLanguageObjects, expectedEncryptionAlgorithm, crossLanguageObjects); } + + public static void Decrypt( S3ECTestServerClient client, String S3ECId, @@ -548,23 +550,39 @@ public static void Decrypt( EncryptionAlgorithm expectedEncryptionAlgorithm, List expectedPlaintexts ) { + List failures = new ArrayList<>(); for (int i = 0; i < crossLanguageObjects.size(); i++) { - String objectKey = crossLanguageObjects.get(i); - String expectedPlaintext = expectedPlaintexts.get(i); - - GetObjectOutput output = client.getObject(GetObjectInput.builder() - .clientID(S3ECId) - .bucket(TestUtils.BUCKET) - .key(objectKey) - .build()); + try { + String objectKey = crossLanguageObjects.get(i); + String expectedPlaintext = expectedPlaintexts.get(i); + + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); - // Then: Pass - assertEquals(expectedPlaintext, new String(output.getBody().array())); - assertEquals( - expectedEncryptionAlgorithm, - GetEncryptionAlgorithm(objectKey), - "When decrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm - ); + // Then: Pass + assertEquals(expectedPlaintext, new String(output.getBody().array())); + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "When decrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm + ); + } catch (Exception e) { + failures.add(String.format( + "Failed to decrypt object '%s' (index %d): %s - %s", + crossLanguageObjects.get(i), i, e.getClass().getSimpleName(), e.getMessage() + )); + } + } + + if (!failures.isEmpty()) { + throw new AssertionError(String.format( + "Decryption failed for %d out of %d objects:\n%s", + failures.size(), crossLanguageObjects.size(), + String.join("\n", failures) + )); } } diff --git a/test-server/java-v3-server/gradle.properties b/test-server/java-v3-server/gradle.properties index 08afce82..483cd315 100644 --- a/test-server/java-v3-server/gradle.properties +++ b/test-server/java-v3-server/gradle.properties @@ -4,8 +4,21 @@ smithyGradleVersion=1.1.0 smithyVersion=[1,2] # Performance optimization settings + +# Force no-daemon mode - ensures Gradle doesn't try to keep a daemon alive +org.gradle.daemon=false + +# Set minimal idle timeout for any daemon-like behavior (1 second) +org.gradle.daemon.idletimeout=1000 + +# JVM arguments to prevent forking a separate JVM process +# By matching the JVM args here with what Gradle expects, we avoid the +# "single-use Daemon process will be forked" behavior +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC + +# Keep builds fast with parallel execution and caching org.gradle.parallel=true org.gradle.caching=true -org.gradle.daemon=true -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -org.gradle.workers.max=4 + +# Configure on demand to reduce startup time +org.gradle.configureondemand=true diff --git a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java index 1d198590..bdb0b30b 100644 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java +++ b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -1,5 +1,8 @@ package software.amazon.encryption.s3; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.S3EncryptionClient; @@ -106,12 +109,25 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c throw new RuntimeException("No KeyMaterial found!"); } + // Configure S3 client with adaptive retry for throttling + RetryPolicy retryPolicy = RetryPolicy.builder() + .numRetries(5) + .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) + .build(); + + S3Client wrappedClient = S3Client.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(retryPolicy) + .build()) + .build(); + // Client Creation boolean instFilePut = false; if (input.getConfig().getInstructionFileConfig() != null) { instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); } S3Client s3Client = S3EncryptionClient.builder() + .wrappedClient(wrappedClient) .instructionFileConfig(InstructionFileConfig.builder() .instructionFileClient(S3Client.create()) .enableInstructionFilePutObject(instFilePut) diff --git a/test-server/java-v3-transition-server/gradle.properties b/test-server/java-v3-transition-server/gradle.properties index 08afce82..483cd315 100644 --- a/test-server/java-v3-transition-server/gradle.properties +++ b/test-server/java-v3-transition-server/gradle.properties @@ -4,8 +4,21 @@ smithyGradleVersion=1.1.0 smithyVersion=[1,2] # Performance optimization settings + +# Force no-daemon mode - ensures Gradle doesn't try to keep a daemon alive +org.gradle.daemon=false + +# Set minimal idle timeout for any daemon-like behavior (1 second) +org.gradle.daemon.idletimeout=1000 + +# JVM arguments to prevent forking a separate JVM process +# By matching the JVM args here with what Gradle expects, we avoid the +# "single-use Daemon process will be forked" behavior +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC + +# Keep builds fast with parallel execution and caching org.gradle.parallel=true org.gradle.caching=true -org.gradle.daemon=true -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -org.gradle.workers.max=4 + +# Configure on demand to reduce startup time +org.gradle.configureondemand=true diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging index 6413811b..597d5b49 160000 --- a/test-server/java-v3-transition-server/s3ec-staging +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -1 +1 @@ -Subproject commit 6413811bb81037999b8238e02047e0e403f78c1f +Subproject commit 597d5b491ac578f5d03d3fa757201eb48690cd00 diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java index 425c0334..e107401e 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -1,5 +1,8 @@ package software.amazon.encryption.s3; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; @@ -106,9 +109,22 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c throw new RuntimeException("No KeyMaterial found!"); } + // Configure S3 client with adaptive retry for throttling + RetryPolicy retryPolicy = RetryPolicy.builder() + .numRetries(5) + .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) + .build(); + + S3Client wrappedClient = S3Client.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(retryPolicy) + .build()) + .build(); + // V3 Transition server configuration // Existing Builder defaults to FORBID_ENCRYPT and ALG_AES_256_GCM_IV12_TAG16_NO_KDF S3EncryptionClient.Builder s3ClientBuilder = S3EncryptionClient.builder() + .wrappedClient(wrappedClient) .keyring(keyring) .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()); diff --git a/test-server/java-v4-server/gradle.properties b/test-server/java-v4-server/gradle.properties index 08afce82..483cd315 100644 --- a/test-server/java-v4-server/gradle.properties +++ b/test-server/java-v4-server/gradle.properties @@ -4,8 +4,21 @@ smithyGradleVersion=1.1.0 smithyVersion=[1,2] # Performance optimization settings + +# Force no-daemon mode - ensures Gradle doesn't try to keep a daemon alive +org.gradle.daemon=false + +# Set minimal idle timeout for any daemon-like behavior (1 second) +org.gradle.daemon.idletimeout=1000 + +# JVM arguments to prevent forking a separate JVM process +# By matching the JVM args here with what Gradle expects, we avoid the +# "single-use Daemon process will be forked" behavior +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC + +# Keep builds fast with parallel execution and caching org.gradle.parallel=true org.gradle.caching=true -org.gradle.daemon=true -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -org.gradle.workers.max=4 + +# Configure on demand to reduce startup time +org.gradle.configureondemand=true diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging index db0c743e..2626ed6e 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit db0c743eec335d16e6dceaf2b09d84becb0f74f8 +Subproject commit 2626ed6e312c1c5e01abea2f30727ea0f2af299d diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java index cb20d5ac..3fdb4b55 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -1,5 +1,8 @@ package software.amazon.encryption.s3; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; @@ -106,8 +109,21 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c throw new RuntimeException("No KeyMaterial found!"); } + // Configure S3 client with adaptive retry for throttling + RetryPolicy retryPolicy = RetryPolicy.builder() + .numRetries(5) + .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) + .build(); + + S3Client wrappedClient = S3Client.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(retryPolicy) + .build()) + .build(); + // V4-Improved server configuration S3EncryptionClient.Builder s3ClientBuilder = S3EncryptionClient.builderV4() + .wrappedClient(wrappedClient) .keyring(keyring) .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()); diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index e33a58e6..437233a8 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -21,8 +21,6 @@ public IActionResult CreateClient([FromBody] ClientRequest request) return StatusCode(501, new GenericServerError { Message = "[NET-current] EnableDelayedAuthenticationMode not supported" }); if (request.Config.SetBufferSize.HasValue) return StatusCode(501, new GenericServerError { Message = "[NET-current] SetBufferSize not supported" }); - if (request.Config.KeyMaterial.AesKey != null) - return StatusCode(501, new GenericServerError { Message = "[NET-current] AesKey not supported" }); try { @@ -47,6 +45,15 @@ public IActionResult CreateClient([FromBody] ClientRequest request) encryptionMaterial = new EncryptionMaterialsV2(rsaKey, AsymmetricAlgorithmType.RsaOaepSha1); logger.LogInformation( "Created EncryptionMaterialsV2: RSA"); + } + else if (request.Config.KeyMaterial.AesKey != null) + { + var aesKeyBytes = request.Config.KeyMaterial.AesKey; + var aes = Aes.Create(); + aes.Key = aesKeyBytes; + encryptionMaterial = new EncryptionMaterialsV2(aes, SymmetricAlgorithmType.AesGcm); + logger.LogInformation( + "[NET-current] Created EncryptionMaterialsV2: AES"); } else { return StatusCode(501, new GenericServerError { Message = "[NET-current] Unknown or missing key material!" }); @@ -62,6 +69,11 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation("[NET-current] Created securityProfile= {securityProfile}", securityProfile.ToString()); var configuration = new AmazonS3CryptoConfigurationV2(securityProfile); + + // Add retry configuration for throttling + configuration.RetryMode = Amazon.Runtime.RequestRetryMode.Adaptive; + configuration.MaxErrorRetry = 5; + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) { configuration.StorageMode = CryptoStorageMode.InstructionFile; @@ -91,4 +103,4 @@ public IActionResult CreateClient([FromBody] ClientRequest request) }); } } -} \ No newline at end of file +} diff --git a/test-server/net-v3-transition-server/Controllers/ClientController.cs b/test-server/net-v3-transition-server/Controllers/ClientController.cs index a66fb342..3deeff61 100644 --- a/test-server/net-v3-transition-server/Controllers/ClientController.cs +++ b/test-server/net-v3-transition-server/Controllers/ClientController.cs @@ -21,8 +21,6 @@ public IActionResult CreateClient([FromBody] ClientRequest request) return StatusCode(501, new GenericServerError { Message = "EnableDelayedAuthenticationMode not supported" }); if (request.Config.SetBufferSize.HasValue) return StatusCode(501, new GenericServerError { Message = "SetBufferSize not supported" }); - if (request.Config.KeyMaterial.AesKey != null) - return StatusCode(501, new GenericServerError { Message = "AesKey not supported" }); try { @@ -47,6 +45,15 @@ public IActionResult CreateClient([FromBody] ClientRequest request) encryptionMaterial = new EncryptionMaterialsV2(rsaKey, AsymmetricAlgorithmType.RsaOaepSha1); logger.LogInformation( "Created EncryptionMaterialsV2: RSA"); + } + else if (request.Config.KeyMaterial.AesKey != null) + { + var aesKeyBytes = request.Config.KeyMaterial.AesKey; + var aes = Aes.Create(); + aes.Key = aesKeyBytes; + encryptionMaterial = new EncryptionMaterialsV2(aes, SymmetricAlgorithmType.AesGcm); + logger.LogInformation( + "[NET-V3-Transitional] Created EncryptionMaterialsV2: AES"); } else { return StatusCode(501, new GenericServerError { Message = "Unknown or missing key material!" }); @@ -67,6 +74,11 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation("[NET-V3-Transitional] Created encryptionAlgorithm= {encryptionAlgorithm}", encryptionAlgorithm); var configuration = new AmazonS3CryptoConfigurationV2(securityProfile, commitmentPolicy, encryptionAlgorithm); + + // Add retry configuration for throttling + configuration.RetryMode = Amazon.Runtime.RequestRetryMode.Adaptive; + configuration.MaxErrorRetry = 5; + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) { configuration.StorageMode = CryptoStorageMode.InstructionFile; @@ -118,4 +130,4 @@ private static ContentEncryptionAlgorithm MapEncryptionAlgorithm(Models.Encrypti _ => ContentEncryptionAlgorithm.AesGcm }; } -} \ No newline at end of file +} diff --git a/test-server/net-v3-transition-server/s3ec-v3-transition-branch b/test-server/net-v3-transition-server/s3ec-v3-transition-branch index ad825917..d099cfd1 160000 --- a/test-server/net-v3-transition-server/s3ec-v3-transition-branch +++ b/test-server/net-v3-transition-server/s3ec-v3-transition-branch @@ -1 +1 @@ -Subproject commit ad8259173de365a13e8b3932ee02493f599f597f +Subproject commit d099cfd151e2c61fb97dcd417828fb1dd5468b0c diff --git a/test-server/net-v4-server/Controllers/ClientController.cs b/test-server/net-v4-server/Controllers/ClientController.cs index b9fbe3f9..2ef8b921 100644 --- a/test-server/net-v4-server/Controllers/ClientController.cs +++ b/test-server/net-v4-server/Controllers/ClientController.cs @@ -20,8 +20,6 @@ public IActionResult CreateClient([FromBody] ClientRequest request) return StatusCode(501, new GenericServerError { Message = "[NET-V4] EnableDelayedAuthenticationMode not supported" }); if (request.Config.SetBufferSize.HasValue) return StatusCode(501, new GenericServerError { Message = "[NET-V4] SetBufferSize not supported" }); - if (request.Config.KeyMaterial.AesKey != null) - return StatusCode(501, new GenericServerError { Message = "[NET-V4] AesKey not supported" }); try { @@ -46,6 +44,15 @@ public IActionResult CreateClient([FromBody] ClientRequest request) encryptionMaterial = new EncryptionMaterialsV4(rsaKey, AsymmetricAlgorithmType.RsaOaepSha1); logger.LogInformation( "[NET-V4] Created EncryptionMaterialsV4: RSA"); + } + else if (request.Config.KeyMaterial.AesKey != null) + { + var aesKeyBytes = request.Config.KeyMaterial.AesKey; + var aes = Aes.Create(); + aes.Key = aesKeyBytes; + encryptionMaterial = new EncryptionMaterialsV4(aes, SymmetricAlgorithmType.AesGcm); + logger.LogInformation( + "[NET-V4] Created EncryptionMaterialsV4: AES"); } else { return StatusCode(501, new GenericServerError { Message = "[NET-V4] Unknown or missing key material!" }); @@ -79,6 +86,10 @@ public IActionResult CreateClient([FromBody] ClientRequest request) ? new AmazonS3CryptoConfigurationV4() : new AmazonS3CryptoConfigurationV4(securityProfile, commitmentPolicy, encryptionAlgorithm); + // Add retry configuration for throttling + configuration.RetryMode = Amazon.Runtime.RequestRetryMode.Adaptive; + configuration.MaxErrorRetry = 5; + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) { configuration.StorageMode = CryptoStorageMode.InstructionFile; @@ -130,4 +141,4 @@ private static ContentEncryptionAlgorithm MapEncryptionAlgorithm(Models.Encrypti _ => ContentEncryptionAlgorithm.AesGcmWithCommitment }; } -} \ No newline at end of file +} diff --git a/test-server/net-v4-server/Makefile b/test-server/net-v4-server/Makefile index e2df658a..b52bbd49 100644 --- a/test-server/net-v4-server/Makefile +++ b/test-server/net-v4-server/Makefile @@ -32,7 +32,7 @@ start-net-V4-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - dotnet run --no-build & echo $! > $(PID_FILE_NET_V4) + dotnet run --no-build > server.log 2>&1 & echo $! > $(PID_FILE_NET_V4) @echo ".NET V4 server starting..." wait-for-server: diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index 1c0a458c..8ce8983b 160000 --- a/test-server/net-v4-server/s3ec-net-v4-improved +++ b/test-server/net-v4-server/s3ec-net-v4-improved @@ -1 +1 @@ -Subproject commit 1c0a458c19b351c266199c72072de746362c5326 +Subproject commit 8ce8983bd0edf973651aee0c29894df9091cf97a diff --git a/test-server/php-v2-transition-server/src/get_object.php b/test-server/php-v2-transition-server/src/get_object.php index 5800e850..dcf683b6 100644 --- a/test-server/php-v2-transition-server/src/get_object.php +++ b/test-server/php-v2-transition-server/src/get_object.php @@ -80,6 +80,10 @@ function handleGetObject($params) } if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); + } elseif (strpos($e->getMessage(), "One or more reserved keys found in Instruction file when they should not be present.") !== false) { + return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Expected a V3 envelope but was unable to constuct one.") !== false) { + return S3EncryptionClientError($e->getMessage()); } else { error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server error: " . $e->getMessage(), 500); diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index e32c9f2b..88ee9515 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit e32c9f2b009a43cf88f2ab35e1e532114c8390c9 +Subproject commit 88ee95156f2884767b72f9219736e976d98a9c96 diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php index 3de7f779..6fb28551 100644 --- a/test-server/php-v3-server/src/get_object.php +++ b/test-server/php-v3-server/src/get_object.php @@ -84,6 +84,10 @@ function handleGetObject($params) return S3EncryptionClientError($e->getMessage()); } elseif (strpos($e->getMessage(), "Message is encrypted with a non commiting algorithm but commitment policy is set to REQUIRE_ENCRYPT_REQUIRE_DECRYPT. Select a valid commitment policy to decrypt this object.") !== false) { return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "One or more reserved keys found in Instruction file when they should not be present.") !== false) { + return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Expected a V3 envelope but was unable to constuct one.") !== false) { + return S3EncryptionClientError($e->getMessage()); } else { error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server argument: " . $e->getMessage(), 500); diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb index 85a91038..3da62b45 100644 --- a/test-server/ruby-v2-server/lib/client_manager.rb +++ b/test-server/ruby-v2-server/lib/client_manager.rb @@ -60,7 +60,13 @@ def create_client(config) end # Create the S3 encryption client - s3_client = Aws::S3::Client.new(region: 'us-west-2') + # Create the S3 encryption client with retry configuration for throttling + s3_client = Aws::S3::Client.new( + region: 'us-west-2', + retry_mode: 'adaptive', + retry_limit: 5, + retry_backoff: lambda { |c| sleep(2 ** c.retries * 0.3 * rand) } + ) encryption_client = Aws::S3::EncryptionV2::Client.new( client: s3_client, **encryption_config diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index 115183b2..5ee3f1ec 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -85,7 +85,13 @@ def create_client(config) end # Create the S3 encryption client - s3_client = Aws::S3::Client.new(region: 'us-west-2') + # Create the S3 encryption client with retry configuration for throttling + s3_client = Aws::S3::Client.new( + region: 'us-west-2', + retry_mode: 'adaptive', + retry_limit: 5, + retry_backoff: lambda { |c| sleep(2 ** c.retries * 0.3 * rand) } + ) encryption_client = Aws::S3::EncryptionV3::Client.new( client: s3_client, **encryption_config