From 2b8f937d185c9e0481eb9fc2407e9a5634848a86 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Fri, 14 Nov 2025 13:54:27 -0800 Subject: [PATCH 01/46] ruby spec updates --- test-server/ruby-v2-server/.duvet/config.toml | 15 +++++++++++++-- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test-server/ruby-v2-server/.duvet/config.toml b/test-server/ruby-v2-server/.duvet/config.toml index 7118cd70..7a34c0ff 100644 --- a/test-server/ruby-v2-server/.duvet/config.toml +++ b/test-server/ruby-v2-server/.duvet/config.toml @@ -4,15 +4,26 @@ pattern = "local-ruby-sdk/gems/aws-sdk-s3/lib/**/*.rb" comment-style = { meta = "##=", content = "##%" } +[[source]] +pattern = "local-ruby-sdk/gems/aws-sdk-s3/spec/**/*.rb" +comment-style = { meta = "##=", content = "##%" } + # Include required specifications here [[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" +source = "../specification/s3-encryption/client.md" [[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" +source = "../specification/s3-encryption/decryption.md" [[specification]] source = "../specification/s3-encryption/encryption.md" [[specification]] +source = "../specification/s3-encryption/key-commitment.md" +[[specification]] source = "../specification/s3-encryption/key-derivation.md" +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" + [report.html] enabled = true diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index 582e0241..d6b93925 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 582e02418ac2c5540c48dd089a7db506712e6f94 +Subproject commit d6b93925c65e0ad4b9410ade709c63ca874634c3 diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 582e0241..d6b93925 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 582e02418ac2c5540c48dd089a7db506712e6f94 +Subproject commit d6b93925c65e0ad4b9410ade709c63ca874634c3 From d0f16e00b726d1f93a9c1da669d1a813dd246e4a Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Fri, 14 Nov 2025 16:57:08 -0800 Subject: [PATCH 02/46] Start on instruction failures --- .../s3/InstructionFileFailures.java | 329 ++++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 29 +- 2 files changed, 351 insertions(+), 7 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java new file mode 100644 index 00000000..0d458a3b --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -0,0 +1,329 @@ +/* +* 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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 org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.encryption.s3.TestUtils.LanguageServerTarget; +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 InstructionFileFailures { + 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 crossLanguageObjects = 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 static Stream improvedClientsCanPutKMSWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + public static Stream clientsCanGetKMSWithInstructionFile() { + Stream improved = improvedClientsForTest() + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + + @Order(1) + @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutKMSWithInstructionFile") + void encrypt_with_instruction_files_kms_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) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), + crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(2) + @Test + void make_good_copies_to_verify_we_can() throws Exception { + // Create a plaintext S3 client to copy objects with instruction files + try (S3Client ptS3Client = S3Client.create()) { + for (String objectKey : crossLanguageObjects) { + // Get the encrypted object + ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Get the instruction file + String instructionFileKey = objectKey + ".instruction"; + ResponseBytes instructionFile = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(instructionFileKey) + .build()); + + String instructionFileJson = instructionFile.asUtf8String(); + Map objectMetadata = encryptedObject.response().metadata(); + + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-good-copy", + encryptedObject.asByteArray(), + objectMetadata, + instructionFileJson + ); + + ObjectMapper mapper = new ObjectMapper(); + Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); + + instructionFileMap.put("x-amz-c", objectMetadata.get("x-amz-c")); + instructionFileMap.put("x-amz-matdesc", objectMetadata.get("x-amz-matdesc")); + + String instructionFileWithCommitmentValues = mapper.writeValueAsString(instructionFileMap); + + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-both-meta-and-instruction", + encryptedObject.asByteArray(), + objectMetadata, + instructionFileWithCommitmentValues + ); + + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-only-instruction", + encryptedObject.asByteArray(), + Map.of(), + instructionFileWithCommitmentValues + ); + + + } + } + } + + void putObjectWithInstructionFile( + S3Client ptS3Client, + String newObjectKey, + byte[] objectData, + Map objectMetadata, + String instructionFileJson + ) { + + // Put the encrypted object copy + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(newObjectKey) + .metadata(objectMetadata) + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(objectData)); + + // Put the instruction file copy + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(newObjectKey + ".instruction") + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(instructionFileJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } + + + @Order(10) + @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjects + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjects + ); + } + + @Order(11) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjects + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(12) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjects + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(13) + @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_fails_with_forbid_policy(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_fails( + client, + S3ECId, + crossLanguageObjects + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(14) + @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(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_fails( + client, + S3ECId, + crossLanguageObjects + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + +} 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 d3d25d58..294f128b 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 @@ -492,18 +492,33 @@ public static void Encrypt( public static void Decrypt( S3ECTestServerClient client, - String S3ECId, List crossLanguageObjects, + String S3ECId, + List crossLanguageObjects, EncryptionAlgorithm expectedEncryptionAlgorithm ) { - for (String objectKey : crossLanguageObjects) { + // Call 5-parameter version with crossLanguageObjects as expectedPlaintexts + Decrypt(client, S3ECId, crossLanguageObjects, expectedEncryptionAlgorithm, crossLanguageObjects); + } + + public static void Decrypt( + S3ECTestServerClient client, + String S3ECId, + List crossLanguageObjects, + EncryptionAlgorithm expectedEncryptionAlgorithm, + List expectedPlaintexts + ) { + 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()); + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); // Then: Pass - assertEquals(objectKey, new String(output.getBody().array())); + assertEquals(expectedPlaintext, new String(output.getBody().array())); assertEquals( expectedEncryptionAlgorithm, GetEncryptionAlgorithm(objectKey), From 496a8db14fc55fce16fa5787dc96a7b58f2f8ad9 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Sun, 16 Nov 2025 16:31:08 -0800 Subject: [PATCH 03/46] Instruction files that pass and fail --- .../amazon/encryption/s3/TestUtils.java | 60 +++++++++++++++---- test-server/ruby-v2-server/app.rb | 4 +- 2 files changed, 52 insertions(+), 12 deletions(-) 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 294f128b..dc07a2d4 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 @@ -17,10 +17,14 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import com.amazonaws.services.s3.model.S3Object; +import com.fasterxml.jackson.databind.ObjectMapper; + import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import com.amazonaws.services.s3.model.ObjectMetadata; @@ -449,22 +453,58 @@ public static String appendTestSuffix(final String s) { private static AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient(); public static EncryptionAlgorithm GetEncryptionAlgorithm(String objectKey) { + // Lambda to determine encryption algorithm from a metadata map + java.util.function.Function, Optional> getAlgorithmFromMap = (map) -> { + if (map.containsKey("x-amz-c")) { + return Optional.of(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else if (map.containsKey("x-amz-cek-alg")) { + String cek = (String) map.get("x-amz-cek-alg"); + if (cek.contains("CBC")) { + return Optional.of(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } else if (cek.contains("GCM")) { + return Optional.of(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + } + return Optional.empty(); + }; + ObjectMetadata metadata = s3Client.getObjectMetadata(TestUtils.BUCKET, objectKey); Map userMetadata = metadata.getUserMetadata(); - // This is optimized to not need to go to the instruction files for commit_key - if (userMetadata.containsKey("x-amz-c")) { - return EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; - } else if (userMetadata.containsKey("x-amz-cek-alg")) { - String cek = userMetadata.get("x-amz-cek-alg"); - if (cek.contains("CBC")) { - return EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF; - } else if (cek.contains("GCM")) { - return EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + // Try to get algorithm from object metadata + Optional algorithm = getAlgorithmFromMap.apply(userMetadata); + if (algorithm.isPresent()) { + return algorithm.get(); + } + + // Check instruction file + try { + String instructionFileKey = objectKey + ".instruction"; + com.amazonaws.services.s3.model.S3Object instructionFileObject = + s3Client.getObject(TestUtils.BUCKET, instructionFileKey); + + // Read instruction file content + java.io.InputStream inputStream = instructionFileObject.getObjectContent(); + String instructionFileJson = new String( + inputStream.readAllBytes(), + java.nio.charset.StandardCharsets.UTF_8 + ); + inputStream.close(); + + // Parse JSON to get metadata + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); + + // Try to get algorithm from instruction file + algorithm = getAlgorithmFromMap.apply(instructionFileMap); + if (algorithm.isPresent()) { + return algorithm.get(); } + } catch (Exception e) { + // Instruction file doesn't exist or couldn't be read } - throw new RuntimeException("Need to support instruction files!"); + throw new RuntimeException("Could not determine encryption algorithm from object metadata or instruction file!"); } public static void Encrypt( diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb index cde757a3..5a39e2ea 100644 --- a/test-server/ruby-v2-server/app.rb +++ b/test-server/ruby-v2-server/app.rb @@ -132,7 +132,7 @@ def initialize metadata: response_metadata }.to_json - rescue Aws::S3::EncryptionV2::Errors::EncryptionError => e + rescue Aws::S3::EncryptionV2::Errors::EncryptionError, Aws::S3::EncryptionV3::Errors::EncryptionError => e S3ECLogger.log_error(e, { endpoint: '/put', error_category: 'EncryptionError' }, @request_id) ErrorHandlers.send_s3_encryption_client_error(self, e.message) rescue StandardError => e @@ -201,7 +201,7 @@ def initialize content_type 'application/octet-stream' body - rescue Aws::S3::EncryptionV2::Errors::DecryptionError => e + rescue Aws::S3::EncryptionV2::Errors::DecryptionError, Aws::S3::EncryptionV3::Errors::DecryptionError => e S3ECLogger.log_error(e, { endpoint: '/get', error_category: 'DecryptionError' }, @request_id) ErrorHandlers.send_s3_encryption_client_error(self, e.message) rescue StandardError => e From 54853fafd056411fcaf3369d81f24cbba413ca33 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 10:19:54 -0800 Subject: [PATCH 04/46] starting to add raw --- .../s3/InstructionFileFailures.java | 132 +++++++++++++++--- 1 file changed, 113 insertions(+), 19 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 0d458a3b..aa00abac 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -17,6 +17,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -54,19 +57,34 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class InstructionFileFailures { - private static final String sharedObjectKeyBaseMetaDataMode = "test-kc-gcm-kms"; - private static final String sharedObjectKeyBaseInsFileMode = "test-kc-gcm-kms-instruction-file"; + private static final String sharedObjectKeyBaseMetaDataMode = "test-instruction-files-cases"; private static KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); - private static final List crossLanguageObjects = new ArrayList<>(); - private static KeyPair RSA_KEY_PAIR_1; + private static final List crossLanguageObjectsKms = new ArrayList<>(); + private static final List crossLanguageObjectsRsa = new ArrayList<>(); + private static final List crossLanguageObjectsAes = new ArrayList<>(); + + private static KeyMaterial RSA_KEY; + private static KeyMaterial AES_KEY; @BeforeAll static void setupKeys() throws Exception { KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); keyPairGen.initialize(2048); - RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + RSA_KEY = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(keyPair.getPrivate().getEncoded())) + .build(); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesSecretKey = keyGen.generateKey(); + + AES_KEY = KeyMaterial.builder() + .aesKey(ByteBuffer.wrap(aesSecretKey.getEncoded())) + .build(); } public static Stream improvedClientsCanPutKMSWithInstructionFile() { @@ -75,6 +93,12 @@ public static Stream improvedClientsCanPutKMSWithInstructionFile() { .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); } + public static Stream improvedClientsCanPutRawWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + public static Stream clientsCanGetKMSWithInstructionFile() { Stream improved = improvedClientsForTest() .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); @@ -85,6 +109,16 @@ public static Stream clientsCanGetKMSWithInstructionFile() { return Stream.concat(improved, transition); } + public static Stream clientsCanGetRawWithInstructionFile() { + Stream improved = improvedClientsForTest() + .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + @Order(1) @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM with instruction files") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutKMSWithInstructionFile") @@ -93,7 +127,6 @@ void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget la CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) - .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) .instructionFileConfig( InstructionFileConfig.builder() .enableInstructionFilePutObject(true) @@ -106,18 +139,77 @@ void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget la TestUtils.Encrypt( client, S3ECId, - appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), - crossLanguageObjects, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-kms" + language.getLanguageName()), + crossLanguageObjectsKms, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); } @Order(2) + @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutRawWithInstructionFile") + void encrypt_with_instruction_files_rsa_kc_gcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-rsa" + language.getLanguageName()), + crossLanguageObjectsRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(3) + @ParameterizedTest(name = "{0}: Encrypt AES KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutRawWithInstructionFile") + void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-aes" + language.getLanguageName()), + crossLanguageObjectsRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(9) @Test - void make_good_copies_to_verify_we_can() throws Exception { + void make_copies_to_verify_things() throws Exception { // Create a plaintext S3 client to copy objects with instruction files try (S3Client ptS3Client = S3Client.create()) { - for (String objectKey : crossLanguageObjects) { + List allCrossLanguageObjects = Stream.of( + crossLanguageObjectsKms.stream(), + crossLanguageObjectsRsa.stream(), + crossLanguageObjectsAes.stream() + ).flatMap(s -> s).collect(Collectors.toList()); + for (String objectKey : allCrossLanguageObjects) { // Get the encrypted object ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder .bucket(TestUtils.BUCKET) @@ -134,6 +226,7 @@ void make_good_copies_to_verify_we_can() throws Exception { String instructionFileJson = instructionFile.asUtf8String(); Map objectMetadata = encryptedObject.response().metadata(); + // Put a strict copy, to verify that we know how to do this putObjectWithInstructionFile( ptS3Client, objectKey + "-good-copy", @@ -146,10 +239,12 @@ void make_good_copies_to_verify_we_can() throws Exception { Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); instructionFileMap.put("x-amz-c", objectMetadata.get("x-amz-c")); - instructionFileMap.put("x-amz-matdesc", objectMetadata.get("x-amz-matdesc")); + instructionFileMap.put("x-amz-d", objectMetadata.get("x-amz-d")); + instructionFileMap.put("x-amz-i", objectMetadata.get("x-amz-i")); String instructionFileWithCommitmentValues = mapper.writeValueAsString(instructionFileMap); + // Put instruction files that should fail: putObjectWithInstructionFile( ptS3Client, objectKey + "-bad-both-meta-and-instruction", @@ -166,7 +261,6 @@ void make_good_copies_to_verify_we_can() throws Exception { instructionFileWithCommitmentValues ); - } } } @@ -212,19 +306,19 @@ void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTar TestUtils.Decrypt( client, S3ECId, - crossLanguageObjects, + crossLanguageObjectsKms, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); TestUtils.Decrypt( client, S3ECId, - crossLanguageObjects + crossLanguageObjectsKms .stream() .map(key -> key + "-good-copy") .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, - crossLanguageObjects + crossLanguageObjectsKms ); } @@ -244,7 +338,7 @@ void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUti TestUtils.Decrypt_fails( client, S3ECId, - crossLanguageObjects + crossLanguageObjectsKms .stream() .map(key -> key + "-bad-both-meta-and-instruction") .collect(Collectors.toList()), @@ -268,7 +362,7 @@ void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageSe TestUtils.Decrypt_fails( client, S3ECId, - crossLanguageObjects + crossLanguageObjectsKms .stream() .map(key -> key + "-bad-only-instruction") .collect(Collectors.toList()), @@ -293,7 +387,7 @@ void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.Langua TestUtils.Decrypt_fails( client, S3ECId, - crossLanguageObjects + crossLanguageObjectsKms .stream() .map(key -> key + "-bad-both-meta-and-instruction") .collect(Collectors.toList()), @@ -318,7 +412,7 @@ void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils TestUtils.Decrypt_fails( client, S3ECId, - crossLanguageObjects + crossLanguageObjectsKms .stream() .map(key -> key + "-bad-only-instruction") .collect(Collectors.toList()), From f0aa6f7845fc7c6a10da473371a414784670fdae Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 10:56:41 -0800 Subject: [PATCH 05/46] adding ruby to raw support --- .../s3/InstructionFileFailures.java | 268 +++++++++++++++++- .../amazon/encryption/s3/TestUtils.java | 1 + .../ruby-v2-server/lib/client_manager.rb | 36 ++- .../ruby-v3-server/lib/client_manager.rb | 38 ++- 4 files changed, 329 insertions(+), 14 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index aa00abac..5456d344 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -194,7 +194,7 @@ void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget la client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-aes" + language.getLanguageName()), - crossLanguageObjectsRsa, + crossLanguageObjectsAes, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); } @@ -289,6 +289,7 @@ void putObjectWithInstructionFile( software.amazon.awssdk.core.sync.RequestBody.fromBytes(instructionFileJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))); } + // KMS instruction files decrypt @Order(10) @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") @@ -420,4 +421,269 @@ void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils ); } + // RSA instruction file decrypt + + @Order(20) + @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsRsa + ); + } + + @Order(21) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(22) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(23) + @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(24) + @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // AES instruction file decrypt + + @Order(30) + @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsAes, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsAes + ); + } + + @Order(31) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(32) + @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(33) + @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @Order(34) + @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") + void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + } 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 dc07a2d4..90995dde 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 @@ -103,6 +103,7 @@ public class TestUtils { public static final Set RAW_SUPPORTED = Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4 , NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4 + , RUBY_V2_TRANSITION, RUBY_V3 ); // .NET only supports decrypting instruction files using AES and RSA. diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb index 717003bf..85a91038 100644 --- a/test-server/ruby-v2-server/lib/client_manager.rb +++ b/test-server/ruby-v2-server/lib/client_manager.rb @@ -2,6 +2,8 @@ require 'securerandom' require 'aws-sdk-s3' require 'aws-sdk-kms' +require 'openssl' +require 'base64' require_relative 'logger' # Manages S3 Encryption Client instances @@ -14,20 +16,42 @@ def initialize # Create a new S3 encryption client and return its ID def create_client(config) - # Extract configuration + # Extract all key material types kms_key_id = config.dig('keyMaterial', 'kmsKeyId') + rsa_key_blob = config.dig('keyMaterial', 'rsaKey') + aes_key_blob = config.dig('keyMaterial', 'aesKey') inst_file_put = config.dig('instructionFileConfig', 'enableInstructionFilePutObject') - raise 'KMS Key ID is required' if kms_key_id.nil? || kms_key_id.empty? + # Validate that only one key type is provided + key_count = [kms_key_id, rsa_key_blob, aes_key_blob].compact.count + raise 'KeyMaterial must contain exactly one non-null key type' unless key_count == 1 # Create S3 encryption client configuration encryption_config = { - kms_key_id: kms_key_id, - kms_client: @kms_client, - key_wrap_schema: :kms_context, content_encryption_schema: :aes_gcm_no_padding, envelope_location: inst_file_put ? :instruction_file : :metadata - }.tap do |hash| + } + + # Configure based on key type + if kms_key_id + encryption_config[:kms_key_id] = kms_key_id + encryption_config[:kms_client] = @kms_client + encryption_config[:key_wrap_schema] = :kms_context + elsif rsa_key_blob + # Parse RSA private key from PKCS8 format + key_bytes = Base64.decode64(rsa_key_blob) + rsa_key = OpenSSL::PKey::RSA.new(key_bytes) + encryption_config[:encryption_key] = rsa_key + encryption_config[:key_wrap_schema] = :rsa_oaep_sha1 + elsif aes_key_blob + # Extract AES key bytes + key_bytes = Base64.decode64(aes_key_blob) + encryption_config[:encryption_key] = key_bytes + encryption_config[:key_wrap_schema] = :aes_gcm + end + + # Apply legacy settings + encryption_config.tap do |hash| if !config['enableLegacyWrappingAlgorithms'].nil? || !config['enableLegacyUnauthenticatedModes'].nil? legacy_modes = config['enableLegacyWrappingAlgorithms'] || config['enableLegacyUnauthenticatedModes'] # Set security profile based on legacy wrapping algorithms setting diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index a6fb551f..115183b2 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -2,6 +2,8 @@ require 'securerandom' require 'aws-sdk-s3' require 'aws-sdk-kms' +require 'openssl' +require 'base64' require_relative 'logger' # Manages S3 Encryption Client instances @@ -14,11 +16,17 @@ def initialize # Create a new S3 encryption client and return its ID def create_client(config) - # Extract configuration + # Extract all key material types kms_key_id = config.dig('keyMaterial', 'kmsKeyId') + rsa_key_blob = config.dig('keyMaterial', 'rsaKey') + aes_key_blob = config.dig('keyMaterial', 'aesKey') inst_file_put = config.dig('instructionFileConfig', 'enableInstructionFilePutObject') content_alg = config.dig('encryptionAlgorithm') + # Validate that only one key type is provided + key_count = [kms_key_id, rsa_key_blob, aes_key_blob].compact.count + raise 'KeyMaterial must contain exactly one non-null key type' unless key_count == 1 + # translate between canonical AlgSuite and Ruby symbols if content_alg.nil? || content_alg == 'ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY' content_alg = :alg_aes_256_gcm_hkdf_sha512_commit_key @@ -28,16 +36,32 @@ def create_client(config) raise 'Unknown content encryption algorithm provided: ' + content_alg end - raise 'KMS Key ID is required' if kms_key_id.nil? || kms_key_id.empty? - # Create S3 encryption client configuration encryption_config = { - kms_key_id: kms_key_id, - kms_client: @kms_client, - key_wrap_schema: :kms_context, envelope_location: inst_file_put ? :instruction_file : :metadata, content_encryption_schema: content_alg - }.tap do |hash| + } + + # Configure based on key type + if kms_key_id + encryption_config[:kms_key_id] = kms_key_id + encryption_config[:kms_client] = @kms_client + encryption_config[:key_wrap_schema] = :kms_context + elsif rsa_key_blob + # Parse RSA private key from PKCS8 format + key_bytes = Base64.decode64(rsa_key_blob) + rsa_key = OpenSSL::PKey::RSA.new(key_bytes) + encryption_config[:encryption_key] = rsa_key + encryption_config[:key_wrap_schema] = :rsa_oaep_sha1 + elsif aes_key_blob + # Extract AES key bytes + key_bytes = Base64.decode64(aes_key_blob) + encryption_config[:encryption_key] = key_bytes + encryption_config[:key_wrap_schema] = :aes_gcm + end + + # Apply additional configuration + encryption_config.tap do |hash| if !config['commitmentPolicy'].nil? hash[:commitment_policy] = case config['commitmentPolicy'] when 'FORBID_ENCRYPT_ALLOW_DECRYPT' From 7c4ddb868d8a36bb0613e2cc9f697e97c85c345f Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 11:01:01 -0800 Subject: [PATCH 06/46] update names --- .../s3/InstructionFileFailures.java | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 5456d344..0433366b 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -292,9 +292,9 @@ void putObjectWithInstructionFile( // KMS instruction files decrypt @Order(10) - @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") + @ParameterizedTest(name = "{0}: Successfully decrypt KMS encrypted original and good-copy objects") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + void decrypt_kms_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -324,9 +324,9 @@ void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTar } @Order(11) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") + @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is duplicated in metadata and instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + void decrypt_kms_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -348,9 +348,9 @@ void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUti } @Order(12) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") + @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is only in instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + void decrypt_kms_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -372,9 +372,9 @@ void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageSe } @Order(13) - @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @ParameterizedTest(name = "{0}: Fail to decrypt KMS duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -397,9 +397,9 @@ void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.Langua } @Order(14) - @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @ParameterizedTest(name = "{0}: Fail to decrypt KMS instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -424,9 +424,9 @@ void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils // RSA instruction file decrypt @Order(20) - @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Successfully decrypt RSA encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_rsa_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -456,9 +456,9 @@ void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTar } @Order(21) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_rsa_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -480,9 +480,9 @@ void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUti } @Order(22) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_rsa_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -504,9 +504,9 @@ void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageSe } @Order(23) - @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt RSA duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -529,9 +529,9 @@ void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.Langua } @Order(24) - @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt RSA instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -556,9 +556,9 @@ void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils // AES instruction file decrypt @Order(30) - @ParameterizedTest(name = "{0}: Successfully decrypt original and good-copy objects") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Successfully decrypt AES encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_aes_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -588,9 +588,9 @@ void decrypt_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTar } @Order(31) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is duplicated in metadata and instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_aes_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -612,9 +612,9 @@ void decrypt_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUti } @Order(32) - @ParameterizedTest(name = "{0}: Fail to decrypt when commitment is only in instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_aes_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -636,9 +636,9 @@ void decrypt_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageSe } @Order(33) - @ParameterizedTest(name = "{0}: Fail to decrypt duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt AES duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -661,9 +661,9 @@ void decrypt_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.Langua } @Order(34) - @ParameterizedTest(name = "{0}: Fail to decrypt instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + @ParameterizedTest(name = "{0}: Fail to decrypt AES instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") + void decrypt_aes_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() From aeb359c08f62596659fa6fff67d4da04390584b5 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 11:15:45 -0800 Subject: [PATCH 07/46] speed up the build --- test-server/Makefile | 2 +- test-server/java-v3-server/Makefile | 2 +- test-server/java-v3-transition-server/Makefile | 2 +- test-server/java-v4-server/Makefile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index 9b18b857..255323a1 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -82,7 +82,7 @@ run-tests: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./gradlew --build-cache --info --parallel integ -Dtest.filter.servers="$(FILTER)" + ./gradlew --build-cache --info --parallel --no-daemon integ -Dtest.filter.servers="$(FILTER)" @echo "Tests completed successfully" # Stop the servers diff --git a/test-server/java-v3-server/Makefile b/test-server/java-v3-server/Makefile index 692e80b3..59dcdff5 100644 --- a/test-server/java-v3-server/Makefile +++ b/test-server/java-v3-server/Makefile @@ -7,7 +7,7 @@ PORT := 8080 build-server: @echo "Building Java V3 server..." - ./gradlew --build-cache --parallel build + ./gradlew --build-cache --parallel --no-daemon build start-server: @echo "Starting Java V3 server..." diff --git a/test-server/java-v3-transition-server/Makefile b/test-server/java-v3-transition-server/Makefile index 5a25a8aa..81726b59 100644 --- a/test-server/java-v3-transition-server/Makefile +++ b/test-server/java-v3-transition-server/Makefile @@ -10,7 +10,7 @@ build-server: cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile && mvn -B -ntp install -DskipTests @echo "S3EC build completed." @echo "Building Java V3 Transition server..." - ./gradlew --build-cache --parallel build + ./gradlew --build-cache --parallel --no-daemon build start-server: @echo "Starting Java V3 Transition server..." diff --git a/test-server/java-v4-server/Makefile b/test-server/java-v4-server/Makefile index 418e0127..3d1aae2a 100644 --- a/test-server/java-v4-server/Makefile +++ b/test-server/java-v4-server/Makefile @@ -10,7 +10,7 @@ build-server: cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile && mvn -B -ntp install -DskipTests @echo "S3EC build completed." @echo "Building Java V4 server..." - ./gradlew --build-cache --parallel build + ./gradlew --build-cache --parallel --no-daemon build start-server: @echo "Starting Java V4 server..." From 270f2d56d2cffc5f558f65d2270463e78d128a19 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 11:42:58 -0800 Subject: [PATCH 08/46] speed up wait --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 880ca3f4..ef47fb14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -171,9 +171,7 @@ jobs: - name: Wait for servers to start run: cd test-server && make wait-all-servers 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 }} + MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }} - name: Run run-tests run: cd test-server && make run-tests From 271fcbaf88fb6e467798148f5a508367a0d5d84d Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 12:35:16 -0800 Subject: [PATCH 09/46] update client building --- .../amazon/encryption/s3/InstructionFileFailures.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 0433366b..4f86a90b 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -381,6 +381,7 @@ void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La .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(); @@ -406,6 +407,7 @@ void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(TestU .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(); @@ -513,6 +515,7 @@ void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La .config(S3ECConfig.builder() .keyMaterial(RSA_KEY) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String S3ECId = clientOutput.getClientId(); @@ -538,6 +541,7 @@ void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestU .config(S3ECConfig.builder() .keyMaterial(RSA_KEY) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String S3ECId = clientOutput.getClientId(); @@ -645,6 +649,7 @@ void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La .config(S3ECConfig.builder() .keyMaterial(AES_KEY) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String S3ECId = clientOutput.getClientId(); @@ -670,6 +675,7 @@ void decrypt_aes_with_instruction_file_commitment_fails_with_forbid_policy(TestU .config(S3ECConfig.builder() .keyMaterial(AES_KEY) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String S3ECId = clientOutput.getClientId(); From 231e3ad8d5769942080e7204cc457ec78a6b55d7 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 17 Nov 2025 13:54:46 -0800 Subject: [PATCH 10/46] fix: Go in instruction files tests (#104) Update Go for instruction file failures --- test-server/go-v3-transition-server/local-go-s3ec | 2 +- test-server/go-v4-server/local-go-s3ec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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-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 From 0a4ef95f13898686818806fe48d8d4097f039abb Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 17 Nov 2025 15:37:43 -0800 Subject: [PATCH 11/46] better failures? --- .../amazon/encryption/s3/RoundTripTests.java | 2 +- .../amazon/encryption/s3/TestUtils.java | 46 +++++++++++++------ 2 files changed, 32 insertions(+), 16 deletions(-) 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..347c44e5 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 @@ -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..351f2c8c 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 @@ -548,23 +548,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) + )); } } From 4d73663712f286a788c6efe398e90969328ba78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:25:46 -0800 Subject: [PATCH 12/46] bump php sdk to fixed instruction file (#106) Update PHP v3 to support failure on bad instruction files --- test-server/php-v2-transition-server/src/get_object.php | 4 ++++ test-server/php-v3-server/local-php-sdk | 2 +- test-server/php-v3-server/src/get_object.php | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) 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); From 63e0ed188f14c3b11b01bedea557622c5db31191 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 19 Nov 2025 14:59:58 -0800 Subject: [PATCH 13/46] faster parallel tests (#107) --- .github/workflows/test.yml | 31 +- test-server/Makefile | 18 +- test-server/cpp-v2-server/Makefile | 2 +- test-server/cpp-v2-server/main.cpp | 44 +- test-server/cpp-v2-transition-server/Makefile | 2 +- test-server/cpp-v2-transition-server/main.cpp | 111 ++++- test-server/cpp-v3-server/CMakeLists.txt | 16 +- test-server/cpp-v3-server/Makefile | 2 +- test-server/cpp-v3-server/main.cpp | 124 +++++- test-server/go-v3-server/main.go | 14 +- test-server/go-v3-transition-server/main.go | 14 +- test-server/go-v4-server/main.go | 14 +- test-server/java-tests/build.gradle.kts | 14 + .../amazon/encryption/s3/GCMTestSuite.java | 255 ++++++++++++ .../amazon/encryption/s3/GCMTests.java | 203 --------- .../amazon/encryption/s3/KC_GCMTestSuite.java | 389 ++++++++++++++++++ .../amazon/encryption/s3/KC_GCMTests.java | 264 ------------ .../amazon/encryption/s3/RoundTripTests.java | 14 +- .../amazon/encryption/s3/TestUtils.java | 2 + test-server/java-v3-server/gradle.properties | 19 +- .../s3/CreateClientOperationImpl.java | 16 + .../gradle.properties | 19 +- .../java-v3-transition-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 16 + test-server/java-v4-server/gradle.properties | 19 +- test-server/java-v4-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 16 + .../Controllers/ClientController.cs | 18 +- .../Controllers/ClientController.cs | 18 +- .../s3ec-v3-transition-branch | 2 +- .../Controllers/ClientController.cs | 17 +- test-server/net-v4-server/Makefile | 2 +- .../net-v4-server/s3ec-net-v4-improved | 2 +- .../ruby-v2-server/lib/client_manager.rb | 8 +- .../ruby-v3-server/lib/client_manager.rb | 8 +- 35 files changed, 1121 insertions(+), 596 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java delete mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTests.java create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java delete mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java 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/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/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 347c44e5..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()); } } } 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 351f2c8c..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, 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/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 From 91933b7f7694c07c07c1649ef44089a229ca39e9 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 19 Nov 2025 15:03:28 -0800 Subject: [PATCH 14/46] enable cpp --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 210e5e7c..d6b7a9d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -155,25 +155,25 @@ jobs: aws-region: us-west-2 - name: Build the servers - run: cd test-server && make build-all-servers FILTER=ruby,java,php,net,go + run: cd test-server && make build-all-servers 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 FILTER=ruby,java,php,net,go + run: cd test-server && make start-all-servers 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 FILTER=ruby,java,php,net,go + run: cd test-server && make wait-all-servers env: MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }} - name: Run run-tests - run: cd test-server && make run-tests FILTER=ruby,java,php,net,go + run: cd test-server && make run-tests env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} From 490ffb264385b29d139e4180e373b498041948a2 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 19 Nov 2025 15:58:23 -0800 Subject: [PATCH 15/46] fix c++? --- test-server/cpp-v2-transition-server/main.cpp | 106 ++++++++---- test-server/cpp-v3-server/main.cpp | 157 ++++++++++++------ 2 files changed, 177 insertions(+), 86 deletions(-) diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 3514b594..535a3e0a 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -88,8 +88,11 @@ std::string get_config(json & request, const char * x) MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { + // Make a copy of body so we own the data even if request_completed fires + std::string body_copy(body); + try { - json request = json::parse(body); + json request = json::parse(body_copy); std::string commitmentPolicy = get_config(request, "commitmentPolicy"); std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); @@ -141,40 +144,58 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - // Create appropriate encryption materials based on key type - std::shared_ptr materials; + // Create CryptoConfigurationV2 and S3EncryptionClientV2 based on key type + std::shared_ptr encryption_client; if (!aes_key_blob.empty()) { // Base64 decode the AES key - auto decoded = Aws::Utils::Base64::Decode(aes_key_blob); - if (!decoded.IsSuccess()) { + Aws::Utils::ByteBuffer decoded = Aws::Utils::HashingUtils::Base64Decode(aes_key_blob); + if (decoded.GetLength() == 0) { return send_response(connection, 400, "{\"error\":\"Failed to decode AES key\"}"); } Aws::Utils::CryptoBuffer key_buffer( - decoded.GetResult().GetUnderlyingData(), - decoded.GetResult().GetLength() + decoded.GetUnderlyingData(), + decoded.GetLength() ); - materials = std::make_shared< + auto materials = std::make_shared< Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( key_buffer ); + CryptoConfigurationV2 config(materials); + + if (legacy1 || legacy2) + config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (inst_put) + config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + // Configure ClientConfiguration with retry strategy for throttling + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 25; + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + encryption_client = std::make_shared(config, clientConfig); } else if (!kms_key_id.empty()) { - materials = std::make_shared(kms_key_id); + auto materials = std::make_shared(kms_key_id); + CryptoConfigurationV2 config(materials); + + if (legacy1 || legacy2) + config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (inst_put) + config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + // Configure ClientConfiguration with retry strategy for throttling + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 25; + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + encryption_client = std::make_shared(config, clientConfig); } else { return send_response(connection, 400, "{\"error\":\"No valid key material provided\"}"); } - - CryptoConfigurationV2 config(materials); - if (legacy1 || legacy2) - config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - auto encryption_client = std::make_shared(config); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); @@ -287,6 +308,9 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, } try { + // Create owned copy of body data to ensure it lives through the S3 operation + auto body_ptr = std::make_shared(body); + Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -294,9 +318,12 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - auto stream = std::make_shared(body); + // Create stream from owned body data + auto stream = std::make_shared(*body_ptr); request.SetBody(stream); + // Synchronous call - waits for S3 operation to complete + // body_ptr keeps the data alive through this entire operation auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; @@ -311,21 +338,31 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, } } +void request_completed(void *cls, struct MHD_Connection *connection, + void **con_cls, enum MHD_RequestTerminationCode toe) { + // Clean up the request-specific context when request is truly complete + if (*con_cls != nullptr) { + std::string *body = static_cast(*con_cls); + delete body; + *con_cls = nullptr; + } +} + MHD_Result request_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { std::string method_str(method); bool is_push = method_str == "POST" || method_str == "PUT"; - static int dummy; + + // Initialize request context on first call if (*con_cls == nullptr) { - if (is_push) { - *con_cls = new std::string(); - } else { - *con_cls = &dummy; - } + // Allocate unique state for each request to avoid race conditions + *con_cls = new std::string(); return MHD_YES; } + + // Accumulate request body data for POST/PUT requests if (is_push && *upload_data_size) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); @@ -335,11 +372,13 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string url_str(url); + // Handle client creation endpoint if (is_push && url_str == "/client") { - std::unique_ptr body(static_cast(*con_cls)); + std::string *body = static_cast(*con_cls); return handle_create_client(connection, *body); } + // Handle object operations if (url_str.find("/object/") == 0) { std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); @@ -347,18 +386,20 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string bucket = path.substr(0, slash_pos); std::string key = path.substr(slash_pos + 1); std::string client_id = get_header_value(connection, "clientid"); - std::string metadata = get_header_value(connection, "content-metadata"); + if (method_str == "GET") { return handle_get_object(connection, bucket, key, client_id, metadata); } else if (method_str == "PUT") { - std::unique_ptr body(static_cast(*con_cls)); - *upload_data_size = 0; + std::string *body = static_cast(*con_cls); return handle_put_object(connection, bucket, key, client_id, *body, metadata); + } else { + return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } } } + // Return error for unrecognized endpoints return send_response(connection, 404, "{\"error\":\"Not idea what is happening\"}"); } @@ -369,8 +410,11 @@ int main() { int port = 8097; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, - &request_handler, NULL, MHD_OPTION_END); + MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, + &request_handler, NULL, + MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, + MHD_OPTION_CONNECTION_LIMIT, 100, + MHD_OPTION_END); if (!daemon) { fprintf(stderr, "Failed to start server on port %d\n", port); diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 21360b90..8a795098 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -87,8 +87,11 @@ std::string get_config(json & request, const char * x) MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { + // Make a copy of body so we own the data even if request_completed fires + std::string body_copy(body); + try { - json request = json::parse(body); + json request = json::parse(body_copy); // Extract all key material types std::string kms_key_id; @@ -133,64 +136,85 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - // Create appropriate encryption materials based on key type - std::shared_ptr materials; + std::string commitmentPolicy = get_config(request, "commitmentPolicy"); + std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); + + // Create CryptoConfigurationV3 and S3EncryptionClientV3 based on key type + std::shared_ptr encryption_client; if (!aes_key_blob.empty()) { // Base64 decode the AES key - auto decoded = Aws::Utils::Base64::Decode(aes_key_blob); - if (!decoded.IsSuccess()) { + Aws::Utils::ByteBuffer decoded = Aws::Utils::HashingUtils::Base64Decode(aes_key_blob); + if (decoded.GetLength() == 0) { return send_response(connection, 400, "{\"error\":\"Failed to decode AES key\"}"); } Aws::Utils::CryptoBuffer key_buffer( - decoded.GetResult().GetUnderlyingData(), - decoded.GetResult().GetLength() + decoded.GetUnderlyingData(), + decoded.GetLength() ); - materials = std::make_shared< + auto materials = std::make_shared< Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( key_buffer ); + CryptoConfigurationV3 config(materials); + + if (legacy1 || legacy2) + config.AllowLegacy(); + if (inst_put) + config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + if (encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); + } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); + } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + } + + // Configure ClientConfiguration with retry strategy for throttling + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 25; + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + encryption_client = std::make_shared(config, clientConfig); } else if (!kms_key_id.empty()) { - materials = std::make_shared(kms_key_id); + auto materials = std::make_shared(kms_key_id); + CryptoConfigurationV3 config(materials); + + if (legacy1 || legacy2) + config.AllowLegacy(); + if (inst_put) + config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + if (encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); + } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); + } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + } + + // Configure ClientConfiguration with retry strategy for throttling + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 25; + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + encryption_client = std::make_shared(config, clientConfig); } 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) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - std::string commitmentPolicy = get_config(request, "commitmentPolicy"); - std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); - - if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - if (encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); - } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); - } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); - } - - auto encryption_client = std::make_shared(config); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); @@ -306,6 +330,9 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, } try { + // Create owned copy of body data to ensure it lives through the S3 operation + auto body_ptr = std::make_shared(body); + Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -313,9 +340,12 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - auto stream = std::make_shared(body); + // Create stream from owned body data + auto stream = std::make_shared(*body_ptr); request.SetBody(stream); + // Synchronous call - waits for S3 operation to complete + // body_ptr keeps the data alive through this entire operation auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; @@ -332,21 +362,31 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, } } +void request_completed(void *cls, struct MHD_Connection *connection, + void **con_cls, enum MHD_RequestTerminationCode toe) { + // Clean up the request-specific context when request is truly complete + if (*con_cls != nullptr) { + std::string *body = static_cast(*con_cls); + delete body; + *con_cls = nullptr; + } +} + MHD_Result request_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { std::string method_str(method); bool is_push = method_str == "POST" || method_str == "PUT"; - static int dummy; + + // Initialize request context on first call if (*con_cls == nullptr) { - if (is_push) { - *con_cls = new std::string(); - } else { - *con_cls = &dummy; - } + // Allocate unique state for each request to avoid race conditions + *con_cls = new std::string(); return MHD_YES; } + + // Accumulate request body data for POST/PUT requests if (is_push && *upload_data_size) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); @@ -356,11 +396,13 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string url_str(url); + // Handle client creation endpoint if (is_push && url_str == "/client") { - std::unique_ptr body(static_cast(*con_cls)); + std::string *body = static_cast(*con_cls); return handle_create_client(connection, *body); } + // Handle object operations if (url_str.find("/object/") == 0) { std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); @@ -368,18 +410,20 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string bucket = path.substr(0, slash_pos); std::string key = path.substr(slash_pos + 1); std::string client_id = get_header_value(connection, "clientid"); - std::string metadata = get_header_value(connection, "content-metadata"); + if (method_str == "GET") { return handle_get_object(connection, bucket, key, client_id, metadata); } else if (method_str == "PUT") { - std::unique_ptr body(static_cast(*con_cls)); - *upload_data_size = 0; + std::string *body = static_cast(*con_cls); return handle_put_object(connection, bucket, key, client_id, *body, metadata); + } else { + return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } } } + // Return error for unrecognized endpoints return send_response(connection, 404, "{\"error\":\"Not idea what is happening\"}"); } @@ -390,8 +434,11 @@ int main() { int port = 8091; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, - &request_handler, NULL, MHD_OPTION_END); + MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, + &request_handler, NULL, + MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, + MHD_OPTION_CONNECTION_LIMIT, 100, + MHD_OPTION_END); if (!daemon) { fprintf(stderr, "Failed to start server on port %d\n", port); From d1366a9d02bffac1ae4b7d3b021169d2a73f03dc Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 19 Nov 2025 16:20:44 -0800 Subject: [PATCH 16/46] Make the instruction file tests parallel --- .../s3/InstructionFileFailures.java | 1305 +++++++++-------- .../amazon/encryption/s3/TestUtils.java | 42 +- 2 files changed, 734 insertions(+), 613 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 4f86a90b..6096d59e 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -11,23 +11,23 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; 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 org.junit.jupiter.api.Test; import org.opentest4j.TestAbortedException; import software.amazon.awssdk.core.ResponseBytes; @@ -47,649 +47,736 @@ 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. +* Instruction File Failures Test Suite +* +* This suite enforces execution order between encrypt and decrypt phases: +* 1. EncryptTests - Encrypts objects with various key materials and creates test copies +* 2. DecryptTests - Waits for encrypt phase to complete, then tests decryption scenarios +* +* Coordination is achieved using a CountDownLatch that EncryptTests signals upon completion +* and DecryptTests awaits before proceeding. * * Tests are based on the exhaustive test matrix defined at: * https://tiny.amazon.com/3xnzwczl/loopcloumicrpeyJ3 -* */ - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class InstructionFileFailures { - private static final String sharedObjectKeyBaseMetaDataMode = "test-instruction-files-cases"; - private static KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) - .build(); - private static final List crossLanguageObjectsKms = new ArrayList<>(); - private static final List crossLanguageObjectsRsa = new ArrayList<>(); - private static final List crossLanguageObjectsAes = new ArrayList<>(); - - private static KeyMaterial RSA_KEY; - private static KeyMaterial AES_KEY; - - @BeforeAll - static void setupKeys() throws Exception { - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); - keyPairGen.initialize(2048); - KeyPair keyPair = keyPairGen.generateKeyPair(); - - RSA_KEY = KeyMaterial.builder() - .rsaKey(ByteBuffer.wrap(keyPair.getPrivate().getEncoded())) - .build(); - - KeyGenerator keyGen = KeyGenerator.getInstance("AES"); - keyGen.init(256); - SecretKey aesSecretKey = keyGen.generateKey(); - - AES_KEY = KeyMaterial.builder() - .aesKey(ByteBuffer.wrap(aesSecretKey.getEncoded())) +public class InstructionFileFailures { + // Synchronization latch - released when encrypt phase completes + private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + + /** + * Encryption Tests - Encrypt Phase + * + * These tests encrypt objects using various key materials (KMS, RSA, AES) with instruction files. + * 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-instruction-files-cases"; + private static KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); - } + + // Thread-safe lists for storing encrypted object keys + private static final List crossLanguageObjectsKms = + Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsRsa = + Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsAes = + Collections.synchronizedList(new ArrayList<>()); + + private static KeyMaterial RSA_KEY; + private static KeyMaterial AES_KEY; + + @BeforeAll + static void setupKeys() throws Exception { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + RSA_KEY = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(keyPair.getPrivate().getEncoded())) + .build(); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesSecretKey = keyGen.generateKey(); + + AES_KEY = KeyMaterial.builder() + .aesKey(ByteBuffer.wrap(aesSecretKey.getEncoded())) + .build(); + } - public static Stream improvedClientsCanPutKMSWithInstructionFile() { - return improvedClientsForTest() - .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) - .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - } + /** + * Public accessors for decrypt tests to retrieve encrypted object keys and key materials + */ + static List getCrossLanguageObjectsKms() { + return new ArrayList<>(crossLanguageObjectsKms); + } - public static Stream improvedClientsCanPutRawWithInstructionFile() { - return improvedClientsForTest() - .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) - .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - } + static List getCrossLanguageObjectsRsa() { + return new ArrayList<>(crossLanguageObjectsRsa); + } - public static Stream clientsCanGetKMSWithInstructionFile() { - Stream improved = improvedClientsForTest() - .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - - Stream transition = transitionClientsForTest() - .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - - return Stream.concat(improved, transition); - } + static List getCrossLanguageObjectsAes() { + return new ArrayList<>(crossLanguageObjectsAes); + } - public static Stream clientsCanGetRawWithInstructionFile() { - Stream improved = improvedClientsForTest() - .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - - Stream transition = transitionClientsForTest() - .filter(target -> RAW_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); - - return Stream.concat(improved, transition); - } + static KeyMaterial getRsaKey() { + return RSA_KEY; + } - @Order(1) - @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM with instruction files") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutKMSWithInstructionFile") - void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .instructionFileConfig( - InstructionFileConfig.builder() - .enableInstructionFilePutObject(true) - .build() - ) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Encrypt( - client, - S3ECId, - appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-kms" + language.getLanguageName()), - crossLanguageObjectsKms, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + static KeyMaterial getAesKey() { + return AES_KEY; + } - @Order(2) - @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction files") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutRawWithInstructionFile") - void encrypt_with_instruction_files_rsa_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) + static KeyMaterial getKmsKeyArn() { + return kmsKeyArn; + } + + public static Stream improvedClientsCanPutKMSWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + public static Stream improvedClientsCanPutRawRSAWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + public static Stream improvedClientsCanPutRawAESWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") + void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) .instructionFileConfig( - InstructionFileConfig.builder() - .enableInstructionFilePutObject(true) - .build() + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() ) .build()) - .build()); + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-kms" + language.getLanguageName()), + crossLanguageObjectsKms, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - String S3ECId = clientOutput.getClientId(); + @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encrypt_with_instruction_files_rsa_kc_gcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-rsa" + language.getLanguageName()), + crossLanguageObjectsRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Encrypt AES KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") + void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-aes" + language.getLanguageName()), + crossLanguageObjectsAes, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + static void make_copies_to_verify_things() throws Exception { + // Create a plaintext S3 client to copy objects with instruction files + try (S3Client ptS3Client = S3Client.create()) { + List allCrossLanguageObjects = Stream.of( + crossLanguageObjectsKms.stream(), + crossLanguageObjectsRsa.stream(), + crossLanguageObjectsAes.stream() + ).flatMap(s -> s).collect(Collectors.toList()); + for (String objectKey : allCrossLanguageObjects) { + // Get the encrypted object + ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Get the instruction file + String instructionFileKey = objectKey + ".instruction"; + ResponseBytes instructionFile = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(instructionFileKey) + .build()); + + String instructionFileJson = instructionFile.asUtf8String(); + Map objectMetadata = encryptedObject.response().metadata(); + + // Put a strict copy, to verify that we know how to do this + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-good-copy", + encryptedObject.asByteArray(), + objectMetadata, + instructionFileJson + ); + + ObjectMapper mapper = new ObjectMapper(); + Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); + + instructionFileMap.put("x-amz-c", objectMetadata.get("x-amz-c")); + instructionFileMap.put("x-amz-d", objectMetadata.get("x-amz-d")); + instructionFileMap.put("x-amz-i", objectMetadata.get("x-amz-i")); + + String instructionFileWithCommitmentValues = mapper.writeValueAsString(instructionFileMap); + + // Put instruction files that should fail: + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-both-meta-and-instruction", + encryptedObject.asByteArray(), + objectMetadata, + instructionFileWithCommitmentValues + ); + + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-only-instruction", + encryptedObject.asByteArray(), + Map.of(), + instructionFileWithCommitmentValues + ); + + } + } + } + + static void putObjectWithInstructionFile( + S3Client ptS3Client, + String newObjectKey, + byte[] objectData, + Map objectMetadata, + String instructionFileJson + ) { + + // Put the encrypted object copy + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(newObjectKey) + .metadata(objectMetadata) + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(objectData)); + + // Put the instruction file copy + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(newObjectKey + ".instruction") + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(instructionFileJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } - TestUtils.Encrypt( - client, - S3ECId, - appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-rsa" + language.getLanguageName()), - crossLanguageObjectsRsa, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); + @AfterAll + static void signalEncryptionComplete() throws Exception { + make_copies_to_verify_things(); + + // Signal that all encryption tests have completed + encryptPhaseComplete.countDown(); + } } - @Order(3) - @ParameterizedTest(name = "{0}: Encrypt AES KC-GCM with instruction files") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#improvedClientsCanPutRawWithInstructionFile") - void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .instructionFileConfig( - InstructionFileConfig.builder() - .enableInstructionFilePutObject(true) - .build() - ) + /** + * 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. + */ + @Nested + class DecryptTests { + private static List crossLanguageObjectsKms; + private static List crossLanguageObjectsRsa; + private static List crossLanguageObjectsAes; + private static KeyMaterial kmsKeyArn; + private static KeyMaterial RSA_KEY; + private static KeyMaterial AES_KEY; + + @BeforeAll + static void setup() throws InterruptedException { + // Wait for all encryption tests to complete + encryptPhaseComplete.await(); + + // Import encrypted objects and key materials from the encrypt phase + crossLanguageObjectsKms = EncryptTests.getCrossLanguageObjectsKms(); + crossLanguageObjectsRsa = EncryptTests.getCrossLanguageObjectsRsa(); + crossLanguageObjectsAes = EncryptTests.getCrossLanguageObjectsAes(); + kmsKeyArn = EncryptTests.getKmsKeyArn(); + RSA_KEY = EncryptTests.getRsaKey(); + AES_KEY = EncryptTests.getAesKey(); + + // Verify we have objects to decrypt + if (crossLanguageObjectsKms.isEmpty() && crossLanguageObjectsRsa.isEmpty() && crossLanguageObjectsAes.isEmpty()) { + throw new IllegalStateException( + "No encrypted objects found. Ensure EncryptTests runs first."); + } + } + + public static Stream clientsCanGetKMSWithInstructionFile() { + Stream improved = improvedClientsForTest() + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + + public static Stream clientsCanGetRawRSAWithInstructionFile() { + Stream improved = improvedClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + + public static Stream clientsCanGetRawAESWithInstructionFile() { + Stream improved = improvedClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + + // KMS instruction files decrypt + + @ParameterizedTest(name = "{0}: Successfully decrypt KMS encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decrypt_kms_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) .build()) - .build()); + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsKms, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsKms + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsKms + ); + } - String S3ECId = clientOutput.getClientId(); + @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decrypt_kms_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { - TestUtils.Encrypt( - client, - S3ECId, - appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-aes" + language.getLanguageName()), - crossLanguageObjectsAes, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsKms + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(9) - @Test - void make_copies_to_verify_things() throws Exception { - // Create a plaintext S3 client to copy objects with instruction files - try (S3Client ptS3Client = S3Client.create()) { - List allCrossLanguageObjects = Stream.of( - crossLanguageObjectsKms.stream(), - crossLanguageObjectsRsa.stream(), - crossLanguageObjectsAes.stream() - ).flatMap(s -> s).collect(Collectors.toList()); - for (String objectKey : allCrossLanguageObjects) { - // Get the encrypted object - ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder - .bucket(TestUtils.BUCKET) - .key(objectKey) - .build()); - - // Get the instruction file - String instructionFileKey = objectKey + ".instruction"; - ResponseBytes instructionFile = ptS3Client.getObjectAsBytes(builder -> builder - .bucket(TestUtils.BUCKET) - .key(instructionFileKey) - .build()); - - String instructionFileJson = instructionFile.asUtf8String(); - Map objectMetadata = encryptedObject.response().metadata(); - - // Put a strict copy, to verify that we know how to do this - putObjectWithInstructionFile( - ptS3Client, - objectKey + "-good-copy", - encryptedObject.asByteArray(), - objectMetadata, - instructionFileJson - ); - - ObjectMapper mapper = new ObjectMapper(); - Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); - - instructionFileMap.put("x-amz-c", objectMetadata.get("x-amz-c")); - instructionFileMap.put("x-amz-d", objectMetadata.get("x-amz-d")); - instructionFileMap.put("x-amz-i", objectMetadata.get("x-amz-i")); - - String instructionFileWithCommitmentValues = mapper.writeValueAsString(instructionFileMap); - - // Put instruction files that should fail: - putObjectWithInstructionFile( - ptS3Client, - objectKey + "-bad-both-meta-and-instruction", - encryptedObject.asByteArray(), - objectMetadata, - instructionFileWithCommitmentValues - ); - - putObjectWithInstructionFile( - ptS3Client, - objectKey + "-bad-only-instruction", - encryptedObject.asByteArray(), - Map.of(), - instructionFileWithCommitmentValues - ); + @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decrypt_kms_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsKms + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); } - } - void putObjectWithInstructionFile( - S3Client ptS3Client, - String newObjectKey, - byte[] objectData, - Map objectMetadata, - String instructionFileJson - ) { - - // Put the encrypted object copy - ptS3Client.putObject(builder -> builder - .bucket(TestUtils.BUCKET) - .key(newObjectKey) - .metadata(objectMetadata) - .build(), - software.amazon.awssdk.core.sync.RequestBody.fromBytes(objectData)); - - // Put the instruction file copy - ptS3Client.putObject(builder -> builder - .bucket(TestUtils.BUCKET) - .key(newObjectKey + ".instruction") - .build(), - software.amazon.awssdk.core.sync.RequestBody.fromBytes(instructionFileJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))); - } + @ParameterizedTest(name = "{0}: Fail to decrypt KMS duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - // KMS instruction files decrypt - - @Order(10) - @ParameterizedTest(name = "{0}: Successfully decrypt KMS encrypted original and good-copy objects") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsKms, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsKms - .stream() - .map(key -> key + "-good-copy") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, - crossLanguageObjectsKms - ); - } + 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_fails( + client, + S3ECId, + crossLanguageObjectsKms + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(11) - @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is duplicated in metadata and instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsKms - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + @ParameterizedTest(name = "{0}: Fail to decrypt KMS instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - @Order(12) - @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is only in instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsKms - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + 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_fails( + client, + S3ECId, + crossLanguageObjectsKms + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(13) - @ParameterizedTest(name = "{0}: Fail to decrypt KMS duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(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_fails( - client, - S3ECId, - crossLanguageObjectsKms - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + // RSA instruction file decrypt - @Order(14) - @ParameterizedTest(name = "{0}: Fail to decrypt KMS instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(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_fails( - client, - S3ECId, - crossLanguageObjectsKms - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + @ParameterizedTest(name = "{0}: Successfully decrypt RSA encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_rsa_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { - // RSA instruction file decrypt - - @Order(20) - @ParameterizedTest(name = "{0}: Successfully decrypt RSA encrypted original and good-copy objects") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_rsa_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsRsa, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsRsa - .stream() - .map(key -> key + "-good-copy") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, - crossLanguageObjectsRsa - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsRsa + ); + } - @Order(21) - @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is duplicated in metadata and instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_rsa_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsRsa - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_rsa_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { - @Order(22) - @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is only in instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_rsa_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsRsa - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(23) - @ParameterizedTest(name = "{0}: Fail to decrypt RSA duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsRsa - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_rsa_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { - @Order(24) - @ParameterizedTest(name = "{0}: Fail to decrypt RSA instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(RSA_KEY) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsRsa - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - // AES instruction file decrypt - - @Order(30) - @ParameterizedTest(name = "{0}: Successfully decrypt AES encrypted original and good-copy objects") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_aes_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsAes, - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - - TestUtils.Decrypt( - client, - S3ECId, - crossLanguageObjectsAes - .stream() - .map(key -> key + "-good-copy") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, - crossLanguageObjectsAes - ); - } + @ParameterizedTest(name = "{0}: Fail to decrypt RSA duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - @Order(31) - @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is duplicated in metadata and instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_aes_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsAes - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(32) - @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is only in instruction file") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_aes_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsAes - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + @ParameterizedTest(name = "{0}: Fail to decrypt RSA instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - @Order(33) - @ParameterizedTest(name = "{0}: Fail to decrypt AES duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsAes - .stream() - .map(key -> key + "-bad-both-meta-and-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsRsa + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } - @Order(34) - @ParameterizedTest(name = "{0}: Fail to decrypt AES instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") - @MethodSource("software.amazon.encryption.s3.InstructionFileFailures#clientsCanGetRawWithInstructionFile") - void decrypt_aes_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { - - S3ECTestServerClient client = TestUtils.testServerClientFor(language); - CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(AES_KEY) - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .build()) - .build()); - String S3ECId = clientOutput.getClientId(); - - TestUtils.Decrypt_fails( - client, - S3ECId, - crossLanguageObjectsAes - .stream() - .map(key -> key + "-bad-only-instruction") - .collect(Collectors.toList()), - EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - ); - } - + // AES instruction file decrypt + + @ParameterizedTest(name = "{0}: Successfully decrypt AES encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_aes_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsAes, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsAes + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is duplicated in metadata and instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_aes_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_aes_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt AES duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-both-meta-and-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt AES instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_aes_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(AES_KEY) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsAes + .stream() + .map(key -> key + "-bad-only-instruction") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + } } 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 a7a86c1d..6f274747 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 @@ -99,13 +99,27 @@ public class TestUtils { public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); - // For now, only .NET and Java have RSA support - public static final Set RAW_SUPPORTED = + + // Cpp only supports Raw AES + public static final Set RAW_AES_SUPPORTED = + Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4 + , NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4 + , RUBY_V2_TRANSITION, RUBY_V3 + , CPP_V2_CURRENT, CPP_V2_TRANSITION, CPP_V3 + ); + + public static final Set RAW_RSA_SUPPORTED = Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4 , NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4 , RUBY_V2_TRANSITION, RUBY_V3 ); + // Intersection of RAW_AES_SUPPORTED and RAW_RSA_SUPPORTED + public static final Set RAW_SUPPORTED = + RAW_AES_SUPPORTED.stream() + .filter(RAW_RSA_SUPPORTED::contains) + .collect(Collectors.toSet()); + // .NET only supports decrypting instruction files using AES and RSA. // Python MUST support decrypting KMS instruction files, but does not yet. public static final Set KMS_INSTRUCTION_FILE_UNSUPPORTED = @@ -366,6 +380,28 @@ public static Stream improvedClientsForTest() { .map(Arguments::of); } + /** + * Get stream of arguments for clients that support RAW AES (includes CPP). + */ + public static Stream clientsRawAesForTest() { + Stream improved = improvedClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + Stream transition = transitionClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + return Stream.concat(improved, transition); + } + + /** + * Get stream of arguments for clients that support RAW RSA (excludes CPP). + */ + public static Stream clientsRawRsaForTest() { + Stream improved = improvedClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + Stream transition = transitionClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + return Stream.concat(improved, transition); + } + /** * These functions provide a stream of arguments for parameterized tests. * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption @@ -541,8 +577,6 @@ public static void Decrypt( Decrypt(client, S3ECId, crossLanguageObjects, expectedEncryptionAlgorithm, crossLanguageObjects); } - - public static void Decrypt( S3ECTestServerClient client, String S3ECId, From c7f19ec3824ee4011ae67107d6106cae1856e189 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 19 Nov 2025 21:48:30 -0800 Subject: [PATCH 17/46] fix cpp --- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 87402c99..4039810c 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 87402c99fd3c9107c6ccc6edf545fd4b05b2b551 +Subproject commit 4039810cd5d32429a64e70733175940d4a73f13c From ab1ba3a28824525da0256e49e9e3579b11e473e4 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 10:19:52 -0800 Subject: [PATCH 18/46] go concurency --- test-server/go-v3-server/main.go | 14 +++++++++++--- test-server/go-v3-transition-server/main.go | 14 +++++++++++--- test-server/go-v4-server/main.go | 14 +++++++++++--- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/test-server/go-v3-server/main.go b/test-server/go-v3-server/main.go index a79f007e..0384c5ff 100644 --- a/test-server/go-v3-server/main.go +++ b/test-server/go-v3-server/main.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "strings" + "sync" "github.com/aws/amazon-s3-encryption-client-go/v3/client" "github.com/aws/amazon-s3-encryption-client-go/v3/materials" @@ -23,6 +24,7 @@ import ( type Server struct { clientCache map[string]*client.S3EncryptionClientV3 kmsClient *kms.Client + mu sync.RWMutex } // CreateClientInput represents the input for creating a client @@ -182,8 +184,10 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { // Generate client ID clientID := uuid.New().String() - // Store client in cache + // Store client in cache (protected by mutex) + s.mu.Lock() s.clientCache[clientID] = s3EncryptionClient + s.mu.Unlock() // Return response w.Header().Set("Content-Type", "application/json") @@ -204,8 +208,10 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) @@ -274,8 +280,10 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) diff --git a/test-server/go-v3-transition-server/main.go b/test-server/go-v3-transition-server/main.go index 3c92582e..64556f12 100644 --- a/test-server/go-v3-transition-server/main.go +++ b/test-server/go-v3-transition-server/main.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "strings" + "sync" "github.com/aws/amazon-s3-encryption-client-go/v3/client" "github.com/aws/amazon-s3-encryption-client-go/v3/materials" @@ -24,6 +25,7 @@ import ( type Server struct { clientCache map[string]*client.S3EncryptionClientV3 kmsClient *kms.Client + mu sync.RWMutex } // CreateClientInput represents the input for creating a client @@ -199,8 +201,10 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { // Generate client ID clientID := uuid.New().String() - // Store client in cache + // Store client in cache (protected by mutex) + s.mu.Lock() s.clientCache[clientID] = s3EncryptionClient + s.mu.Unlock() // Return response w.Header().Set("Content-Type", "application/json") @@ -221,8 +225,10 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) @@ -291,8 +297,10 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go index d9d897aa..50999e95 100644 --- a/test-server/go-v4-server/main.go +++ b/test-server/go-v4-server/main.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "strings" + "sync" "github.com/aws/amazon-s3-encryption-client-go/v4/client" "github.com/aws/amazon-s3-encryption-client-go/v4/materials" @@ -24,6 +25,7 @@ import ( type Server struct { clientCache map[string]*client.S3EncryptionClientV4 kmsClient *kms.Client + mu sync.RWMutex } // CreateClientInput represents the input for creating a client @@ -199,8 +201,10 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { // Generate client ID clientID := uuid.New().String() - // Store client in cache + // Store client in cache (protected by mutex) + s.mu.Lock() s.clientCache[clientID] = s3EncryptionClient + s.mu.Unlock() // Return response w.Header().Set("Content-Type", "application/json") @@ -221,8 +225,10 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) @@ -291,8 +297,10 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { return } - // Get client from cache + // Get client from cache (protected by mutex) + s.mu.RLock() client, exists := s.clientCache[clientID] + s.mu.RUnlock() if !exists { s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) From 99f05fe6dbc8c7f0d8c3aab24459ef4d90269722 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 12:09:14 -0800 Subject: [PATCH 19/46] cpp is hard --- test-server/cpp-v2-server/Makefile | 4 +- test-server/cpp-v2-server/main.cpp | 5 ++- test-server/cpp-v2-transition-server/Makefile | 4 +- test-server/cpp-v2-transition-server/main.cpp | 3 +- test-server/cpp-v3-server/Makefile | 4 +- test-server/cpp-v3-server/main.cpp | 38 ++++++++++++++----- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index e4d2f954..2d0a4b55 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -20,7 +20,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) + ./s3ec-server > ../server.log 2>&1 & echo $$! > ../$(PID_FILE) @echo "Cpp V2 server starting..." stop-server: @@ -31,7 +31,7 @@ stop-server: kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ rm -f $(PID_FILE); \ fi - @rm -f build/server.log + @rm -f server.log @echo "Server stopped" wait-for-server: diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 1b2de659..8ba5a49a 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -283,7 +283,10 @@ int main() { int port = 8085; struct MHD_Daemon *daemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, - &request_handler, NULL, MHD_OPTION_END); + &request_handler, NULL, + MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_TIMEOUT, 30, + MHD_OPTION_END); if (!daemon) { fprintf(stderr, "Failed to start server on port %d\n", port); diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 2ca6ee5f..0383b4d8 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -19,7 +19,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) + ./s3ec-server > ../server.log 2>&1 & echo $$! > ../$(PID_FILE) @echo "Cpp transition server starting..." stop-server: @@ -30,7 +30,7 @@ stop-server: kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ rm -f $(PID_FILE); \ fi - @rm -f build/server.log + @rm -f server.log @echo "Server stopped" wait-for-server: diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 535a3e0a..f7dbef11 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -413,7 +413,8 @@ int main() { MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 100, + MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); if (!daemon) { diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 05a286f0..e90c8d73 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -19,7 +19,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) + ./s3ec-server > ../server.log 2>&1 & echo $$! > ../$(PID_FILE) @echo "Cpp V3 server starting..." stop-server: @@ -30,7 +30,7 @@ stop-server: kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ rm -f $(PID_FILE); \ fi - @rm -f build/server.log + @rm -f server.log @echo "Server stopped" wait-for-server: diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 8a795098..bbc3fbf1 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -288,8 +288,12 @@ 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) { + fprintf(stderr, "[CPP-V3] GetObject request: bucket=%s, key=%s, client_id=%s\n", + bucket.c_str(), key.c_str(), client_id.c_str()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V3] GetObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -306,15 +310,17 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, auto &stream = outcome.GetResult().GetBody(); std::string content((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + fprintf(stderr, "[CPP-V3] GetObject success: bucket=%s, key=%s, size=%zu bytes\n", + bucket.c_str(), key.c_str(), content.length()); return send_response(connection, 200, content); } else { auto msg = make_error(outcome.GetError().GetMessage(), 500); - fprintf(stderr, "handle_get_object error %s\n", msg.c_str()); + fprintf(stderr, "[CPP-V3] GetObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { - fprintf(stderr, "handle_get_object exception %s\n", e.what()); - auto msg = make_error("An exception was thrown", 500); + fprintf(stderr, "[CPP-V3] GetObject exception: %s\n", e.what()); + auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } } @@ -324,8 +330,12 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, const std::string &client_id, const std::string &body, const std::string &metadata) { + fprintf(stderr, "[CPP-V3] PutObject request: bucket=%s, key=%s, client_id=%s, body_size=%zu\n", + bucket.c_str(), key.c_str(), client_id.c_str(), body.length()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V3] PutObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -348,15 +358,16 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, // body_ptr keeps the data alive through this entire operation auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { + fprintf(stderr, "[CPP-V3] PutObject success: bucket=%s, key=%s\n", bucket.c_str(), key.c_str()); json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); } else { auto msg = make_error(outcome.GetError().GetMessage(), 500); - fprintf(stderr, "handle_put_object error %s\n", msg.c_str()); + fprintf(stderr, "[CPP-V3] PutObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { - fprintf(stderr, "handle_put_object exception %s\n", e.what()); + fprintf(stderr, "[CPP-V3] PutObject exception: %s\n", e.what()); auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } @@ -387,19 +398,23 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, } // Accumulate request body data for POST/PUT requests - if (is_push && *upload_data_size) { + if (is_push && *upload_data_size > 0) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); *upload_data_size = 0; return MHD_YES; } + + // At this point, *upload_data_size == 0, meaning we have all the data + // Now we can safely process the request std::string url_str(url); // Handle client creation endpoint if (is_push && url_str == "/client") { std::string *body = static_cast(*con_cls); - return handle_create_client(connection, *body); + MHD_Result result = handle_create_client(connection, *body); + return result; } // Handle object operations @@ -413,10 +428,12 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string metadata = get_header_value(connection, "content-metadata"); if (method_str == "GET") { - return handle_get_object(connection, bucket, key, client_id, metadata); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + return result; } else if (method_str == "PUT") { std::string *body = static_cast(*con_cls); - return handle_put_object(connection, bucket, key, client_id, *body, metadata); + MHD_Result result = handle_put_object(connection, bucket, key, client_id, *body, metadata); + return result; } else { return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } @@ -437,7 +454,8 @@ int main() { MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 100, + MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); if (!daemon) { From 35e87728233d8503c8f9156e21842d8361643a25 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 12:34:33 -0800 Subject: [PATCH 20/46] try this --- test-server/cpp-v3-server/main.cpp | 35 ++++++++++++++++++++++--- test-server/java-tests/build.gradle.kts | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index bbc3fbf1..64f2c9ef 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -304,15 +304,40 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); + + // Keep outcome alive to ensure stream remains valid auto outcome = client->GetObject(request, kmsContextMap); if (outcome.IsSuccess()) { + // Read the stream completely before outcome goes out of scope auto &stream = outcome.GetResult().GetBody(); - std::string content((std::istreambuf_iterator(stream)), - std::istreambuf_iterator()); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string content = buffer.str(); + + // Validate we read something + if (content.empty() && stream.fail()) { + fprintf(stderr, "[CPP-V3] GetObject error: Failed to read stream for bucket=%s, key=%s\n", + bucket.c_str(), key.c_str()); + auto msg = make_error("Failed to read response stream", 500); + return send_response(connection, 500, msg); + } + fprintf(stderr, "[CPP-V3] GetObject success: bucket=%s, key=%s, size=%zu bytes\n", bucket.c_str(), key.c_str(), content.length()); - return send_response(connection, 200, content); + + // Create and send response + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); + + // Add keep-alive header + MHD_add_response_header(response, "Connection", "keep-alive"); + MHD_add_response_header(response, "Keep-Alive", "timeout=30, max=100"); + + MHD_Result ret = MHD_queue_response(connection, 200, response); + MHD_destroy_response(response); + + return ret; } else { auto msg = make_error(outcome.GetError().GetMessage(), 500); fprintf(stderr, "[CPP-V3] GetObject AWS error: %s\n", msg.c_str()); @@ -322,6 +347,10 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, fprintf(stderr, "[CPP-V3] GetObject exception: %s\n", e.what()); auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); + } catch (...) { + fprintf(stderr, "[CPP-V3] GetObject unknown exception\n"); + auto msg = make_error("Unknown error in GetObject", 500); + return send_response(connection, 500, msg); } } diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 14c3eec1..1c0d4308 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -61,7 +61,7 @@ tasks { 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 + Math.max(1, Runtime.getRuntime().availableProcessors() - 6).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")) From 954b55921ceadd8dfcf1ac8366a5eb55e664d06b Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 14:05:42 -0800 Subject: [PATCH 21/46] next cut --- test-server/cpp-v2-server/main.cpp | 2 +- test-server/cpp-v2-transition-server/main.cpp | 2 +- test-server/cpp-v3-server/main.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 8ba5a49a..33e60bfb 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -284,7 +284,7 @@ int main() { struct MHD_Daemon *daemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, &request_handler, NULL, - MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_LIMIT, 100, MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index f7dbef11..8163376f 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -413,7 +413,7 @@ int main() { MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_LIMIT, 100, MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 64f2c9ef..c9cac063 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -483,7 +483,7 @@ int main() { MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 250, + MHD_OPTION_CONNECTION_LIMIT, 100, MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); From ff1a84a262947ab0fd8462889ce3e829d09d994a Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:19:48 -0500 Subject: [PATCH 22/46] Enable address sanitizer during tests (#105) Fail loud! From 0858f67d21e06a8c1ab002acf34854d05fe3f5ff Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 14:29:56 -0800 Subject: [PATCH 23/46] try not deleting the body --- test-server/cpp-v3-server/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index c9cac063..ac1a8dd7 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -407,7 +407,9 @@ void request_completed(void *cls, struct MHD_Connection *connection, // Clean up the request-specific context when request is truly complete if (*con_cls != nullptr) { std::string *body = static_cast(*con_cls); - delete body; + // This tests that we never can delete a string that someone is using. + // this seems safe to do for a test server because these strings are small. + // delete body; *con_cls = nullptr; } } From 5bfc9af38f0834b87e091d93ef73c643680a9950 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 14:35:18 -0800 Subject: [PATCH 24/46] simplify? --- test-server/cpp-v3-server/main.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index ac1a8dd7..c287c781 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -285,9 +285,10 @@ void fill_context(Aws::Map &map, } 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) { + std::string bucket, + std::string key, + std::string client_id, + std::string metadata) { fprintf(stderr, "[CPP-V3] GetObject request: bucket=%s, key=%s, client_id=%s\n", bucket.c_str(), key.c_str(), client_id.c_str()); @@ -355,10 +356,11 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, } MHD_Result handle_put_object(struct MHD_Connection *connection, - const std::string &bucket, const std::string &key, - const std::string &client_id, - const std::string &body, - const std::string &metadata) { + std::string bucket, + std::string key, + std::string client_id, + std::string body, + std::string metadata) { fprintf(stderr, "[CPP-V3] PutObject request: bucket=%s, key=%s, client_id=%s, body_size=%zu\n", bucket.c_str(), key.c_str(), client_id.c_str(), body.length()); From 8d9ff3052860ba9f31d0e5cfd3479fe5acccc269 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 14:44:17 -0800 Subject: [PATCH 25/46] try less work --- test-server/java-tests/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 1c0d4308..d30888a3 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -61,7 +61,7 @@ tasks { 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() - 6).toString()) // Scale with CPU, reserve 2 cores + Math.max(1, Runtime.getRuntime().availableProcessors() - 9).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")) From 03d315179e827072701ebc2b408dd781122231cd Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 15:17:31 -0800 Subject: [PATCH 26/46] try a different direction --- test-server/cpp-v3-server/main.cpp | 96 +++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index c287c781..95b5b523 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -1,3 +1,44 @@ +/* + * S3 Encryption Test Server - C++ V3 + * + * CONCURRENCY AND SYNCHRONIZATION DESIGN: + * + * 1. Threading Model: + * - Uses MHD_USE_SELECT_INTERNALLY with fixed thread pool + * - Thread pool size = CPU cores * 2 (auto-detected at startup) + * - Threads are reused across connections for efficiency + * - I/O multiplexing (select/poll) distributes connections across thread pool + * - All S3 operations are SYNCHRONOUS - server waits for S3 completion before responding + * + * 2. Resource Scaling: + * - All limits automatically scale with detected CPU count: + * * Thread pool size = num_cores * 2 + * * Connection limit = num_cores * 2 + * * S3 client maxConnections = num_cores * 2 + * - Multiplier of 2 accounts for I/O blocking without starving throughput + * - Ensures optimal resource usage on any hardware configuration + * + * 3. Client Cache (client_cache_secret): + * - Protected by std::shared_mutex for efficient concurrent access + * - get_client() uses shared_lock (multiple threads can read simultaneously) + * - set_client() uses unique_lock (exclusive write access) + * - This allows concurrent GET/PUT operations without serialization + * - UUID-based keys guarantee uniqueness (always insert, never update) + * + * 4. Memory Management: + * - Request body allocated in request_handler (*con_cls = new std::string()) + * - Body lifetime managed by libmicrohttpd - valid until request_completed() + * - All handler functions complete synchronously before returning + * - request_completed() safely deletes body after response sent + * - No memory leaks under sustained concurrent load + * + * 5. Synchronous Operation Guarantees: + * - GetObject: Waits for S3, reads full response stream, then returns + * - PutObject: Waits for S3 operation to complete, then returns + * - No async callbacks or background operations + * - Client receives response only after S3 operation completes + */ + #include #include #include @@ -15,12 +56,16 @@ #include #include #include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; std::unordered_map> client_cache_secret; -std::mutex client_mutex; +std::shared_mutex client_mutex; // Using shared_mutex for concurrent reads + +// Threading configuration - set at startup based on CPU cores +unsigned int g_thread_pool_size = 8; // Default, will be overwritten in main() std::string generate_uuid() { uuid_t uuid; @@ -32,7 +77,8 @@ std::string generate_uuid() { std::shared_ptr get_client(const std::string &client_id) { - std::lock_guard lock(client_mutex); + // Use shared_lock for concurrent reads - multiple threads can read simultaneously + std::shared_lock lock(client_mutex); auto it = client_cache_secret.find(client_id); if (it == client_cache_secret.end()) { return std::shared_ptr(); @@ -43,8 +89,10 @@ std::shared_ptr get_client(const std::string &client_id) void set_client(const std::string &client_id, std::shared_ptr client) { - std::lock_guard lock(client_mutex); - client_cache_secret[client_id] = client; + // UUID guarantees unique keys - always insert, never update + // Still need exclusive lock because std::unordered_map isn't thread-safe for concurrent inserts + std::unique_lock lock(client_mutex); + client_cache_secret.emplace(client_id, client); } std::string get_header_value(struct MHD_Connection *connection, @@ -87,11 +135,11 @@ std::string get_config(json & request, const char * x) MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { - // Make a copy of body so we own the data even if request_completed fires - std::string body_copy(body); + // Body is kept alive by *con_cls until request_completed fires, so it's safe to use directly + // All operations here are synchronous and complete before returning to caller try { - json request = json::parse(body_copy); + json request = json::parse(body); // Extract all key material types std::string kms_key_id; @@ -179,8 +227,9 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, } // Configure ClientConfiguration with retry strategy for throttling + // Match S3 connection pool size to thread pool size for optimal resource usage Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = 25; + clientConfig.maxConnections = g_thread_pool_size; clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); encryption_client = std::make_shared(config, clientConfig); @@ -206,8 +255,9 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, } // Configure ClientConfiguration with retry strategy for throttling + // Match S3 connection pool size to thread pool size for optimal resource usage Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = 25; + clientConfig.maxConnections = g_thread_pool_size; clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); encryption_client = std::make_shared(config, clientConfig); @@ -407,11 +457,10 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { // Clean up the request-specific context when request is truly complete + // This is called AFTER all handlers have returned and the response has been sent if (*con_cls != nullptr) { std::string *body = static_cast(*con_cls); - // This tests that we never can delete a string that someone is using. - // this seems safe to do for a test server because these strings are small. - // delete body; + delete body; // Safe to delete now - all synchronous operations are complete *con_cls = nullptr; } } @@ -481,13 +530,32 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, int main() { Aws::SDKOptions options; Aws::InitAPI(options); + + // Detect CPU core count and configure threading + unsigned int num_cores = std::thread::hardware_concurrency(); + if (num_cores == 0) { + num_cores = 4; // Fallback if detection fails + fprintf(stderr, "[WARNING] CPU core detection failed, defaulting to %u cores\n", num_cores); + } + + // Thread pool size = num_cores * 2 (allows for I/O blocking without starving throughput) + g_thread_pool_size = num_cores * 2; + unsigned int connection_limit = g_thread_pool_size; + + // Log configuration + fprintf(stderr, "[CONFIG] Detected CPU cores: %u\n", num_cores); + fprintf(stderr, "[CONFIG] Thread pool size: %u\n", g_thread_pool_size); + fprintf(stderr, "[CONFIG] Connection limit: %u\n", connection_limit); + fprintf(stderr, "[CONFIG] S3 client maxConnections: %u\n", g_thread_pool_size); + int port = 8091; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, + MHD_start_daemon(MHD_USE_SELECT_INTERNALLY | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 100, + MHD_OPTION_THREAD_POOL_SIZE, g_thread_pool_size, + MHD_OPTION_CONNECTION_LIMIT, connection_limit, MHD_OPTION_CONNECTION_TIMEOUT, 30, MHD_OPTION_END); From b4416dc16d0db8f5d4b26a30c0c526c2e9d32593 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 15:36:37 -0800 Subject: [PATCH 27/46] cpp build update --- test-server/cpp-v3-server/main.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 95b5b523..4663b4ec 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -56,13 +56,14 @@ #include #include #include +#include #include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; std::unordered_map> client_cache_secret; -std::shared_mutex client_mutex; // Using shared_mutex for concurrent reads +std::shared_timed_mutex client_mutex; // Using shared_timed_mutex (C++14 compatible) for concurrent reads // Threading configuration - set at startup based on CPU cores unsigned int g_thread_pool_size = 8; // Default, will be overwritten in main() @@ -78,7 +79,7 @@ std::string generate_uuid() { std::shared_ptr get_client(const std::string &client_id) { // Use shared_lock for concurrent reads - multiple threads can read simultaneously - std::shared_lock lock(client_mutex); + std::shared_lock lock(client_mutex); auto it = client_cache_secret.find(client_id); if (it == client_cache_secret.end()) { return std::shared_ptr(); @@ -91,7 +92,7 @@ void set_client(const std::string &client_id, std::shared_ptr lock(client_mutex); + std::unique_lock lock(client_mutex); client_cache_secret.emplace(client_id, client); } From 30d797e11145b624397a885233fcf17f1e704d32 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Thu, 20 Nov 2025 19:36:33 -0800 Subject: [PATCH 28/46] Making C test servers concurent Make the servers cuoncurent. Needed to add a LRU, so there is a subtle difference with Cpp. Also added logging, including SDK logging. --- test-server/cpp-v2-server/main.cpp | 435 ++++++++++++++++-- test-server/cpp-v2-transition-server/main.cpp | 433 ++++++++++++++--- test-server/cpp-v3-server/main.cpp | 326 ++++++++++--- test-server/java-tests/build.gradle.kts | 2 +- test-server/php-v2-server/Makefile | 2 +- test-server/php-v2-transition-server/Makefile | 2 +- test-server/php-v3-server/Makefile | 2 +- 7 files changed, 1015 insertions(+), 187 deletions(-) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 33e60bfb..f5c4e406 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -1,8 +1,53 @@ +/* + * S3 Encryption Test Server - C++ V2 + * + * CONCURRENCY AND SYNCHRONIZATION DESIGN: + * + * 1. Threading Model: + * - Uses MHD_USE_POLL_INTERNALLY with fixed thread pool + * - Thread pool size = CPU cores * 2 (auto-detected at startup) + * - Threads are reused across connections for efficiency + * - I/O multiplexing (poll) distributes connections across thread pool + * - All S3 operations are SYNCHRONOUS - server waits for S3 completion before responding + * - POLL mechanism avoids FD_SETSIZE=1024 limitation of select() + * + * 2. Resource Scaling: + * - All limits automatically scale with detected CPU count: + * * Thread pool size = num_cores * 2 + * * Connection limit = num_cores * 2 + * * S3 client maxConnections = num_cores * 2 + * - Multiplier of 2 accounts for I/O blocking without starving throughput + * - Ensures optimal resource usage on any hardware configuration + * + * 3. Client Cache (client_cache_secret): + * - Protected by std::shared_mutex for efficient concurrent access + * - get_client() uses shared_lock (multiple threads can read simultaneously) + * - set_client() uses unique_lock (exclusive write access) + * - This allows concurrent GET/PUT operations without serialization + * - UUID-based keys guarantee uniqueness (always insert, never update) + * + * 4. Memory Management: + * - Request body allocated in request_handler (*con_cls = new std::string()) + * - Body lifetime managed by libmicrohttpd - valid until request_completed() + * - All handler functions complete synchronously before returning + * - request_completed() safely deletes body after response sent + * - No memory leaks under sustained concurrent load + * + * 5. Synchronous Operation Guarantees: + * - GetObject: Waits for S3, reads full response stream, then returns + * - PutObject: Waits for S3 operation to complete, then returns + * - No async callbacks or background operations + * - Client receives response only after S3 operation completes + */ + #include +#include #include #include #include #include +#include +#include #include #include #include @@ -10,14 +55,33 @@ #include #include +#include #include #include +#include +#include +#include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> client_cache_secret; -std::mutex client_mutex; + +// LRU cache for S3 encryption clients +// Limits memory and connection pool growth by evicting least recently used clients +const size_t MAX_CACHED_CLIENTS = 100; // Reasonable limit for concurrent test operations + +struct ClientCacheEntry { + std::shared_ptr client; + std::list::iterator lru_iter; +}; + +std::unordered_map client_cache_secret; +std::list lru_order; // Most recently used at front +std::shared_timed_mutex client_mutex; // Using shared_timed_mutex (C++14 compatible) for concurrent reads + +// Threading configuration - set at startup based on CPU cores +unsigned int g_thread_pool_size = 8; // Default, will be overwritten in main() std::string generate_uuid() { uuid_t uuid; @@ -29,19 +93,51 @@ std::string generate_uuid() { std::shared_ptr get_client(const std::string &client_id) { - std::lock_guard lock(client_mutex); + // Need unique_lock to update LRU order even on reads + std::unique_lock 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; + // Move to front of LRU list (mark as most recently used) + lru_order.erase(it->second.lru_iter); + lru_order.push_front(client_id); + it->second.lru_iter = lru_order.begin(); + + return it->second.client; } } void set_client(const std::string &client_id, std::shared_ptr client) { - std::lock_guard lock(client_mutex); - client_cache_secret[client_id] = client; + // UUID guarantees unique keys - always insert, never update + // Still need exclusive lock because std::unordered_map isn't thread-safe for concurrent inserts + std::unique_lock lock(client_mutex); + + // Add to front of LRU list (most recently used) + lru_order.push_front(client_id); + + ClientCacheEntry entry; + entry.client = client; + entry.lru_iter = lru_order.begin(); + + client_cache_secret.emplace(client_id, entry); + + // Evict least recently used clients if we exceed the limit + while (client_cache_secret.size() > MAX_CACHED_CLIENTS) { + std::string lru_client_id = lru_order.back(); + lru_order.pop_back(); + + auto evict_it = client_cache_secret.find(lru_client_id); + if (evict_it != client_cache_secret.end()) { + fprintf(stderr, "[CPP-V2] [CACHE-EVICT] Evicting client %s (cache size was %zu)\n", + lru_client_id.c_str(), client_cache_secret.size()); + client_cache_secret.erase(evict_it); + } + } + + fprintf(stderr, "[CPP-V2] [CACHE-ADD] Added client %s (cache size now %zu)\n", + client_id.c_str(), client_cache_secret.size()); } std::string get_header_value(struct MHD_Connection *connection, @@ -69,6 +165,9 @@ std::string make_error(const std::string &message, int status_code) { MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { + // Body is kept alive by *con_cls until request_completed fires, so it's safe to use directly + // All operations here are synchronous and complete before returning to caller + try { json request = json::parse(body); std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; @@ -88,7 +187,12 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, if (inst_put) config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - auto encryption_client = std::make_shared(config); + // Each client gets a large connection pool since we cannot share HTTP clients + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 512; // Large pool per client + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + auto encryption_client = std::make_shared(config, clientConfig); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); @@ -96,6 +200,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2] handle_create_client exception: %s\n", e.what()); return send_response(connection, 500, "{\"error\":\"An exception was thrown.\"}"); } catch (...) { @@ -106,13 +211,18 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, void fill_context(Aws::Map &map, const std::string &metadata) { if (metadata.empty()) { + fprintf(stderr, "[CPP-V2] [DEBUG] fill_context: metadata is empty\n"); return; } + fprintf(stderr, "[CPP-V2] [DEBUG] fill_context: raw metadata='%s' (length=%zu)\n", + metadata.c_str(), metadata.length()); + // Parse metadata format: [key1]:[value1],[key2]:[value2],... // or single pair: [key]:[value] std::string current = metadata; size_t pos = 0; + int pair_count = 0; while (pos < current.length()) { // Find opening bracket for key @@ -145,6 +255,9 @@ void fill_context(Aws::Map &map, std::string value = current.substr(value_start + 1, value_end - value_start - 1); + fprintf(stderr, "[CPP-V2] [DEBUG] fill_context: parsed pair #%d: key='%s', value='%s'\n", + ++pair_count, key.c_str(), value.c_str()); + // Add to map map.emplace(key, value); @@ -155,14 +268,24 @@ void fill_context(Aws::Map &map, pos = comma + 1; } } + + fprintf(stderr, "[CPP-V2] [DEBUG] fill_context: completed, parsed %d pairs into map\n", pair_count); } 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) { + std::string bucket, + std::string key, + std::string client_id, + std::string metadata) { + // Get thread ID for debugging concurrent operations + std::thread::id thread_id = std::this_thread::get_id(); + + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V2] GetObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -173,34 +296,98 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); + + // Log the encryption context map size and contents + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject: encryption context map size=%zu\n", kmsContextMap.size()); + for (const auto& pair : kmsContextMap) { + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject: context['%s']='%s'\n", + pair.first.c_str(), pair.second.c_str()); + } + + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject: calling client->GetObject() for key=%s\n", key.c_str()); + + // Keep outcome alive to ensure stream remains valid auto outcome = client->GetObject(request, kmsContextMap); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject: client->GetObject() returned for key=%s\n", key.c_str()); + if (outcome.IsSuccess()) { + // Read the stream completely before outcome goes out of scope auto &stream = outcome.GetResult().GetBody(); - std::string content((std::istreambuf_iterator(stream)), - std::istreambuf_iterator()); - return send_response(connection, 200, content); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string content = buffer.str(); + + // Validate we read something + if (content.empty() && stream.fail()) { + fprintf(stderr, "[CPP-V2] GetObject error: Failed to read stream for bucket=%s, key=%s\n", + bucket.c_str(), key.c_str()); + auto msg = make_error("Failed to read response stream", 500); + return send_response(connection, 500, msg); + } + + fprintf(stderr, "[CPP-V2] GetObject success: bucket=%s, key=%s, size=%zu bytes\n", + bucket.c_str(), key.c_str(), content.length()); + + // Create and send response + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); + + // Add keep-alive header + MHD_add_response_header(response, "Connection", "keep-alive"); + MHD_add_response_header(response, "Keep-Alive", "timeout=30, max=100"); + + MHD_Result ret = MHD_queue_response(connection, 200, response); + MHD_destroy_response(response); + + return ret; } else { + // Enhanced error logging with thread info + auto error = outcome.GetError(); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject FAILED: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject error details:\n"); + fprintf(stderr, "[CPP-V2] [DEBUG] - Message: %s\n", error.GetMessage().c_str()); + fprintf(stderr, "[CPP-V2] [DEBUG] - ExceptionName: %s\n", error.GetExceptionName().c_str()); + fprintf(stderr, "[CPP-V2] [DEBUG] - ResponseCode: %d\n", (int)error.GetResponseCode()); + fprintf(stderr, "[CPP-V2] [DEBUG] - ShouldRetry: %s\n", error.ShouldRetry() ? "true" : "false"); + auto msg = make_error(outcome.GetError().GetMessage(), 500); + fprintf(stderr, "[CPP-V2] GetObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { - auto msg = make_error("An exception was thrown", 500); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject EXCEPTION: thread=%lu, key=%s, what=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str(), e.what()); + auto msg = make_error(e.what(), 500); + return send_response(connection, 500, msg); + } catch (...) { + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject UNKNOWN EXCEPTION: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); + auto msg = make_error("Unknown error in GetObject", 500); return send_response(connection, 500, msg); } } MHD_Result handle_put_object(struct MHD_Connection *connection, - const std::string &bucket, const std::string &key, - const std::string &client_id, - const std::string &body, - const std::string &metadata) { + std::string bucket, + std::string key, + std::string client_id, + std::string body, + std::string metadata) { + fprintf(stderr, "[CPP-V2] PutObject request: bucket=%s, key=%s, client_id=%s, body_size=%zu\n", + bucket.c_str(), key.c_str(), client_id.c_str(), body.length()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V2] PutObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } try { + // Create owned copy of body data to ensure it lives through the S3 operation + auto body_ptr = std::make_shared(body); + Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -208,88 +395,248 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, request.SetBucket(bucket); request.SetKey(key); - auto stream = std::make_shared(body); + // Create stream from owned body data + auto stream = std::make_shared(*body_ptr); request.SetBody(stream); + // Synchronous call - waits for S3 operation to complete + // body_ptr keeps the data alive through this entire operation auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { + fprintf(stderr, "[CPP-V2] PutObject success: bucket=%s, key=%s\n", bucket.c_str(), key.c_str()); json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); } else { auto msg = make_error(outcome.GetError().GetMessage(), 500); + fprintf(stderr, "[CPP-V2] PutObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2] PutObject exception: %s\n", e.what()); auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } } +void request_completed(void *cls, struct MHD_Connection *connection, + void **con_cls, enum MHD_RequestTerminationCode toe) { + // Clean up the request-specific context when request is truly complete + // This is called AFTER all handlers have returned and the response has been sent + + // Log why the request was terminated + const char* reason = "UNKNOWN"; + switch (toe) { + case MHD_REQUEST_TERMINATED_COMPLETED_OK: + reason = "COMPLETED_OK"; + break; + case MHD_REQUEST_TERMINATED_WITH_ERROR: + reason = "WITH_ERROR"; + break; + case MHD_REQUEST_TERMINATED_TIMEOUT_REACHED: + reason = "TIMEOUT_REACHED"; + break; + case MHD_REQUEST_TERMINATED_DAEMON_SHUTDOWN: + reason = "DAEMON_SHUTDOWN"; + break; + case MHD_REQUEST_TERMINATED_READ_ERROR: + reason = "READ_ERROR"; + break; + case MHD_REQUEST_TERMINATED_CLIENT_ABORT: + reason = "CLIENT_ABORT"; + break; + } + fprintf(stderr, "[CPP-V2] request_completed called, reason=%s, con_cls=%p\n", + reason, *con_cls); + + if (*con_cls != nullptr) { + std::string *body = static_cast(*con_cls); + delete body; // Safe to delete now - all synchronous operations are complete + *con_cls = nullptr; + } +} + MHD_Result request_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { - std::string method_str(method); - bool is_push = method_str == "POST" || method_str == "PUT"; - static int dummy; - if (*con_cls == nullptr) { - if (is_push) { - *con_cls = new std::string(); - } else { - *con_cls = &dummy; + try { + std::string method_str(method); + std::string url_str(url); + bool is_push = method_str == "POST" || method_str == "PUT"; + + // LOG: Every request entry (even first-time calls) + if (*con_cls == nullptr) { + fprintf(stderr, "[CPP-V2] REQUEST START: method=%s, url=%s, version=%s, con_cls=NULL, upload_data_size=%zu\n", + method, url, version, *upload_data_size); } + + // Initialize request context on first call + if (*con_cls == nullptr) { + // Allocate unique state for each request to avoid race conditions + *con_cls = new std::string(); + fprintf(stderr, "[CPP-V2] REQUEST INIT: allocated new request context for %s %s\n", method, url); return MHD_YES; } - if (is_push && *upload_data_size) { + + // LOG: Subsequent calls + if (is_push && *upload_data_size > 0) { + fprintf(stderr, "[CPP-V2] REQUEST DATA: %s %s receiving %zu bytes\n", method, url, *upload_data_size); + } else if (*upload_data_size == 0) { + fprintf(stderr, "[CPP-V2] REQUEST COMPLETE: %s %s ready for processing\n", method, url); + } + + // Accumulate request body data for POST/PUT requests + if (is_push && *upload_data_size > 0) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); *upload_data_size = 0; return MHD_YES; } - - std::string url_str(url); - + + // At this point, *upload_data_size == 0, meaning we have all the data + // Now we can safely process the request + + // LOG: About to process request + fprintf(stderr, "[CPP-V2] PROCESSING: %s %s\n", method, url); + + // Handle client creation endpoint if (is_push && url_str == "/client") { - std::unique_ptr body(static_cast(*con_cls)); - return handle_create_client(connection, *body); + fprintf(stderr, "[CPP-V2] Handling /client endpoint\n"); + std::string *body = static_cast(*con_cls); + MHD_Result result = handle_create_client(connection, *body); + fprintf(stderr, "[CPP-V2] /client handler returned: %d\n", result); + return result; } + // Handle object operations if (url_str.find("/object/") == 0) { + fprintf(stderr, "[CPP-V2] Handling /object/ endpoint\n"); std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); if (slash_pos != std::string::npos) { std::string bucket = path.substr(0, slash_pos); std::string key = path.substr(slash_pos + 1); std::string client_id = get_header_value(connection, "clientid"); - std::string metadata = get_header_value(connection, "content-metadata"); + + fprintf(stderr, "[CPP-V2] Object operation: bucket=%s, key=%s, client_id=%s, method=%s\n", + bucket.c_str(), key.c_str(), client_id.c_str(), method); + if (method_str == "GET") { - return handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V2] Dispatching to handle_get_object\n"); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V2] handle_get_object returned: %d\n", result); + return result; } else if (method_str == "PUT") { - std::unique_ptr body(static_cast(*con_cls)); - *upload_data_size = 0; - return handle_put_object(connection, bucket, key, client_id, *body, metadata); + fprintf(stderr, "[CPP-V2] Dispatching to handle_put_object\n"); + std::string *body = static_cast(*con_cls); + MHD_Result result = handle_put_object(connection, bucket, key, client_id, *body, metadata); + fprintf(stderr, "[CPP-V2] handle_put_object returned: %d\n", result); + return result; + } else { + fprintf(stderr, "[CPP-V2] Method not allowed: %s\n", method); + return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } } } - return send_response(connection, 404, - "{\"error\":\"Not idea what is happening\"}"); + // Return error for unrecognized endpoints + fprintf(stderr, "[CPP-V2] ERROR: Unrecognized endpoint: %s %s\n", method, url); + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); + } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2] FATAL: Unhandled exception in request_handler: %s (method=%s, url=%s)\n", + e.what(), method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unhandled exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V2] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } catch (...) { + fprintf(stderr, "[CPP-V2] FATAL: Unknown exception in request_handler (method=%s, url=%s)\n", + method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unknown exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V2] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } +} + +// Error log callback for libmicrohttpd +void log_mhd_error(void* cls, const char* fmt, va_list ap) { + fprintf(stderr, "[CPP-V2] [MHD-ERROR] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); +} + +// Connection notification callback - called when a client connects +MHD_Result notify_connection(void *cls, + struct MHD_Connection *connection, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) { + if (toe == MHD_CONNECTION_NOTIFY_STARTED) { + fprintf(stderr, "[CPP-V2] [MHD-CONNECT] New connection started\n"); + } else if (toe == MHD_CONNECTION_NOTIFY_CLOSED) { + fprintf(stderr, "[CPP-V2] [MHD-DISCONNECT] Connection closed\n"); + } + return MHD_YES; } int main() { Aws::SDKOptions options; + + // Configure AWS SDK logging to output to stderr (which goes to server.log) + // Using Debug level to capture all SDK activity including CryptoModule errors + options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug; + options.loggingOptions.logger_create_fn = []() { + return std::make_shared( + Aws::Utils::Logging::LogLevel::Debug + ); + }; + + fprintf(stderr, "[CONFIG] AWS SDK logging enabled at Debug level\n"); + Aws::InitAPI(options); + + // Detect CPU core count and configure threading + unsigned int num_cores = std::thread::hardware_concurrency(); + if (num_cores == 0) { + num_cores = 4; + fprintf(stderr, "[CPP-V2] [WARNING] CPU core detection failed, defaulting to %u cores\n", num_cores); + } + + g_thread_pool_size = num_cores * 2; + unsigned int connection_limit = g_thread_pool_size; + + // Log configuration + fprintf(stderr, "[CONFIG] Detected CPU cores: %u\n", num_cores); + fprintf(stderr, "[CONFIG] Thread pool size: %u\n", g_thread_pool_size); + fprintf(stderr, "[CONFIG] Connection limit: %u\n", connection_limit); + fprintf(stderr, "[CONFIG] Each S3 client will use 512 max connections\n"); + int port = 8085; + struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, + MHD_start_daemon(MHD_USE_POLL_INTERNALLY | MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_ERROR_LOG, + port, NULL, NULL, &request_handler, NULL, - MHD_OPTION_CONNECTION_LIMIT, 100, - MHD_OPTION_CONNECTION_TIMEOUT, 30, + MHD_OPTION_EXTERNAL_LOGGER, log_mhd_error, NULL, + MHD_OPTION_NOTIFY_CONNECTION, notify_connection, NULL, + MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, + MHD_OPTION_THREAD_POOL_SIZE, g_thread_pool_size, + MHD_OPTION_CONNECTION_LIMIT, connection_limit, + MHD_OPTION_CONNECTION_TIMEOUT, 10, MHD_OPTION_END); if (!daemon) { - fprintf(stderr, "Failed to start server on port %d\n", port); + fprintf(stderr, "[CPP-V2] Failed to start server on port %d\n", port); Aws::ShutdownAPI(options); return 1; } diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 8163376f..dc863723 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -1,10 +1,55 @@ +/* + * S3 Encryption Test Server - C++ V2 Transition + * + * CONCURRENCY AND SYNCHRONIZATION DESIGN: + * + * 1. Threading Model: + * - Uses MHD_USE_POLL_INTERNALLY with fixed thread pool + * - Thread pool size = CPU cores * 2 (auto-detected at startup) + * - Threads are reused across connections for efficiency + * - I/O multiplexing (poll) distributes connections across thread pool + * - All S3 operations are SYNCHRONOUS - server waits for S3 completion before responding + * - POLL mechanism avoids FD_SETSIZE=1024 limitation of select() + * + * 2. Resource Scaling: + * - All limits automatically scale with detected CPU count: + * * Thread pool size = num_cores * 2 + * * Connection limit = num_cores * 2 + * * S3 client maxConnections = num_cores * 2 + * - Multiplier of 2 accounts for I/O blocking without starving throughput + * - Ensures optimal resource usage on any hardware configuration + * + * 3. Client Cache (client_cache_secret): + * - Protected by std::shared_mutex for efficient concurrent access + * - get_client() uses shared_lock (multiple threads can read simultaneously) + * - set_client() uses unique_lock (exclusive write access) + * - This allows concurrent GET/PUT operations without serialization + * - UUID-based keys guarantee uniqueness (always insert, never update) + * + * 4. Memory Management: + * - Request body allocated in request_handler (*con_cls = new std::string()) + * - Body lifetime managed by libmicrohttpd - valid until request_completed() + * - All handler functions complete synchronously before returning + * - request_completed() safely deletes body after response sent + * - No memory leaks under sustained concurrent load + * + * 5. Synchronous Operation Guarantees: + * - GetObject: Waits for S3, reads full response stream, then returns + * - PutObject: Waits for S3 operation to complete, then returns + * - No async callbacks or background operations + * - Client receives response only after S3 operation completes + */ + #include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -12,14 +57,33 @@ #include #include +#include #include #include +#include +#include +#include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> client_cache_secret; -std::mutex client_mutex; + +// LRU cache for S3 encryption clients +// Limits memory and connection pool growth by evicting least recently used clients +const size_t MAX_CACHED_CLIENTS = 100; // Reasonable limit for concurrent test operations + +struct ClientCacheEntry { + std::shared_ptr client; + std::list::iterator lru_iter; +}; + +std::unordered_map client_cache_secret; +std::list lru_order; // Most recently used at front +std::shared_timed_mutex client_mutex; // Using shared_timed_mutex (C++14 compatible) for concurrent reads + +// Threading configuration - set at startup based on CPU cores +unsigned int g_thread_pool_size = 8; // Default, will be overwritten in main() std::string generate_uuid() { uuid_t uuid; @@ -31,19 +95,51 @@ std::string generate_uuid() { std::shared_ptr get_client(const std::string &client_id) { - std::lock_guard lock(client_mutex); + // Need unique_lock to update LRU order even on reads + std::unique_lock 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; + // Move to front of LRU list (mark as most recently used) + lru_order.erase(it->second.lru_iter); + lru_order.push_front(client_id); + it->second.lru_iter = lru_order.begin(); + + return it->second.client; } } void set_client(const std::string &client_id, std::shared_ptr client) { - std::lock_guard lock(client_mutex); - client_cache_secret[client_id] = client; + // UUID guarantees unique keys - always insert, never update + // Still need exclusive lock because std::unordered_map isn't thread-safe for concurrent inserts + std::unique_lock lock(client_mutex); + + // Add to front of LRU list (most recently used) + lru_order.push_front(client_id); + + ClientCacheEntry entry; + entry.client = client; + entry.lru_iter = lru_order.begin(); + + client_cache_secret.emplace(client_id, entry); + + // Evict least recently used clients if we exceed the limit + while (client_cache_secret.size() > MAX_CACHED_CLIENTS) { + std::string lru_client_id = lru_order.back(); + lru_order.pop_back(); + + auto evict_it = client_cache_secret.find(lru_client_id); + if (evict_it != client_cache_secret.end()) { + fprintf(stderr, "[CPP-V2-TRANSITION] [CACHE-EVICT] Evicting client %s (cache size was %zu)\n", + lru_client_id.c_str(), client_cache_secret.size()); + client_cache_secret.erase(evict_it); + } + } + + fprintf(stderr, "[CPP-V2-TRANSITION] [CACHE-ADD] Added client %s (cache size now %zu)\n", + client_id.c_str(), client_cache_secret.size()); } std::string get_header_value(struct MHD_Connection *connection, @@ -88,11 +184,11 @@ std::string get_config(json & request, const char * x) MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { - // Make a copy of body so we own the data even if request_completed fires - std::string body_copy(body); + // Body is kept alive by *con_cls until request_completed fires, so it's safe to use directly + // All operations here are synchronous and complete before returning to caller try { - json request = json::parse(body_copy); + json request = json::parse(body); std::string commitmentPolicy = get_config(request, "commitmentPolicy"); std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); @@ -144,8 +240,8 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - // Create CryptoConfigurationV2 and S3EncryptionClientV2 based on key type - std::shared_ptr encryption_client; + // Create CryptoConfigurationV2 based on key type + std::shared_ptr config; if (!aes_key_blob.empty()) { // Base64 decode the AES key @@ -164,38 +260,26 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( key_buffer ); - CryptoConfigurationV2 config(materials); - - if (legacy1 || legacy2) - config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - // Configure ClientConfiguration with retry strategy for throttling - Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = 25; - clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); - - encryption_client = std::make_shared(config, clientConfig); + config = std::make_shared(materials); } else if (!kms_key_id.empty()) { auto materials = std::make_shared(kms_key_id); - CryptoConfigurationV2 config(materials); - - if (legacy1 || legacy2) - config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - // Configure ClientConfiguration with retry strategy for throttling - Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = 25; - clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); - - encryption_client = std::make_shared(config, clientConfig); + config = std::make_shared(materials); } else { return send_response(connection, 400, "{\"error\":\"No valid key material provided\"}"); } + + // Apply common configuration settings (applies to both AES and KMS) + if (legacy1 || legacy2) + config->SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (inst_put) + config->SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + // Create S3EncryptionClientV2 with standard configuration + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 512; // Large pool per client + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + auto encryption_client = std::make_shared(*config, clientConfig); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); @@ -203,6 +287,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2-TRANSITION] handle_create_client exception: %s\n", e.what()); return send_response(connection, 500, "{\"error\":\"An exception was thrown.\"}"); } catch (...) { @@ -213,13 +298,18 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, void fill_context(Aws::Map &map, const std::string &metadata) { if (metadata.empty()) { + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] fill_context: metadata is empty\n"); return; } + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] fill_context: raw metadata='%s' (length=%zu)\n", + metadata.c_str(), metadata.length()); + // Parse metadata format: [key1]:[value1],[key2]:[value2],... // or single pair: [key]:[value] std::string current = metadata; size_t pos = 0; + int pair_count = 0; while (pos < current.length()) { // Find opening bracket for key @@ -252,6 +342,9 @@ void fill_context(Aws::Map &map, std::string value = current.substr(value_start + 1, value_end - value_start - 1); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] fill_context: parsed pair #%d: key='%s', value='%s'\n", + ++pair_count, key.c_str(), value.c_str()); + // Add to map map.emplace(key, value); @@ -262,14 +355,24 @@ void fill_context(Aws::Map &map, pos = comma + 1; } } + + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] fill_context: completed, parsed %d pairs into map\n", pair_count); } 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) { + std::string bucket, + std::string key, + std::string client_id, + std::string metadata) { + // Get thread ID for debugging concurrent operations + std::thread::id thread_id = std::this_thread::get_id(); + + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V2-TRANSITION] GetObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -280,30 +383,91 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); + + // Log the encryption context map size and contents + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject: encryption context map size=%zu\n", kmsContextMap.size()); + for (const auto& pair : kmsContextMap) { + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject: context['%s']='%s'\n", + pair.first.c_str(), pair.second.c_str()); + } + + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject: calling client->GetObject() for key=%s\n", key.c_str()); + + // Keep outcome alive to ensure stream remains valid auto outcome = client->GetObject(request, kmsContextMap); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject: client->GetObject() returned for key=%s\n", key.c_str()); + if (outcome.IsSuccess()) { + // Read the stream completely before outcome goes out of scope auto &stream = outcome.GetResult().GetBody(); - std::string content((std::istreambuf_iterator(stream)), - std::istreambuf_iterator()); - return send_response(connection, 200, content); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string content = buffer.str(); + + // Validate we read something + if (content.empty() && stream.fail()) { + fprintf(stderr, "[CPP-V2-TRANSITION] GetObject error: Failed to read stream for bucket=%s, key=%s\n", + bucket.c_str(), key.c_str()); + auto msg = make_error("Failed to read response stream", 500); + return send_response(connection, 500, msg); + } + + fprintf(stderr, "[CPP-V2-TRANSITION] GetObject success: bucket=%s, key=%s, size=%zu bytes\n", + bucket.c_str(), key.c_str(), content.length()); + + // Create and send response + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); + + // Add keep-alive header + MHD_add_response_header(response, "Connection", "keep-alive"); + MHD_add_response_header(response, "Keep-Alive", "timeout=30, max=100"); + + MHD_Result ret = MHD_queue_response(connection, 200, response); + MHD_destroy_response(response); + + return ret; } else { + // Enhanced error logging with thread info + auto error = outcome.GetError(); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject FAILED: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject error details:\n"); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] - Message: %s\n", error.GetMessage().c_str()); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] - ExceptionName: %s\n", error.GetExceptionName().c_str()); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] - ResponseCode: %d\n", (int)error.GetResponseCode()); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] - ShouldRetry: %s\n", error.ShouldRetry() ? "true" : "false"); + auto msg = make_error(outcome.GetError().GetMessage(), 500); + fprintf(stderr, "[CPP-V2-TRANSITION] GetObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { - auto msg = make_error("An exception was thrown", 500); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject EXCEPTION: thread=%lu, key=%s, what=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str(), e.what()); + auto msg = make_error(e.what(), 500); + return send_response(connection, 500, msg); + } catch (...) { + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject UNKNOWN EXCEPTION: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); + auto msg = make_error("Unknown error in GetObject", 500); return send_response(connection, 500, msg); } } MHD_Result handle_put_object(struct MHD_Connection *connection, - const std::string &bucket, const std::string &key, - const std::string &client_id, - const std::string &body, - const std::string &metadata) { + std::string bucket, + std::string key, + std::string client_id, + std::string body, + std::string metadata) { + fprintf(stderr, "[CPP-V2-TRANSITION] PutObject request: bucket=%s, key=%s, client_id=%s, body_size=%zu\n", + bucket.c_str(), key.c_str(), client_id.c_str(), body.length()); + auto client = get_client(client_id); if (!client) { + fprintf(stderr, "[CPP-V2-TRANSITION] PutObject error: Client not found for client_id=%s\n", client_id.c_str()); return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } @@ -326,13 +490,16 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, // body_ptr keeps the data alive through this entire operation auto outcome = client->PutObject(request, kmsContextMap); if (outcome.IsSuccess()) { + fprintf(stderr, "[CPP-V2-TRANSITION] PutObject success: bucket=%s, key=%s\n", bucket.c_str(), key.c_str()); json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); } else { auto msg = make_error(outcome.GetError().GetMessage(), 500); + fprintf(stderr, "[CPP-V2-TRANSITION] PutObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2-TRANSITION] PutObject exception: %s\n", e.what()); auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } @@ -341,9 +508,36 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { // Clean up the request-specific context when request is truly complete + // This is called AFTER all handlers have returned and the response has been sent + + // Log why the request was terminated + const char* reason = "UNKNOWN"; + switch (toe) { + case MHD_REQUEST_TERMINATED_COMPLETED_OK: + reason = "COMPLETED_OK"; + break; + case MHD_REQUEST_TERMINATED_WITH_ERROR: + reason = "WITH_ERROR"; + break; + case MHD_REQUEST_TERMINATED_TIMEOUT_REACHED: + reason = "TIMEOUT_REACHED"; + break; + case MHD_REQUEST_TERMINATED_DAEMON_SHUTDOWN: + reason = "DAEMON_SHUTDOWN"; + break; + case MHD_REQUEST_TERMINATED_READ_ERROR: + reason = "READ_ERROR"; + break; + case MHD_REQUEST_TERMINATED_CLIENT_ABORT: + reason = "CLIENT_ABORT"; + break; + } + fprintf(stderr, "[CPP-V2-TRANSITION] request_completed called, reason=%s, con_cls=%p\n", + reason, *con_cls); + if (*con_cls != nullptr) { std::string *body = static_cast(*con_cls); - delete body; + delete body; // Safe to delete now - all synchronous operations are complete *con_cls = nullptr; } } @@ -352,34 +546,58 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { - std::string method_str(method); - bool is_push = method_str == "POST" || method_str == "PUT"; + try { + std::string method_str(method); + std::string url_str(url); + bool is_push = method_str == "POST" || method_str == "PUT"; + + // LOG: Every request entry (even first-time calls) + if (*con_cls == nullptr) { + fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST START: method=%s, url=%s, version=%s, con_cls=NULL, upload_data_size=%zu\n", + method, url, version, *upload_data_size); + } // Initialize request context on first call if (*con_cls == nullptr) { // Allocate unique state for each request to avoid race conditions *con_cls = new std::string(); + fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST INIT: allocated new request context for %s %s\n", method, url); return MHD_YES; } + // LOG: Subsequent calls + if (is_push && *upload_data_size > 0) { + fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST DATA: %s %s receiving %zu bytes\n", method, url, *upload_data_size); + } else if (*upload_data_size == 0) { + fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST COMPLETE: %s %s ready for processing\n", method, url); + } + // Accumulate request body data for POST/PUT requests - if (is_push && *upload_data_size) { + if (is_push && *upload_data_size > 0) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); *upload_data_size = 0; return MHD_YES; } - - std::string url_str(url); + + // At this point, *upload_data_size == 0, meaning we have all the data + // Now we can safely process the request + + // LOG: About to process request + fprintf(stderr, "[CPP-V2-TRANSITION] PROCESSING: %s %s\n", method, url); // Handle client creation endpoint if (is_push && url_str == "/client") { + fprintf(stderr, "[CPP-V2-TRANSITION] Handling /client endpoint\n"); std::string *body = static_cast(*con_cls); - return handle_create_client(connection, *body); + MHD_Result result = handle_create_client(connection, *body); + fprintf(stderr, "[CPP-V2-TRANSITION] /client handler returned: %d\n", result); + return result; } // Handle object operations if (url_str.find("/object/") == 0) { + fprintf(stderr, "[CPP-V2-TRANSITION] Handling /object/ endpoint\n"); std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); if (slash_pos != std::string::npos) { @@ -388,37 +606,124 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string client_id = get_header_value(connection, "clientid"); std::string metadata = get_header_value(connection, "content-metadata"); + fprintf(stderr, "[CPP-V2-TRANSITION] Object operation: bucket=%s, key=%s, client_id=%s, method=%s\n", + bucket.c_str(), key.c_str(), client_id.c_str(), method); + if (method_str == "GET") { - return handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V2-TRANSITION] Dispatching to handle_get_object\n"); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V2-TRANSITION] handle_get_object returned: %d\n", result); + return result; } else if (method_str == "PUT") { + fprintf(stderr, "[CPP-V2-TRANSITION] Dispatching to handle_put_object\n"); std::string *body = static_cast(*con_cls); - return handle_put_object(connection, bucket, key, client_id, *body, metadata); + MHD_Result result = handle_put_object(connection, bucket, key, client_id, *body, metadata); + fprintf(stderr, "[CPP-V2-TRANSITION] handle_put_object returned: %d\n", result); + return result; } else { + fprintf(stderr, "[CPP-V2-TRANSITION] Method not allowed: %s\n", method); return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } } } - // Return error for unrecognized endpoints - return send_response(connection, 404, - "{\"error\":\"Not idea what is happening\"}"); + // Return error for unrecognized endpoints + fprintf(stderr, "[CPP-V2-TRANSITION] ERROR: Unrecognized endpoint: %s %s\n", method, url); + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); + } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V2-TRANSITION] FATAL: Unhandled exception in request_handler: %s (method=%s, url=%s)\n", + e.what(), method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unhandled exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V2-TRANSITION] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } catch (...) { + fprintf(stderr, "[CPP-V2-TRANSITION] FATAL: Unknown exception in request_handler (method=%s, url=%s)\n", + method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unknown exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V2-TRANSITION] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } +} + +// Error log callback for libmicrohttpd +void log_mhd_error(void* cls, const char* fmt, va_list ap) { + fprintf(stderr, "[CPP-V2-TRANSITION] [MHD-ERROR] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); +} + +// Connection notification callback - called when a client connects +MHD_Result notify_connection(void *cls, + struct MHD_Connection *connection, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) { + if (toe == MHD_CONNECTION_NOTIFY_STARTED) { + fprintf(stderr, "[CPP-V2-TRANSITION] [MHD-CONNECT] New connection started\n"); + } else if (toe == MHD_CONNECTION_NOTIFY_CLOSED) { + fprintf(stderr, "[CPP-V2-TRANSITION] [MHD-DISCONNECT] Connection closed\n"); + } + return MHD_YES; } int main() { Aws::SDKOptions options; + + // Configure AWS SDK logging to output to stderr (which goes to server.log) + // Using Debug level to capture all SDK activity including CryptoModule errors + options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug; + options.loggingOptions.logger_create_fn = []() { + return std::make_shared( + Aws::Utils::Logging::LogLevel::Debug + ); + }; + + fprintf(stderr, "[CONFIG] AWS SDK logging enabled at Debug level\n"); + Aws::InitAPI(options); + + // Detect CPU core count and configure threading + unsigned int num_cores = std::thread::hardware_concurrency(); + if (num_cores == 0) { + num_cores = 4; + fprintf(stderr, "[CPP-V2-TRANSITION] [WARNING] CPU core detection failed, defaulting to %u cores\n", num_cores); + } + + g_thread_pool_size = num_cores * 2; + unsigned int connection_limit = g_thread_pool_size; + + // Log configuration + fprintf(stderr, "[CONFIG] Detected CPU cores: %u\n", num_cores); + fprintf(stderr, "[CONFIG] Thread pool size: %u\n", g_thread_pool_size); + fprintf(stderr, "[CONFIG] Connection limit: %u\n", connection_limit); + fprintf(stderr, "[CONFIG] Each S3 client will use 512 max connections\n"); + int port = 8097; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, + MHD_start_daemon(MHD_USE_POLL_INTERNALLY | MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_ERROR_LOG, + port, NULL, NULL, &request_handler, NULL, + MHD_OPTION_EXTERNAL_LOGGER, log_mhd_error, NULL, + MHD_OPTION_NOTIFY_CONNECTION, notify_connection, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - MHD_OPTION_CONNECTION_LIMIT, 100, - MHD_OPTION_CONNECTION_TIMEOUT, 30, + MHD_OPTION_THREAD_POOL_SIZE, g_thread_pool_size, + MHD_OPTION_CONNECTION_LIMIT, connection_limit, + MHD_OPTION_CONNECTION_TIMEOUT, 10, MHD_OPTION_END); if (!daemon) { - fprintf(stderr, "Failed to start server on port %d\n", port); + fprintf(stderr, "[CPP-V2-TRANSITION] Failed to start server on port %d\n", port); Aws::ShutdownAPI(options); return 1; } diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 4663b4ec..a407d1e4 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -4,11 +4,12 @@ * CONCURRENCY AND SYNCHRONIZATION DESIGN: * * 1. Threading Model: - * - Uses MHD_USE_SELECT_INTERNALLY with fixed thread pool + * - Uses MHD_USE_POLL_INTERNALLY with fixed thread pool * - Thread pool size = CPU cores * 2 (auto-detected at startup) * - Threads are reused across connections for efficiency - * - I/O multiplexing (select/poll) distributes connections across thread pool + * - I/O multiplexing (poll) distributes connections across thread pool * - All S3 operations are SYNCHRONOUS - server waits for S3 completion before responding + * - POLL mechanism avoids FD_SETSIZE=1024 limitation of select() * * 2. Resource Scaling: * - All limits automatically scale with detected CPU count: @@ -40,12 +41,16 @@ */ #include +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -58,11 +63,23 @@ #include #include #include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; -std::unordered_map> client_cache_secret; + +// LRU cache for S3 encryption clients +// Limits memory and connection pool growth by evicting least recently used clients +const size_t MAX_CACHED_CLIENTS = 100; // Reasonable limit for concurrent test operations + +struct ClientCacheEntry { + std::shared_ptr client; + std::list::iterator lru_iter; +}; + +std::unordered_map client_cache_secret; +std::list lru_order; // Most recently used at front std::shared_timed_mutex client_mutex; // Using shared_timed_mutex (C++14 compatible) for concurrent reads // Threading configuration - set at startup based on CPU cores @@ -78,13 +95,18 @@ std::string generate_uuid() { std::shared_ptr get_client(const std::string &client_id) { - // Use shared_lock for concurrent reads - multiple threads can read simultaneously - std::shared_lock lock(client_mutex); + // Need unique_lock to update LRU order even on reads + std::unique_lock 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; + // Move to front of LRU list (mark as most recently used) + lru_order.erase(it->second.lru_iter); + lru_order.push_front(client_id); + it->second.lru_iter = lru_order.begin(); + + return it->second.client; } } @@ -93,7 +115,31 @@ void set_client(const std::string &client_id, std::shared_ptr lock(client_mutex); - client_cache_secret.emplace(client_id, client); + + // Add to front of LRU list (most recently used) + lru_order.push_front(client_id); + + ClientCacheEntry entry; + entry.client = client; + entry.lru_iter = lru_order.begin(); + + client_cache_secret.emplace(client_id, entry); + + // Evict least recently used clients if we exceed the limit + while (client_cache_secret.size() > MAX_CACHED_CLIENTS) { + std::string lru_client_id = lru_order.back(); + lru_order.pop_back(); + + auto evict_it = client_cache_secret.find(lru_client_id); + if (evict_it != client_cache_secret.end()) { + fprintf(stderr, "[CPP-V3] [CACHE-EVICT] Evicting client %s (cache size was %zu)\n", + lru_client_id.c_str(), client_cache_secret.size()); + client_cache_secret.erase(evict_it); + } + } + + fprintf(stderr, "[CPP-V3] [CACHE-ADD] Added client %s (cache size now %zu)\n", + client_id.c_str(), client_cache_secret.size()); } std::string get_header_value(struct MHD_Connection *connection, @@ -188,8 +234,8 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string commitmentPolicy = get_config(request, "commitmentPolicy"); std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); - // Create CryptoConfigurationV3 and S3EncryptionClientV3 based on key type - std::shared_ptr encryption_client; + // Create CryptoConfigurationV3 based on key type + CryptoConfigurationV3 config; if (!aes_key_blob.empty()) { // Base64 decode the AES key @@ -208,64 +254,45 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( key_buffer ); - CryptoConfigurationV3 config(materials); - - if (legacy1 || legacy2) - config.AllowLegacy(); - if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - if (encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); - } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); - } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); - } - - // Configure ClientConfiguration with retry strategy for throttling - // Match S3 connection pool size to thread pool size for optimal resource usage - Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = g_thread_pool_size; - clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); - - encryption_client = std::make_shared(config, clientConfig); + config = CryptoConfigurationV3(materials); } else if (!kms_key_id.empty()) { auto materials = std::make_shared(kms_key_id); - CryptoConfigurationV3 config(materials); - - if (legacy1 || legacy2) - config.AllowLegacy(); - if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - if (encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); - } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); - } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { - if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return unsupported(connection, commitmentPolicy, encryptionAlgorithm); - config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); - } - - // Configure ClientConfiguration with retry strategy for throttling - // Match S3 connection pool size to thread pool size for optimal resource usage - Aws::Client::ClientConfiguration clientConfig; - clientConfig.maxConnections = g_thread_pool_size; - clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); - - encryption_client = std::make_shared(config, clientConfig); + config = CryptoConfigurationV3(materials); } else { return send_response(connection, 400, "{\"error\":\"No valid key material provided\"}"); } + + // Apply common configuration settings (applies to both AES and KMS) + if (legacy1 || legacy2) + config.AllowLegacy(); + if (inst_put) + config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + // Configure commitment policy (applies to both AES and KMS) + if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF" || + encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") { + return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + } + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); + } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") { + return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + } + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); + } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { + if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") { + return unsupported(connection, commitmentPolicy, encryptionAlgorithm); + } + config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + } + + // Create S3EncryptionClientV3 with standard configuration + Aws::Client::ClientConfiguration clientConfig; + clientConfig.maxConnections = 512; // Large pool per client + clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + auto encryption_client = std::make_shared(config, clientConfig); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); @@ -284,13 +311,18 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, void fill_context(Aws::Map &map, const std::string &metadata) { if (metadata.empty()) { + fprintf(stderr, "[CPP-V3] [DEBUG] fill_context: metadata is empty\n"); return; } + fprintf(stderr, "[CPP-V3] [DEBUG] fill_context: raw metadata='%s' (length=%zu)\n", + metadata.c_str(), metadata.length()); + // Parse metadata format: [key1]:[value1],[key2]:[value2],... // or single pair: [key]:[value] std::string current = metadata; size_t pos = 0; + int pair_count = 0; while (pos < current.length()) { // Find opening bracket for key @@ -323,6 +355,9 @@ void fill_context(Aws::Map &map, std::string value = current.substr(value_start + 1, value_end - value_start - 1); + fprintf(stderr, "[CPP-V3] [DEBUG] fill_context: parsed pair #%d: key='%s', value='%s'\n", + ++pair_count, key.c_str(), value.c_str()); + // Add to map map.emplace(key, value); @@ -333,6 +368,8 @@ void fill_context(Aws::Map &map, pos = comma + 1; } } + + fprintf(stderr, "[CPP-V3] [DEBUG] fill_context: completed, parsed %d pairs into map\n", pair_count); } MHD_Result handle_get_object(struct MHD_Connection *connection, @@ -340,8 +377,11 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, std::string key, std::string client_id, std::string metadata) { - fprintf(stderr, "[CPP-V3] GetObject request: bucket=%s, key=%s, client_id=%s\n", - bucket.c_str(), key.c_str(), client_id.c_str()); + // Get thread ID for debugging concurrent operations + std::thread::id thread_id = std::this_thread::get_id(); + + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); auto client = get_client(client_id); if (!client) { @@ -357,9 +397,20 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); + // Log the encryption context map size and contents + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject: encryption context map size=%zu\n", kmsContextMap.size()); + for (const auto& pair : kmsContextMap) { + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject: context['%s']='%s'\n", + pair.first.c_str(), pair.second.c_str()); + } + + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject: calling client->GetObject() for key=%s\n", key.c_str()); + // Keep outcome alive to ensure stream remains valid auto outcome = client->GetObject(request, kmsContextMap); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject: client->GetObject() returned for key=%s\n", key.c_str()); + if (outcome.IsSuccess()) { // Read the stream completely before outcome goes out of scope auto &stream = outcome.GetResult().GetBody(); @@ -391,16 +442,28 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, return ret; } else { + // Enhanced error logging with thread info + auto error = outcome.GetError(); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject FAILED: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject error details:\n"); + fprintf(stderr, "[CPP-V3] [DEBUG] - Message: %s\n", error.GetMessage().c_str()); + fprintf(stderr, "[CPP-V3] [DEBUG] - ExceptionName: %s\n", error.GetExceptionName().c_str()); + fprintf(stderr, "[CPP-V3] [DEBUG] - ResponseCode: %d\n", (int)error.GetResponseCode()); + fprintf(stderr, "[CPP-V3] [DEBUG] - ShouldRetry: %s\n", error.ShouldRetry() ? "true" : "false"); + auto msg = make_error(outcome.GetError().GetMessage(), 500); fprintf(stderr, "[CPP-V3] GetObject AWS error: %s\n", msg.c_str()); return send_response(connection, 500, msg); } } catch (const std::exception &e) { - fprintf(stderr, "[CPP-V3] GetObject exception: %s\n", e.what()); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject EXCEPTION: thread=%lu, key=%s, what=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str(), e.what()); auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } catch (...) { - fprintf(stderr, "[CPP-V3] GetObject unknown exception\n"); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject UNKNOWN EXCEPTION: thread=%lu, key=%s\n", + (unsigned long)std::hash{}(thread_id), key.c_str()); auto msg = make_error("Unknown error in GetObject", 500); return send_response(connection, 500, msg); } @@ -459,6 +522,32 @@ void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { // Clean up the request-specific context when request is truly complete // This is called AFTER all handlers have returned and the response has been sent + + // Log why the request was terminated + const char* reason = "UNKNOWN"; + switch (toe) { + case MHD_REQUEST_TERMINATED_COMPLETED_OK: + reason = "COMPLETED_OK"; + break; + case MHD_REQUEST_TERMINATED_WITH_ERROR: + reason = "WITH_ERROR"; + break; + case MHD_REQUEST_TERMINATED_TIMEOUT_REACHED: + reason = "TIMEOUT_REACHED"; + break; + case MHD_REQUEST_TERMINATED_DAEMON_SHUTDOWN: + reason = "DAEMON_SHUTDOWN"; + break; + case MHD_REQUEST_TERMINATED_READ_ERROR: + reason = "READ_ERROR"; + break; + case MHD_REQUEST_TERMINATED_CLIENT_ABORT: + reason = "CLIENT_ABORT"; + break; + } + fprintf(stderr, "[CPP-V3] request_completed called, reason=%s, con_cls=%p\n", + reason, *con_cls); + if (*con_cls != nullptr) { std::string *body = static_cast(*con_cls); delete body; // Safe to delete now - all synchronous operations are complete @@ -470,16 +559,32 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { - std::string method_str(method); - bool is_push = method_str == "POST" || method_str == "PUT"; + try { + std::string method_str(method); + std::string url_str(url); + bool is_push = method_str == "POST" || method_str == "PUT"; + + // LOG: Every request entry (even first-time calls) + if (*con_cls == nullptr) { + fprintf(stderr, "[CPP-V3] REQUEST START: method=%s, url=%s, version=%s, con_cls=NULL, upload_data_size=%zu\n", + method, url, version, *upload_data_size); + } // Initialize request context on first call if (*con_cls == nullptr) { // Allocate unique state for each request to avoid race conditions *con_cls = new std::string(); + fprintf(stderr, "[CPP-V3] REQUEST INIT: allocated new request context for %s %s\n", method, url); return MHD_YES; } + // LOG: Subsequent calls + if (is_push && *upload_data_size > 0) { + fprintf(stderr, "[CPP-V3] REQUEST DATA: %s %s receiving %zu bytes\n", method, url, *upload_data_size); + } else if (*upload_data_size == 0) { + fprintf(stderr, "[CPP-V3] REQUEST COMPLETE: %s %s ready for processing\n", method, url); + } + // Accumulate request body data for POST/PUT requests if (is_push && *upload_data_size > 0) { std::string *body = static_cast(*con_cls); @@ -490,18 +595,22 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, // At this point, *upload_data_size == 0, meaning we have all the data // Now we can safely process the request - - std::string url_str(url); + + // LOG: About to process request + fprintf(stderr, "[CPP-V3] PROCESSING: %s %s\n", method, url); // Handle client creation endpoint if (is_push && url_str == "/client") { + fprintf(stderr, "[CPP-V3] Handling /client endpoint\n"); std::string *body = static_cast(*con_cls); MHD_Result result = handle_create_client(connection, *body); + fprintf(stderr, "[CPP-V3] /client handler returned: %d\n", result); return result; } // Handle object operations if (url_str.find("/object/") == 0) { + fprintf(stderr, "[CPP-V3] Handling /object/ endpoint\n"); std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); if (slash_pos != std::string::npos) { @@ -510,26 +619,90 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string client_id = get_header_value(connection, "clientid"); std::string metadata = get_header_value(connection, "content-metadata"); + fprintf(stderr, "[CPP-V3] Object operation: bucket=%s, key=%s, client_id=%s, method=%s\n", + bucket.c_str(), key.c_str(), client_id.c_str(), method); + if (method_str == "GET") { + fprintf(stderr, "[CPP-V3] Dispatching to handle_get_object\n"); MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V3] handle_get_object returned: %d\n", result); return result; } else if (method_str == "PUT") { + fprintf(stderr, "[CPP-V3] Dispatching to handle_put_object\n"); std::string *body = static_cast(*con_cls); MHD_Result result = handle_put_object(connection, bucket, key, client_id, *body, metadata); + fprintf(stderr, "[CPP-V3] handle_put_object returned: %d\n", result); return result; } else { + fprintf(stderr, "[CPP-V3] Method not allowed: %s\n", method); return send_response(connection, 405, "{\"error\":\"Method not allowed\"}"); } } } - // Return error for unrecognized endpoints - return send_response(connection, 404, - "{\"error\":\"Not idea what is happening\"}"); + // Return error for unrecognized endpoints + fprintf(stderr, "[CPP-V3] ERROR: Unrecognized endpoint: %s %s\n", method, url); + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); + } catch (const std::exception &e) { + fprintf(stderr, "[CPP-V3] FATAL: Unhandled exception in request_handler: %s (method=%s, url=%s)\n", + e.what(), method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unhandled exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V3] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } catch (...) { + fprintf(stderr, "[CPP-V3] FATAL: Unknown exception in request_handler (method=%s, url=%s)\n", + method, url); + // Try to send error response, but connection might already be broken + try { + return send_response(connection, 500, + "{\"error\":\"Internal server error: unknown exception\"}"); + } catch (...) { + fprintf(stderr, "[CPP-V3] FATAL: Failed to send error response\n"); + return MHD_NO; + } + } +} + +// Error log callback for libmicrohttpd +void log_mhd_error(void* cls, const char* fmt, va_list ap) { + fprintf(stderr, "[CPP-V3] [MHD-ERROR] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); +} + +// Connection notification callback - called when a client connects +MHD_Result notify_connection(void *cls, + struct MHD_Connection *connection, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) { + if (toe == MHD_CONNECTION_NOTIFY_STARTED) { + fprintf(stderr, "[CPP-V3] [MHD-CONNECT] New connection started\n"); + } else if (toe == MHD_CONNECTION_NOTIFY_CLOSED) { + fprintf(stderr, "[CPP-V3] [MHD-DISCONNECT] Connection closed\n"); + } + return MHD_YES; } int main() { Aws::SDKOptions options; + + // Configure AWS SDK logging to output to stderr (which goes to server.log) + // Using Debug level to capture all SDK activity including CryptoModule errors + options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug; + options.loggingOptions.logger_create_fn = []() { + return std::make_shared( + Aws::Utils::Logging::LogLevel::Debug + ); + }; + + fprintf(stderr, "[CONFIG] AWS SDK logging enabled at Debug level\n"); + Aws::InitAPI(options); // Detect CPU core count and configure threading @@ -547,17 +720,20 @@ int main() { fprintf(stderr, "[CONFIG] Detected CPU cores: %u\n", num_cores); fprintf(stderr, "[CONFIG] Thread pool size: %u\n", g_thread_pool_size); fprintf(stderr, "[CONFIG] Connection limit: %u\n", connection_limit); - fprintf(stderr, "[CONFIG] S3 client maxConnections: %u\n", g_thread_pool_size); + fprintf(stderr, "[CONFIG] Each S3 client will use 512 max connections\n"); int port = 8091; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_SELECT_INTERNALLY | MHD_USE_INTERNAL_POLLING_THREAD, port, NULL, NULL, + MHD_start_daemon(MHD_USE_POLL_INTERNALLY | MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_ERROR_LOG, + port, NULL, NULL, &request_handler, NULL, + MHD_OPTION_EXTERNAL_LOGGER, log_mhd_error, NULL, + MHD_OPTION_NOTIFY_CONNECTION, notify_connection, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, MHD_OPTION_THREAD_POOL_SIZE, g_thread_pool_size, MHD_OPTION_CONNECTION_LIMIT, connection_limit, - MHD_OPTION_CONNECTION_TIMEOUT, 30, + MHD_OPTION_CONNECTION_TIMEOUT, 10, MHD_OPTION_END); if (!daemon) { diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index d30888a3..1c0d4308 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -61,7 +61,7 @@ tasks { 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() - 9).toString()) // Scale with CPU, reserve 2 cores + Math.max(1, Runtime.getRuntime().availableProcessors() - 6).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")) diff --git a/test-server/php-v2-server/Makefile b/test-server/php-v2-server/Makefile index a9d04134..719ea238 100644 --- a/test-server/php-v2-server/Makefile +++ b/test-server/php-v2-server/Makefile @@ -15,7 +15,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) + composer run start --timeout=0 > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V2 server starting..." stop-server: diff --git a/test-server/php-v2-transition-server/Makefile b/test-server/php-v2-transition-server/Makefile index 61eb3a84..a3d038de 100644 --- a/test-server/php-v2-transition-server/Makefile +++ b/test-server/php-v2-transition-server/Makefile @@ -15,7 +15,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) + composer run start --timeout=0 > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V2 Transition server starting..." stop-server: diff --git a/test-server/php-v3-server/Makefile b/test-server/php-v3-server/Makefile index 2b9661f2..9460d4ed 100644 --- a/test-server/php-v3-server/Makefile +++ b/test-server/php-v3-server/Makefile @@ -15,7 +15,7 @@ start-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) + composer run start --timeout=0 > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V3 server starting..." stop-server: From d0936710ac0d2258e512bcba4b9fbc3a324ccc5d Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Fri, 21 Nov 2025 21:39:35 -0800 Subject: [PATCH 29/46] update the test --- test-server/java-tests/build.gradle.kts | 2 +- .../amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 1c0d4308..14c3eec1 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -61,7 +61,7 @@ tasks { 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() - 6).toString()) // Scale with CPU, reserve 2 cores + 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")) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java index 100925a9..9565639b 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java @@ -180,7 +180,7 @@ public void GIVEN_KCGCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncry ) { S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); - final String objectKey = "encrypt-kc-gcm-decrypt-improved-test-key-" + encLang; + final String objectKey = "encrypt-kc-gcm-decrypt-improved-test-key-" + encLang + "-" + decLang; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(TestUtils.KMS_KEY_ARN) From 2e315c707205f799b59c7c12658511eb31c566c5 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Sat, 22 Nov 2025 16:44:01 -0800 Subject: [PATCH 30/46] fix c build issue --- test-server/cpp-v3-server/main.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index a407d1e4..fc0d0452 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -64,6 +64,7 @@ #include #include #include +#include using json = nlohmann::json; using namespace Aws::S3Encryption; @@ -235,7 +236,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); // Create CryptoConfigurationV3 based on key type - CryptoConfigurationV3 config; + std::optional config; if (!aes_key_blob.empty()) { // Base64 decode the AES key @@ -254,10 +255,10 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( key_buffer ); - config = CryptoConfigurationV3(materials); + config.emplace(materials); } else if (!kms_key_id.empty()) { auto materials = std::make_shared(kms_key_id); - config = CryptoConfigurationV3(materials); + config.emplace(materials); } else { return send_response(connection, 400, "{\"error\":\"No valid key material provided\"}"); @@ -265,9 +266,9 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, // Apply common configuration settings (applies to both AES and KMS) if (legacy1 || legacy2) - config.AllowLegacy(); + config->AllowLegacy(); if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + config->SetStorageMethod(StorageMethod::INSTRUCTION_FILE); // Configure commitment policy (applies to both AES and KMS) if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") { @@ -275,24 +276,24 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") { return unsupported(connection, commitmentPolicy, encryptionAlgorithm); } - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); + config->SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") { if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") { return unsupported(connection, commitmentPolicy, encryptionAlgorithm); } - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); + config->SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") { if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") { return unsupported(connection, commitmentPolicy, encryptionAlgorithm); } - config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + config->SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); } // Create S3EncryptionClientV3 with standard configuration Aws::Client::ClientConfiguration clientConfig; clientConfig.maxConnections = 512; // Large pool per client clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); - auto encryption_client = std::make_shared(config, clientConfig); + auto encryption_client = std::make_shared(*config, clientConfig); std::string client_id = generate_uuid(); set_client(client_id, encryption_client); From 76baf0614b62723925c855e633a77962606a8235 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 24 Nov 2025 11:29:16 -0800 Subject: [PATCH 31/46] trying to add RangedGets update ranged get tests update cpp --- test-server/cpp-v2-server/main.cpp | 18 +- .../cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v2-transition-server/main.cpp | 18 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/main.cpp | 18 +- .../amazon/encryption/s3/RangedGetTests.java | 1123 +++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 108 ++ .../encryption/s3/GetObjectOperationImpl.java | 14 +- .../encryption/s3/GetObjectOperationImpl.java | 16 +- .../encryption/s3/GetObjectOperationImpl.java | 14 +- test-server/model/object.smithy | 4 + 11 files changed, 1310 insertions(+), 27 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index f5c4e406..ae0a0e8a 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -184,6 +184,8 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, CryptoConfigurationV2 config(materials); if (legacy1 || legacy2) config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (legacy2) + config.SetUnAuthenticatedRangeGet(RangeGetMode::ALL); if (inst_put) config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); @@ -276,12 +278,13 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, std::string bucket, std::string key, std::string client_id, - std::string metadata) { + std::string metadata, + std::string range) { // Get thread ID for debugging concurrent operations std::thread::id thread_id = std::this_thread::get_id(); - fprintf(stderr, "[CPP-V2] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", - (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu, range=%s\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length(), range.c_str()); auto client = get_client(client_id); if (!client) { @@ -293,6 +296,12 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; request.SetBucket(bucket); request.SetKey(key); + + // Add range header if provided + if (!range.empty()) { + request.SetRange(range); + fprintf(stderr, "[CPP-V2] [DEBUG] GetObject: Setting range=%s\n", range.c_str()); + } Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -524,7 +533,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, if (method_str == "GET") { fprintf(stderr, "[CPP-V2] Dispatching to handle_get_object\n"); - MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + std::string range = get_header_value(connection, "Range"); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata, range); fprintf(stderr, "[CPP-V2] handle_get_object returned: %d\n", result); return result; } else if (method_str == "PUT") { diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index 87402c99..8d82fa39 160000 --- a/test-server/cpp-v2-transition-server/aws-sdk-cpp +++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 87402c99fd3c9107c6ccc6edf545fd4b05b2b551 +Subproject commit 8d82fa393b08f8564b0aa939fba04df402e3ee47 diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index dc863723..9e9f942d 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -272,6 +272,8 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, // Apply common configuration settings (applies to both AES and KMS) if (legacy1 || legacy2) config->SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (legacy2) + config->SetUnAuthenticatedRangeGet(RangeGetMode::ALL); if (inst_put) config->SetStorageMethod(StorageMethod::INSTRUCTION_FILE); @@ -363,12 +365,13 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, std::string bucket, std::string key, std::string client_id, - std::string metadata) { + std::string metadata, + std::string range) { // Get thread ID for debugging concurrent operations std::thread::id thread_id = std::this_thread::get_id(); - fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", - (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu, range=%s\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length(), range.c_str()); auto client = get_client(client_id); if (!client) { @@ -380,6 +383,12 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; request.SetBucket(bucket); request.SetKey(key); + + // Add range header if provided + if (!range.empty()) { + request.SetRange(range); + fprintf(stderr, "[CPP-V2-TRANSITION] [DEBUG] GetObject: Setting range=%s\n", range.c_str()); + } Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -611,7 +620,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, if (method_str == "GET") { fprintf(stderr, "[CPP-V2-TRANSITION] Dispatching to handle_get_object\n"); - MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + std::string range = get_header_value(connection, "Range"); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata, range); fprintf(stderr, "[CPP-V2-TRANSITION] handle_get_object returned: %d\n", result); return result; } else if (method_str == "PUT") { diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 4039810c..8d82fa39 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 4039810cd5d32429a64e70733175940d4a73f13c +Subproject commit 8d82fa393b08f8564b0aa939fba04df402e3ee47 diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index fc0d0452..609d7e90 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -267,6 +267,8 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, // Apply common configuration settings (applies to both AES and KMS) if (legacy1 || legacy2) config->AllowLegacy(); + if (legacy2) + config->SetUnAuthenticatedRangeGet(RangeGetMode::ALL); if (inst_put) config->SetStorageMethod(StorageMethod::INSTRUCTION_FILE); @@ -377,12 +379,13 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, std::string bucket, std::string key, std::string client_id, - std::string metadata) { + std::string metadata, + std::string range) { // Get thread ID for debugging concurrent operations std::thread::id thread_id = std::this_thread::get_id(); - fprintf(stderr, "[CPP-V3] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu\n", - (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length()); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject START: thread=%lu, bucket=%s, key=%s, client_id=%s, metadata_length=%zu, range=%s\n", + (unsigned long)std::hash{}(thread_id), bucket.c_str(), key.c_str(), client_id.c_str(), metadata.length(), range.c_str()); auto client = get_client(client_id); if (!client) { @@ -394,6 +397,12 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; request.SetBucket(bucket); request.SetKey(key); + + // Add range header if provided + if (!range.empty()) { + request.SetRange(range); + fprintf(stderr, "[CPP-V3] [DEBUG] GetObject: Setting range=%s\n", range.c_str()); + } Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -625,7 +634,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, if (method_str == "GET") { fprintf(stderr, "[CPP-V3] Dispatching to handle_get_object\n"); - MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata); + std::string range = get_header_value(connection, "Range"); + MHD_Result result = handle_get_object(connection, bucket, key, client_id, metadata, range); fprintf(stderr, "[CPP-V3] handle_get_object returned: %d\n", result); return result; } else if (method_str == "PUT") { diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java new file mode 100644 index 00000000..5b5ac6e1 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -0,0 +1,1123 @@ +/* + * 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.assertFalse; +import static software.amazon.encryption.s3.TestUtils.*; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; +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 com.amazonaws.services.s3.model.EncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; + +import software.amazon.encryption.s3.TestUtils.LanguageServerTarget; +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.S3ECConfig; + +/** + * Ranged Get Tests - S3 Encryption Client Cross-Language Compatibility + * + * PURPOSE: + * This test suite validates that ranged get operations (partial object reads) work correctly + * across all three encryption algorithms (CBC, GCM, KC-GCM) and that commitment validation + * occurs properly during ranged gets for KC-GCM encrypted objects. + * + * WHAT IS BEING TESTED: + * 1. Ranged gets successfully retrieve partial content from encrypted objects across all algorithms + * 2. Commitment validation is enforced during ranged gets for KC-GCM encrypted objects + * 3. Corrupted commitment metadata (removed, moved, or mutated) causes ranged gets to fail + * 4. Various byte ranges work correctly: start, end, middle, whole file, and auth tag only + * + * WHY THIS IS IMPORTANT: + * - Ranged gets are a critical S3 feature that must work with encrypted objects + * - KC-GCM's commitment mechanism must be validated even for partial reads to prevent + * commitment-based issues where an actor control the encryption keys + * - Cross-language compatibility ensures all SDKs handle ranged gets consistently + * - Edge cases (first/last bytes, auth tags) verify boundary condition handling + * + * TEST STRUCTURE: + * This suite uses a two-phase approach with enforced ordering: + * 1. EncryptTests - Encrypts objects with CBC, GCM, and KC-GCM algorithms + * - Creates corrupted KC-GCM test cases with manipulated commitment metadata + * - All encrypt tests can run in parallel within this phase + * 2. RangedGetTests - Waits for encryption to complete, then tests ranged gets + * - Tests successful ranged gets on valid objects + * - Tests failed ranged gets on corrupted commitment objects + * - All ranged get tests can run in parallel within this phase + * + * Coordination uses a CountDownLatch to ensure all encryption completes before ranged gets begin. + * + * INPUT DIMENSIONS: + * - Encryption Algorithm: CBC, GCM, KC-GCM + * - Language Implementation: All languages supporting RANGED_GETS_SUPPORTED + * - Byte Range Types: + * * Start (bytes 0-99) + * * End (last 100 bytes) + * * Middle (100 bytes centered in file) + * * Whole file (all bytes) + * * Auth tag only (last 16 bytes for authenticated algorithms) + * - Commitment State (KC-GCM only): + * * Valid (original and good-copy) + * * No commitment - removed from metadata, added to instruction file + * * No commitment - removed from metadata, only in instruction file + * * Mutated commitment - bit flipped in x-amz-c value + * * Mutated commitment - bit flipped in x-amz-d value + * * Mutated commitment - bit flipped in x-amz-i value + * + * EXPECTED RESULTS: + * - Positive: Ranged gets on valid CBC, GCM, KC-GCM objects return correct partial content + * - Negative: Ranged gets on corrupted KC-GCM objects fail with commitment validation errors + * + * REPRESENTATIVE VALUES: + * - Bit flip position: Randomly selected per test run, included in object key name + * - File size: Object keys themselves (short strings) serve as representative small files + * - Byte ranges: Fixed patterns covering important boundary conditions + * + * FILTERING: + * - Only languages in RANGED_GETS_SUPPORTED set are tested + * - CBC and GCM tests validate ranged get functionality works + * - KC-GCM tests focus on commitment validation during ranged gets + */ +public class RangedGetTests { + // Synchronization latch - released when encrypt phase completes + private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + + // Random number generator for bit flipping (seeded for reproducibility) + private static final Random random = new Random(System.currentTimeMillis()); + + /** + * Encryption Tests - Encrypt Phase + * + * These tests encrypt objects using CBC, GCM, and KC-GCM algorithms, then create + * corrupted copies for failure testing. All tests in this class can run in parallel. + */ + @Nested + class EncryptTests { + private static final String sharedObjectKeyBase = "test-ranged-get"; + private static KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + // Thread-safe lists for storing encrypted object keys + private static final List cbcObjects = + Collections.synchronizedList(new ArrayList<>()); + private static final List gcmObjects = + Collections.synchronizedList(new ArrayList<>()); + private static final List kcGcmObjects = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedCObjects = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedDObjects = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedIObjects = + Collections.synchronizedList(new ArrayList<>()); + + private static KeyMaterial RSA_KEY; + private static KeyMaterial AES_KEY; + + @BeforeAll + static void setupKeys() throws Exception { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + RSA_KEY = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(keyPair.getPrivate().getEncoded())) + .build(); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesSecretKey = keyGen.generateKey(); + + AES_KEY = KeyMaterial.builder() + .aesKey(ByteBuffer.wrap(aesSecretKey.getEncoded())) + .build(); + } + + /** + * Public accessors for ranged get tests to retrieve encrypted object keys + */ + static List getCbcObjects() { + return new ArrayList<>(cbcObjects); + } + + static List getGcmObjects() { + return new ArrayList<>(gcmObjects); + } + + static List getKcGcmObjects() { + return new ArrayList<>(kcGcmObjects); + } + + static List getMutatedCObjects() { + return new ArrayList<>(mutatedCObjects); + } + + static List getMutatedDObjects() { + return new ArrayList<>(mutatedDObjects); + } + + static List getMutatedIObjects() { + return new ArrayList<>(mutatedIObjects); + } + + static KeyMaterial getKmsKeyArn() { + return kmsKeyArn; + } + + static KeyMaterial getRsaKey() { + return RSA_KEY; + } + + static KeyMaterial getAesKey() { + return AES_KEY; + } + + // GCM can be encrypted by transition and improved clients + public static Stream transitionAndImprovedForGCM() { + return Stream.concat( + transitionClientsForTest(), + improvedClientsForTest() + ); + } + + // KC-GCM can be encrypted by improved clients only + public static Stream improvedClientsForKCGCM() { + return improvedClientsForTest(); + } + + @org.junit.jupiter.api.Test + void encrypt_cbc_for_ranged_gets() { + // Use old V1 client for CBC encryption (legacy algorithm) + // Only Java V1 client is available - no V1 test servers for other languages + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); + + CryptoConfiguration v1Config = + new CryptoConfiguration(CryptoMode.EncryptionOnly) + .withStorageMode(CryptoStorageMode.ObjectMetadata) + .withAwsKmsRegion(TestUtils.KMS_REGION); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() + .withCryptoConfiguration(v1Config) + .withEncryptionMaterials(materialsProvider) + .build(); + + String objectKey = appendTestSuffix(sharedObjectKeyBase + "-cbc-java"); + v1Client.putObject(TestUtils.BUCKET, objectKey, objectKey); + cbcObjects.add(objectKey); + } + + @ParameterizedTest(name = "{0}: Encrypt GCM for ranged get testing") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#transitionAndImprovedForGCM") + void encrypt_gcm_for_ranged_gets(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 + "-gcm-" + language.getLanguageName()), + gcmObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + + @ParameterizedTest(name = "{0}: Encrypt KC-GCM for ranged get testing") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsForKCGCM") + void encrypt_kc_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-kc-gcm-" + language.getLanguageName()), + kcGcmObjects, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + /** + * Flips a random bit in the given byte array + * @param data The byte array to modify + * @return The bit position that was flipped + */ + static int flipRandomBit(byte[] data) { + if (data.length == 0) { + return -1; + } + int bitPosition = random.nextInt(data.length * 8); + int byteIndex = bitPosition / 8; + int bitIndex = bitPosition % 8; + data[byteIndex] ^= (1 << bitIndex); + return bitPosition; + } + + /** + * Creates corrupted copies of KC-GCM objects for failure testing + */ + static void createCorruptedCopies() throws Exception { + try (S3Client ptS3Client = S3Client.create()) { + for (String objectKey : kcGcmObjects) { + // Get the encrypted object + ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + byte[] objectData = encryptedObject.asByteArray(); + Map objectMetadata = encryptedObject.response().metadata(); + + // Create good copy + putObjectWithMetadata(ptS3Client, objectKey + "-good-copy", objectData, objectMetadata); + + // Extract commitment values from metadata + String commitC = objectMetadata.get("x-amz-c"); + String commitD = objectMetadata.get("x-amz-d"); + String commitI = objectMetadata.get("x-amz-i"); + + // Create copies with no commitment in metadata + Map noCommitMetadata = objectMetadata.entrySet().stream() + .filter(e -> !e.getKey().equals("x-amz-c") && !e.getKey().equals("x-amz-d") && !e.getKey().equals("x-amz-i")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Create instruction file JSON with commitment + ObjectMapper mapper = new ObjectMapper(); + Map instructionFileMap = new java.util.HashMap<>(); + instructionFileMap.put("x-amz-c", commitC); + instructionFileMap.put("x-amz-d", commitD); + instructionFileMap.put("x-amz-i", commitI); + String instructionFileJson = mapper.writeValueAsString(instructionFileMap); + + // No commitment - removed from metadata, added to instruction file + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-no-commitment-add-to-instruction", + objectData, + noCommitMetadata, + instructionFileJson + ); + + // No commitment - removed from metadata, only in instruction file + putObjectWithInstructionFile( + ptS3Client, + objectKey + "-bad-no-commitment-only-instruction", + objectData, + noCommitMetadata, + instructionFileJson + ); + + // Create mutated commitment copies + if (commitC != null) { + byte[] commitCBytes = Base64.getDecoder().decode(commitC); + int bitPos = flipRandomBit(commitCBytes); + String mutatedC = Base64.getEncoder().encodeToString(commitCBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-c", mutatedC); + String mutatedKey = objectKey + "-bad-mutated-c-bit-" + bitPos; + putObjectWithMetadata( + ptS3Client, + mutatedKey, + objectData, + mutatedMetadata + ); + mutatedCObjects.add(mutatedKey); + } + + if (commitD != null) { + byte[] commitDBytes = Base64.getDecoder().decode(commitD); + int bitPos = flipRandomBit(commitDBytes); + String mutatedD = Base64.getEncoder().encodeToString(commitDBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-d", mutatedD); + String mutatedKey = objectKey + "-bad-mutated-d-bit-" + bitPos; + putObjectWithMetadata( + ptS3Client, + mutatedKey, + objectData, + mutatedMetadata + ); + mutatedDObjects.add(mutatedKey); + } + + if (commitI != null) { + byte[] commitIBytes = Base64.getDecoder().decode(commitI); + int bitPos = flipRandomBit(commitIBytes); + String mutatedI = Base64.getEncoder().encodeToString(commitIBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-i", mutatedI); + String mutatedKey = objectKey + "-bad-mutated-i-bit-" + bitPos; + putObjectWithMetadata( + ptS3Client, + mutatedKey, + objectData, + mutatedMetadata + ); + mutatedIObjects.add(mutatedKey); + } + } + } + } + + static void putObjectWithMetadata( + S3Client ptS3Client, + String objectKey, + byte[] objectData, + Map objectMetadata + ) { + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .metadata(objectMetadata) + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(objectData)); + } + + static void putObjectWithInstructionFile( + S3Client ptS3Client, + String objectKey, + byte[] objectData, + Map objectMetadata, + String instructionFileJson + ) { + // Put the encrypted object + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .metadata(objectMetadata) + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes(objectData)); + + // Put the instruction file + ptS3Client.putObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey + ".instruction") + .build(), + software.amazon.awssdk.core.sync.RequestBody.fromBytes( + instructionFileJson.getBytes(StandardCharsets.UTF_8))); + } + + @AfterAll + static void signalEncryptionComplete() throws Exception { + createCorruptedCopies(); + + // Signal that all encryption tests have completed + encryptPhaseComplete.countDown(); + } + } + + /** + * Ranged Get Tests - Test Phase + * + * These tests perform ranged get operations on objects encrypted by EncryptTests. + * All tests in this class can run fully in parallel with each other. + * They depend on EncryptTests completing first. + */ + @Nested + class RangedGetTestsNested { + private static List cbcObjects; + private static List gcmObjects; + private static List kcGcmObjects; + private static List mutatedCObjects; + private static List mutatedDObjects; + private static List mutatedIObjects; + private static KeyMaterial kmsKeyArn; + private static KeyMaterial RSA_KEY; + private static KeyMaterial AES_KEY; + + @BeforeAll + static void setup() throws InterruptedException { + // Wait for all encryption tests to complete + encryptPhaseComplete.await(); + + // Import encrypted objects from the encrypt phase + cbcObjects = EncryptTests.getCbcObjects(); + gcmObjects = EncryptTests.getGcmObjects(); + kcGcmObjects = EncryptTests.getKcGcmObjects(); + mutatedCObjects = EncryptTests.getMutatedCObjects(); + mutatedDObjects = EncryptTests.getMutatedDObjects(); + mutatedIObjects = EncryptTests.getMutatedIObjects(); + kmsKeyArn = EncryptTests.getKmsKeyArn(); + RSA_KEY = EncryptTests.getRsaKey(); + AES_KEY = EncryptTests.getAesKey(); + + // Verify we have objects to test + if (cbcObjects.isEmpty() && gcmObjects.isEmpty() && kcGcmObjects.isEmpty()) { + throw new IllegalStateException( + "No encrypted objects found. Ensure EncryptTests runs first."); + } + } + + public static Stream rangedGetSupportedClients() { + Stream improved = improvedClientsForTest() + .filter(target -> RANGED_GETS_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + Stream transition = transitionClientsForTest() + .filter(target -> RANGED_GETS_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + + return Stream.concat(improved, transition); + } + + public static Stream rangedGetCBCSupportedClients() { + return rangedGetSupportedClients() + // This is just a quick hack. Perhaps it would be good to have an equivalent group for languages. + .filter(target -> !((LanguageServerTarget) target.get()[0]).getLanguageName().startsWith("CPP")); + } + + // CBC Ranged Get Tests + + @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - start range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") + void ranged_get_cbc_start_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + cbcObjects, + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - end range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") + void ranged_get_cbc_end_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // For each object, get its length and test the last 5 bytes + for (String objectKey : cbcObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF + ); + } + } + + @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - middle range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") + void ranged_get_cbc_middle_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + cbcObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - whole file") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") + void ranged_get_cbc_whole_file_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // For each object, get its length and test the whole file using range + for (String objectKey : cbcObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF + ); + } + } + + // // GCM Ranged Get Tests + + @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - start range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_gcm_start_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + gcmObjects, + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - end range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_gcm_end_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // For each object, get its length and test the last 5 bytes + for (String objectKey : gcmObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + } + + @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - middle range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_gcm_middle_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + gcmObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - whole file") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_gcm_whole_file_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // For each object, get its length and test the whole file using range + for (String objectKey : gcmObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + } + + @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - Include tag") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_gcm_tag_only_succeeds(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + gcmObjects, + 10, + 1000, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + + // KC-GCM Ranged Get Tests - Valid Objects + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - start range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_start_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects, + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - middle range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_middle_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - Include tag") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_tag_only_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects, + 10, + 1000, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjects + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + 10, + 1000, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - end range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_end_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Test original objects + for (String objectKey : kcGcmObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // Test good-copy objects + for (String objectKey : kcGcmObjects) { + String goodCopyKey = objectKey + "-good-copy"; + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(goodCopyKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(goodCopyKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - whole file") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Test original objects + for (String objectKey : kcGcmObjects) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // Test good-copy objects + for (String objectKey : kcGcmObjects) { + String goodCopyKey = objectKey + "-good-copy"; + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(goodCopyKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(goodCopyKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + } + + // KC-GCM Ranged Get Tests - Failure Cases + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with no commitment - add to instruction") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_no_commitment_add_to_instruction_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + kcGcmObjects.stream() + .map(key -> key + "-bad-no-commitment-add-to-instruction") + .collect(Collectors.toList()), + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with no commitment - only instruction") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_no_commitment_only_instruction_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + kcGcmObjects.stream() + .map(key -> key + "-bad-no-commitment-only-instruction") + .collect(Collectors.toList()), + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment C") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_mutated_c_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + assertFalse(mutatedCObjects.isEmpty(), "Expected mutated C objects to be created but list is empty"); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedCObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment D") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_mutated_d_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + assertFalse(mutatedDObjects.isEmpty(), "Expected mutated D objects to be created but list is empty"); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedDObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment I") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_mutated_i_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + assertFalse(mutatedIObjects.isEmpty(), "Expected mutated I objects to be created but list is empty"); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedIObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + } +} 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 6f274747..73b363e2 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 @@ -6,6 +6,7 @@ package software.amazon.encryption.s3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import java.net.Socket; import java.net.URI; @@ -99,6 +100,14 @@ public class TestUtils { public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); + public static final Set RE_ENCRYPT_SUPPORTED = + Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4); + + public static final Set RANGED_GETS_SUPPORTED = + Set.of( + JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4 + , CPP_V2_CURRENT, CPP_V2_TRANSITION, CPP_V3 + ); // Cpp only supports Raw AES public static final Set RAW_AES_SUPPORTED = @@ -649,4 +658,103 @@ public static void Decrypt_fails( assertEquals(successfulDecrypt.size(), 0, "Decryption should have failed:" + String.join(",", successfulDecrypt)); } + + /** + * Perform ranged get operation with specified byte range + */ + public static void RangedGet( + S3ECTestServerClient client, + String S3ECId, + List objectKeys, + long rangeStart, + long rangeEnd, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + List failures = new ArrayList<>(); + for (String objectKey : objectKeys) { + try { + // Get the full object first to know expected content + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + byte[] fullContent = fullOutput.getBody().array(); + + // Perform ranged get + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .range("bytes=" + rangeStart + "-" + rangeEnd) + .build()); + + // Verify the ranged content matches expected slice + byte[] rangedContent = output.getBody().array(); + int startIndex = (int) rangeStart; + int endIndex = (int) Math.min(rangeEnd + 1, fullContent.length); // +1 because HTTP ranges are inclusive + byte[] expectedContent = Arrays.copyOfRange(fullContent, startIndex, endIndex); + assertArrayEquals(expectedContent, rangedContent, + "Ranged get returned unexpected data for:" + objectKey); + + // Verify encryption algorithm + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "Encryption algorithm mismatch for " + objectKey + ); + } catch (Exception e) { + failures.add(String.format( + "Failed ranged get on '%s': %s - %s", + objectKey, e.getClass().getSimpleName(), e.getMessage() + )); + } + } + + if (!failures.isEmpty()) { + throw new AssertionError(String.format( + "Ranged get failed for %d out of %d objects:\n%s", + failures.size(), objectKeys.size(), + String.join("\n", failures) + )); + } + } + + /** + * Perform ranged get operations that are expected to fail + */ + public static void RangedGet_fails( + S3ECTestServerClient client, + String S3ECId, + List objectKeys, + long rangeStart, + long rangeEnd, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + List successfulGets = new ArrayList<>(); + for (String objectKey : objectKeys) { + try { + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "Encryption algorithm mismatch for " + objectKey + ); + + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .range("bytes=" + rangeStart + "-" + rangeEnd) + .build()); + + // Should have failed but didn't + successfulGets.add(objectKey); + } catch (S3EncryptionClientError e) { + // This is expected - the ranged get should fail + } + } + + assertEquals(0, successfulGets.size(), + "Ranged get should have failed for: " + String.join(", ", successfulGets)); + } } diff --git a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java index e7c5493f..fbccd458 100644 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -34,10 +34,16 @@ public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { Map ecMap = metadataListToMap(input.getMetadata()); try { - ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap))); + ResponseBytes resp = s3Client.getObjectAsBytes(builder -> { + builder.bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(ecMap)); + + // Add range header if provided + if (input.getRange() != null && !input.getRange().isEmpty()) { + builder.range(input.getRange()); + } + }); List mdAsList = metadataMapToList(resp.response().metadata()); // Can't use asBB else it gets mad bc cant access backing array diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java index 86749489..9dd9c044 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -35,11 +35,17 @@ public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { S3Client s3Client = clientCache_.get(input.getClientID()); Map ecMap = metadataListToMap(input.getMetadata()); - try { - ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap))); + try { + ResponseBytes resp = s3Client.getObjectAsBytes(builder -> { + builder.bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(ecMap)); + + // Add range header if provided + if (input.getRange() != null && !input.getRange().isEmpty()) { + builder.range(input.getRange()); + } + }); List mdAsList = metadataMapToList(resp.response().metadata()); // Can't use asBB else it gets mad bc cant access backing array diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java index 17e9a8ee..23dcc944 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -34,10 +34,16 @@ public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { Map ecMap = metadataListToMap(input.getMetadata()); try { - ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap))); + ResponseBytes resp = s3Client.getObjectAsBytes(builder -> { + builder.bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(ecMap)); + + // Add range header if provided + if (input.getRange() != null && !input.getRange().isEmpty()) { + builder.range(input.getRange()); + } + }); List mdAsList = metadataMapToList(resp.response().metadata()); // Can't use asBB else it gets mad bc cant access backing array diff --git a/test-server/model/object.smithy b/test-server/model/object.smithy index 6d793353..3ff1cf3c 100644 --- a/test-server/model/object.smithy +++ b/test-server/model/object.smithy @@ -80,6 +80,10 @@ operation GetObject { @required @notProperty clientID: String + + @httpHeader("Range") + @notProperty + range: String } output := for Object { From cd3b3cdc36ad5f4f4ea1cd74ada7f4cfc6f261d9 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Tue, 25 Nov 2025 12:04:47 -0800 Subject: [PATCH 32/46] try a different timeing --- test-server/java-tests/build.gradle.kts | 2 +- .../encryption/s3/ExhaustiveRoundTripTests1_25.java | 7 +++++++ .../software/amazon/encryption/s3/RoundTripTests.java | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 14c3eec1..2d1cbdeb 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -61,7 +61,7 @@ tasks { 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 + Math.max(1, Runtime.getRuntime().availableProcessors() - 3).toString()) // Scale with CPU, reserve 3 cores // Passing information from Gradle into the tests so that we can filter our servers systemProperty("test.filter.servers", System.getProperty("test.filter.servers")) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java index 9565639b..36f9da9f 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java @@ -211,6 +211,13 @@ public void GIVEN_KCGCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncry .build()); String decS3ECId = decClientOutput.getClientId(); + // At high concurrency, this test tends to get: + // BadDigest Message: The CRC64NVME you specified did not match the calculated checksum. + // I think this is a read after write issue. + // A better fix, would be to break this tests suite up into encrypt/decrypt + // rather than having a test for many pairs and doing encrypt/decrypt on each pair + Thread.sleep(100); + // When: decrypt KC-GCM object with an improved version client with ForbidEncryptAllowDecrypt policy GetObjectOutput output = decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) 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 2e946030..fb017bb4 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 @@ -601,6 +601,14 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe // Ruby and PHP do not include it :( assertTrue(ptInstFile.response().metadata().containsKey("x-amz-crypto-instr-file")); } + + // At high concurrency, this test tends to get: + // BadDigest Message: The CRC64NVME you specified did not match the calculated checksum. + // I think this is a read after write issue. + // A better fix, would be to break this tests suite up into encrypt/decrypt + // rather than having a test for many pairs and doing encrypt/decrypt on each pair + Thread.sleep(100); + assertFalse(ptInstFile.asUtf8String().isEmpty()); // Read should be enabled by default GetObjectOutput output = decClient.getObject(GetObjectInput.builder() From 107e91e46f14a0a555876a093b85d0fba4a33434 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 26 Nov 2025 09:07:15 -0800 Subject: [PATCH 33/46] add some exceptions --- .../amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java | 2 +- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java index 36f9da9f..b161feb6 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java @@ -177,7 +177,7 @@ public void GIVEN_GCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncrypt @MethodSource("software.amazon.encryption.s3.TestUtils#encryptImprovedDecryptImproved") public void GIVEN_KCGCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncryptAllowDecrypt_WHEN_Decrypt_THEN_Pass( TestUtils.LanguageServerTarget encLang, TestUtils.LanguageServerTarget decLang - ) { + ) throws Exception { S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); final String objectKey = "encrypt-kc-gcm-decrypt-improved-test-key-" + encLang + "-" + decLang; 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 fb017bb4..cf45006e 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 @@ -536,7 +536,7 @@ public void instructionFileReadV2Format(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") - public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageServerTarget decLang) { + public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageServerTarget decLang) throws Exception { if (INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(encLang.getLanguageName())) { throw new TestAbortedException("not testing " + encLang.getLanguageName()); } @@ -684,4 +684,4 @@ public void instructionFileWriteAndReadWithRSA(LanguageServerTarget encLang, Lan assertEquals(input, new String(output.getBody().array())); } -} \ No newline at end of file +} From 5e0833505c6974f4cac542ee56bf398a81fc57d9 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 26 Nov 2025 11:47:35 -0800 Subject: [PATCH 34/46] disable checksum? --- test-server/cpp-v2-server/main.cpp | 7 +++++++ test-server/cpp-v3-server/main.cpp | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index ae0a0e8a..e8ffe770 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -193,6 +193,13 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, Aws::Client::ClientConfiguration clientConfig; clientConfig.maxConnections = 512; // Large pool per client clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + // Disable automatic checksum calculation for encrypted streams + // The ChecksumInterceptor cannot handle non-seekable SymmetricCryptoStream + // which causes intermittent "BadDigest: CRC64NVME you specified did not match" errors + // when the stream gets consumed during checksum calculation and can't be rewound + clientConfig.checksumConfig.requestChecksumCalculation = + Aws::Client::RequestChecksumCalculation::WHEN_REQUIRED; auto encryption_client = std::make_shared(config, clientConfig); diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 609d7e90..4e7227df 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -295,6 +295,14 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, Aws::Client::ClientConfiguration clientConfig; clientConfig.maxConnections = 512; // Large pool per client clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + + // Disable automatic checksum calculation for encrypted streams + // The ChecksumInterceptor cannot handle non-seekable SymmetricCryptoStream + // which causes intermittent "BadDigest: CRC64NVME you specified did not match" errors + // when the stream gets consumed during checksum calculation and can't be rewound + clientConfig.checksumConfig.requestChecksumCalculation = + Aws::Client::RequestChecksumCalculation::WHEN_REQUIRED; + auto encryption_client = std::make_shared(*config, clientConfig); std::string client_id = generate_uuid(); From 9dafb9bc90a6fa78a743d00605ce291eaa23af89 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 1 Dec 2025 15:47:56 -0800 Subject: [PATCH 35/46] update words --- .../software/amazon/encryption/s3/RangedGetTests.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java index 5b5ac6e1..bf9219ab 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -100,7 +100,7 @@ * * Auth tag only (last 16 bytes for authenticated algorithms) * - Commitment State (KC-GCM only): * * Valid (original and good-copy) - * * No commitment - removed from metadata, added to instruction file + * * Commitment duplicated - left in metadata added to instruction file * * No commitment - removed from metadata, only in instruction file * * Mutated commitment - bit flipped in x-amz-c value * * Mutated commitment - bit flipped in x-amz-d value @@ -115,8 +115,9 @@ * - File size: Object keys themselves (short strings) serve as representative small files * - Byte ranges: Fixed patterns covering important boundary conditions * - * FILTERING: - * - Only languages in RANGED_GETS_SUPPORTED set are tested + * SCOPE: + * - Languages in RANGED_GETS_SUPPORTED set are tested, + * the encrypt tests are to create values that are then tested. * - CBC and GCM tests validate ranged get functionality works * - KC-GCM tests focus on commitment validation during ranged gets */ @@ -346,9 +347,9 @@ static void createCorruptedCopies() throws Exception { // No commitment - removed from metadata, added to instruction file putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-no-commitment-add-to-instruction", + objectKey + "-bad-commitment-add-to-instruction", objectData, - noCommitMetadata, + objectMetadata, instructionFileJson ); From ef72f035aa99199ec7f28157e63361513f80b267 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Tue, 2 Dec 2025 13:46:42 -0800 Subject: [PATCH 36/46] Adding ReEncrypt tests and support for custom instruction files --- test-server/Makefile | 4 +- .../cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- .../amazon/encryption/s3/ReEncryptTests.java | 644 ++++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 113 +++ .../s3/CreateClientOperationImpl.java | 5 +- .../encryption/s3/GetObjectOperationImpl.java | 12 +- .../encryption/s3/ReEncryptOperationImpl.java | 108 +++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- .../s3/CreateClientOperationImpl.java | 39 +- .../encryption/s3/GetObjectOperationImpl.java | 12 +- .../encryption/s3/ReEncryptOperationImpl.java | 185 +++++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- test-server/model/client.smithy | 11 +- test-server/model/object.smithy | 61 +- .../src/get_object.php | 14 +- test-server/php-v3-server/src/get_object.php | 14 +- test-server/ruby-v2-server/app.rb | 11 +- test-server/ruby-v3-server/app.rb | 11 +- 19 files changed, 1215 insertions(+), 41 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java diff --git a/test-server/Makefile b/test-server/Makefile index 28f6e1be..0ae81124 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -84,7 +84,9 @@ run-tests: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./gradlew --build-cache --info --parallel --no-daemon integ -Dtest.filter.servers="$(FILTER)" + ./gradlew --build-cache --info --parallel --no-daemon integ \ + $(if $(TEST),--tests "$(TEST)",) \ + -Dtest.filter.servers="$(FILTER)" @echo "Tests completed successfully" # Stop the servers diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index 8d82fa39..9fc3de92 160000 --- a/test-server/cpp-v2-transition-server/aws-sdk-cpp +++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 8d82fa393b08f8564b0aa939fba04df402e3ee47 +Subproject commit 9fc3de9203e72e0aa843c43657ff3afefdbfdfa5 diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 8d82fa39..9fc3de92 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 8d82fa393b08f8564b0aa939fba04df402e3ee47 +Subproject commit 9fc3de9203e72e0aa843c43657ff3afefdbfdfa5 diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java new file mode 100644 index 00000000..1fd46e21 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java @@ -0,0 +1,644 @@ +/* + * 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.assertNotNull; +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.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +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 com.amazonaws.services.s3.model.EncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; + +import software.amazon.encryption.s3.TestUtils.LanguageServerTarget; +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.ReEncryptInput; +import software.amazon.encryption.s3.model.ReEncryptOutput; +import software.amazon.encryption.s3.model.S3ECConfig; +import software.amazon.encryption.s3.model.S3EncryptionClientError; + +/** + * ReEncrypt Instruction File Tests - S3 Encryption Client Cross-Language Compatibility + * + * PURPOSE: + * This test suite validates that instruction file re-encryption enables key rotation without + * re-uploading encrypted objects, and that re-encrypted objects maintain cross-language + * compatibility and commitment validation guarantees. + * + * WHAT IS BEING TESTED: + * 1. Instruction file re-encryption for KC-GCM algorithm with raw keyrings + * 2. Re-encryption across different raw keyring types (AES, RSA) + * 3. Same-type keyring rotation (AES => AES, RSA => RSA) + * 4. Cross-type keyring rotation (AES => RSA, RSA => AES) + * 5. Default instruction file suffix (.instruction) and custom suffixes (.instruction-rsa, .instruction-aes) + * 6. Cross-language compatibility: all languages can decrypt after re-encryption + * 7. Rotation enforcement to prevent re-encryption with the same key + * + * WHY THIS IS IMPORTANT: + * - Key rotation is a critical security operation that should not require expensive object re-uploads + * - ReEncryptInstructionFile enables updating the encrypted data key without touching the ciphertext + * - Raw keyrings (AES, RSA) provide direct key material access required for re-encryption + * - Cross-type rotation (e.g., AES to RSA) enables flexibility in key management strategies + * - Commitment validation must be maintained even when instruction files are re-encrypted + * - Cross-language compatibility ensures key rotation doesn't break existing clients + * - Rotation enforcement prevents accidental re-encryption with the same key material + * - Custom instruction file suffixes enable sharing encrypted objects with partners + * + * TEST STRUCTURE: + * This suite uses a three-phase approach with enforced ordering: + * 1. EncryptTests - Encrypts objects with instruction files using AES and RSA keyrings + * - All encrypt tests can run in parallel within this phase + * - Signals encryptPhaseComplete latch when done + * 2. ReEncryptTests - Waits for encryption to complete, then re-encrypts instruction files + * - Tests same-type rotations (AES => AES, RSA => RSA) + * - Tests cross-type rotations (AES => RSA with .instruction-rsa suffix, RSA => AES with .instruction-aes suffix) + * - Tests rotation enforcement (same key rejection) + * - All re-encrypt tests can run in parallel within this phase + * - Tracks which objects were re-encrypted to which keys to prevent conflicts + * - Signals reEncryptPhaseComplete latch when done + * 3. DecryptReEncryptedTests - Waits for re-encryption to complete, then tests decryption + * - Tests cross-language decryption compatibility after re-encryption + * - Uses tracked object lists to decrypt with correct keys and custom instruction file suffixes + * - All decrypt tests can run in parallel within this phase + * + * Coordination uses two CountDownLatches: + * - encryptPhaseComplete: Ensures all encryption completes before re-encryption begins + * - reEncryptPhaseComplete: Ensures all re-encryption completes before decryption begins + * + * INPUT DIMENSIONS: + * - Source Key Material: AES (256-bit), RSA (2048-bit key pairs) + * - Destination Key Material: Different AES or RSA keys (raw keyrings) + * - Encryption Algorithm: KC-GCM (ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) + * - Instruction File Suffix: default (.instruction), custom (.instruction-rsa, .instruction-aes) + * - Language for Re-encryption: Java V3-Transition, Java V4 (RE_ENCRYPT_SUPPORTED) + * - Language for Decryption: All languages supporting instruction files + * - Rotation Enforcement: enforceRotation flag (true/false) + * + * EXPECTED RESULTS: + * - Positive: Re-encryption succeeds with different key material, all languages can decrypt + * - Negative: Re-encryption fails when enforceRotation detects same key material + * + * REPRESENTATIVE VALUES: + * - Object keys themselves (short strings) serve as representative small plaintext files + * - Instruction file suffix: ".instruction" (default), ".instruction-rsa", ".instruction-aes" + * - Key materials: Generated once per type and reused across tests + * + * FILTERING: + * - Only languages in RE_ENCRYPT_SUPPORTED can perform re-encryption operations + * - Languages in INSTRUCTION_FILE_GET_UNSUPPORTED cannot decrypt with instruction files + * + * NOTE: KMS keyrings are NOT supported for re-encryption as the reEncryptInstructionFile + * method requires RawKeyring instances (AES or RSA) which provide direct access to key material. + * + */ +public class ReEncryptTests { + // Synchronization latches for three-phase coordination + private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + private static final CountDownLatch reEncryptPhaseComplete = new CountDownLatch(1); + + // Tracking lists for re-encrypted objects - shared across nested test classes + private static final List reEncryptedAesToAes = Collections.synchronizedList(new ArrayList<>()); + private static final List reEncryptedRsaToRsa = Collections.synchronizedList(new ArrayList<>()); + private static final List reEncryptedAesToRsa = Collections.synchronizedList(new ArrayList<>()); + private static final List reEncryptedRsaToAesDefault = Collections.synchronizedList(new ArrayList<>()); + private static final List reEncryptedAesToRsaDefault = Collections.synchronizedList(new ArrayList<>()); + + @Nested + class EncryptTests { + private static final String sharedObjectKeyBase = "test-reencrypt"; + + private static SecretKey aesKey1, aesKey2; + private static KeyMaterial aesKeyMaterial1, aesKeyMaterial2; + private static KeyPair rsaKeyPair1, rsaKeyPair2; + private static KeyMaterial rsaKeyMaterial1, rsaKeyMaterial2; + + // Separate object lists for each re-encryption path to avoid conflicts + private static final List kcGcmObjectsAesToAes = Collections.synchronizedList(new ArrayList<>()); + private static final List kcGcmObjectsAesToRsaCustom = Collections.synchronizedList(new ArrayList<>()); + private static final List kcGcmObjectsAesToRsaDefault = Collections.synchronizedList(new ArrayList<>()); + private static final List kcGcmObjectsRsaToRsa = Collections.synchronizedList(new ArrayList<>()); + private static final List kcGcmObjectsRsaToAesDefault = Collections.synchronizedList(new ArrayList<>()); + + @BeforeAll + static void generateKeys() throws Exception { + KeyGenerator aesKeyGen = KeyGenerator.getInstance("AES"); + aesKeyGen.init(256); + aesKey1 = aesKeyGen.generateKey(); + aesKey2 = aesKeyGen.generateKey(); + + Map aesMatDesc1 = new HashMap<>(); + aesMatDesc1.put("keyId", "aes-key-1"); + aesKeyMaterial1 = KeyMaterial.builder() + .aesKey(ByteBuffer.wrap(aesKey1.getEncoded())) + .materialsDescription(aesMatDesc1) + .build(); + + Map aesMatDesc2 = new HashMap<>(); + aesMatDesc2.put("keyId", "aes-key-2"); + aesKeyMaterial2 = KeyMaterial.builder() + .aesKey(ByteBuffer.wrap(aesKey2.getEncoded())) + .materialsDescription(aesMatDesc2) + .build(); + + KeyPairGenerator rsaKeyGen = KeyPairGenerator.getInstance("RSA"); + rsaKeyGen.initialize(2048); + rsaKeyPair1 = rsaKeyGen.generateKeyPair(); + rsaKeyPair2 = rsaKeyGen.generateKeyPair(); + + Map rsaMatDesc1 = new HashMap<>(); + rsaMatDesc1.put("keyId", "rsa-key-1"); + rsaKeyMaterial1 = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(rsaKeyPair1.getPrivate().getEncoded())) + .materialsDescription(rsaMatDesc1) + .build(); + + Map rsaMatDesc2 = new HashMap<>(); + rsaMatDesc2.put("keyId", "rsa-key-2"); + rsaKeyMaterial2 = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(rsaKeyPair2.getPrivate().getEncoded())) + .materialsDescription(rsaMatDesc2) + .build(); + } + + static List getKcGcmObjectsAesToAes() { return new ArrayList<>(kcGcmObjectsAesToAes); } + static List getKcGcmObjectsAesToRsaCustom() { return new ArrayList<>(kcGcmObjectsAesToRsaCustom); } + static List getKcGcmObjectsAesToRsaDefault() { return new ArrayList<>(kcGcmObjectsAesToRsaDefault); } + static List getKcGcmObjectsRsaToRsa() { return new ArrayList<>(kcGcmObjectsRsaToRsa); } + static List getKcGcmObjectsRsaToAesDefault() { return new ArrayList<>(kcGcmObjectsRsaToAesDefault); } + static KeyMaterial getAesKeyMaterial1() { return aesKeyMaterial1; } + static KeyMaterial getAesKeyMaterial2() { return aesKeyMaterial2; } + static KeyMaterial getRsaKeyMaterial1() { return rsaKeyMaterial1; } + static KeyMaterial getRsaKeyMaterial2() { return rsaKeyMaterial2; } + + public static Stream improvedClientsCanPutRawRSAWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + public static Stream improvedClientsCanPutRawAESWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => AES re-encryption") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") + void encrypt_aes_for_aes_to_aes_reencrypt(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-aes-to-aes-" + language.getLanguageName()), + kcGcmObjectsAesToAes, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => RSA custom suffix re-encryption") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") + void encrypt_aes_for_aes_to_rsa_custom_reencrypt(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-aes-to-rsa-custom-" + language.getLanguageName()), + kcGcmObjectsAesToRsaCustom, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => RSA default suffix re-encryption") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") + void encrypt_aes_for_aes_to_rsa_default_reencrypt(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-aes-to-rsa-default-" + language.getLanguageName()), + kcGcmObjectsAesToRsaDefault, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Encrypt RSA objects for RSA => RSA re-encryption") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encrypt_rsa_for_rsa_to_rsa_reencrypt(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-rsa-to-rsa-" + language.getLanguageName()), + kcGcmObjectsRsaToRsa, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @ParameterizedTest(name = "{0}: Encrypt RSA objects for RSA => AES default suffix re-encryption") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encrypt_rsa_for_rsa_to_aes_default_reencrypt(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt(client, S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-rsa-to-aes-default-" + language.getLanguageName()), + kcGcmObjectsRsaToAesDefault, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + + @AfterAll + static void signalEncryptionComplete() { + encryptPhaseComplete.countDown(); + } + } + + @Nested + class ReEncryptTestsNested { + private static List kcGcmObjectsAesToAes, kcGcmObjectsAesToRsaCustom, kcGcmObjectsAesToRsaDefault; + private static List kcGcmObjectsRsaToRsa, kcGcmObjectsRsaToAesDefault; + private static KeyMaterial aesKeyMaterial1, aesKeyMaterial2, rsaKeyMaterial1, rsaKeyMaterial2; + + @BeforeAll + static void setup() throws InterruptedException { + encryptPhaseComplete.await(); + kcGcmObjectsAesToAes = EncryptTests.getKcGcmObjectsAesToAes(); + kcGcmObjectsAesToRsaCustom = EncryptTests.getKcGcmObjectsAesToRsaCustom(); + kcGcmObjectsAesToRsaDefault = EncryptTests.getKcGcmObjectsAesToRsaDefault(); + kcGcmObjectsRsaToRsa = EncryptTests.getKcGcmObjectsRsaToRsa(); + kcGcmObjectsRsaToAesDefault = EncryptTests.getKcGcmObjectsRsaToAesDefault(); + aesKeyMaterial1 = EncryptTests.getAesKeyMaterial1(); + aesKeyMaterial2 = EncryptTests.getAesKeyMaterial2(); + rsaKeyMaterial1 = EncryptTests.getRsaKeyMaterial1(); + rsaKeyMaterial2 = EncryptTests.getRsaKeyMaterial2(); + } + + public static Stream reencryptSupportedClients() { + return improvedClientsForTest() + .filter(target -> RE_ENCRYPT_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + + @ParameterizedTest(name = "{0}: ReEncrypt AES => AES instruction file") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") + void reencrypt_aes_to_aes_instruction_file(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + + for (int i = 0; i < kcGcmObjectsAesToAes.size(); i++) { + String objectKey = kcGcmObjectsAesToAes.get(i); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + ReEncryptOutput response = client.reEncrypt(ReEncryptInput.builder() + .bucket(TestUtils.BUCKET).key(objectKey).clientID(S3ECId) + .newKeyMaterial(aesKeyMaterial2).build()); + + assertNotNull(response); + reEncryptedAesToAes.add(objectKey); + } + } + + @ParameterizedTest(name = "{0}: ReEncrypt RSA => RSA instruction file") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") + void reencrypt_rsa_to_rsa_instruction_file(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + + for (int i = 0; i < kcGcmObjectsRsaToRsa.size(); i++) { + String objectKey = kcGcmObjectsRsaToRsa.get(i); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + ReEncryptOutput response = client.reEncrypt(ReEncryptInput.builder() + .bucket(TestUtils.BUCKET).key(objectKey).clientID(S3ECId) + .newKeyMaterial(rsaKeyMaterial2).build()); + + assertNotNull(response); + reEncryptedRsaToRsa.add(objectKey); + } + } + + @ParameterizedTest(name = "{0}: ReEncrypt AES => RSA instruction file with custom suffix") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") + void reencrypt_aes_to_rsa_instruction_file(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + + for (int i = 0; i < kcGcmObjectsAesToRsaCustom.size(); i++) { + String objectKey = kcGcmObjectsAesToRsaCustom.get(i); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + ReEncryptOutput response = client.reEncrypt(ReEncryptInput.builder() + .bucket(TestUtils.BUCKET).key(objectKey).clientID(S3ECId) + .newKeyMaterial(rsaKeyMaterial1) + // Java always prepends a `.` + .instructionFileSuffix("instruction-rsa") + .build()); + + assertNotNull(response); + reEncryptedAesToRsa.add(objectKey); + } + } + + @ParameterizedTest(name = "{0}: ReEncrypt RSA => AES instruction file (default suffix)") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") + void reencrypt_rsa_to_aes_default_instruction_file(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + + for (int i = 0; i < kcGcmObjectsRsaToAesDefault.size(); i++) { + String objectKey = kcGcmObjectsRsaToAesDefault.get(i); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + ReEncryptOutput response = client.reEncrypt(ReEncryptInput.builder() + .bucket(TestUtils.BUCKET).key(objectKey).clientID(S3ECId) + .newKeyMaterial(aesKeyMaterial1) + .build()); + + assertNotNull(response); + reEncryptedRsaToAesDefault.add(objectKey); + } + } + + @ParameterizedTest(name = "{0}: ReEncrypt AES => RSA instruction file (default suffix)") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") + void reencrypt_aes_to_rsa_default_instruction_file(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + + for (int i = 0; i < kcGcmObjectsAesToRsaDefault.size(); i++) { + String objectKey = kcGcmObjectsAesToRsaDefault.get(i); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + ReEncryptOutput response = client.reEncrypt(ReEncryptInput.builder() + .bucket(TestUtils.BUCKET).key(objectKey).clientID(S3ECId) + .newKeyMaterial(rsaKeyMaterial1) + .build()); + + assertNotNull(response); + reEncryptedAesToRsaDefault.add(objectKey); + } + } + + @AfterAll + static void signalReEncryptionComplete() { + reEncryptPhaseComplete.countDown(); + } + } + + @Nested + class DecryptReEncryptedTests { + private static KeyMaterial aesKeyMaterial1, aesKeyMaterial2, rsaKeyMaterial1, rsaKeyMaterial2; + + @BeforeAll + static void setup() throws InterruptedException { + reEncryptPhaseComplete.await(); + aesKeyMaterial1 = EncryptTests.getAesKeyMaterial1(); + aesKeyMaterial2 = EncryptTests.getAesKeyMaterial2(); + rsaKeyMaterial1 = EncryptTests.getRsaKeyMaterial1(); + rsaKeyMaterial2 = EncryptTests.getRsaKeyMaterial2(); + } + + public static Stream clientsCanGetRawRSAWithInstructionFile() { + return Stream.concat( + improvedClientsForTest().filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())), + transitionClientsForTest().filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + ); + } + + public static Stream clientsCanGetRawAESWithInstructionFile() { + return Stream.concat( + improvedClientsForTest().filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())), + transitionClientsForTest().filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + ); + } + + public static Stream clientsCanGetRawRSAWithInstructionFileAndCustomSuffix() { + return Stream.concat( + improvedClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())), + transitionClientsForTest() + .filter(target -> RAW_RSA_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + ); + } + + public static Stream clientsCanGetRawAESWithInstructionFileAndCustomSuffix() { + return Stream.concat( + improvedClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())), + transitionClientsForTest() + .filter(target -> RAW_AES_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + ); + } + + @ParameterizedTest(name = "{0}: Decrypt AES => AES re-encrypted objects") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_reencrypted_aes_to_aes_objects(TestUtils.LanguageServerTarget language) { + if (reEncryptedAesToAes.isEmpty()) return; + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder().keyMaterial(aesKeyMaterial2).build()) + .build()); + + // C++ clients require materials description to be passed per-operation + if (language.getLanguageName().startsWith("CPP")) { + TestUtils.DecryptWithMaterialsDescription(client, clientOutput.getClientId(), + reEncryptedAesToAes, aesKeyMaterial2, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else { + TestUtils.Decrypt(client, clientOutput.getClientId(), reEncryptedAesToAes, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + } + + @ParameterizedTest(name = "{0}: Decrypt RSA => RSA re-encrypted objects") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_reencrypted_rsa_to_rsa_objects(TestUtils.LanguageServerTarget language) { + if (reEncryptedRsaToRsa.isEmpty()) return; + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder().keyMaterial(rsaKeyMaterial2).build()) + .build()); + + // C++ clients require materials description to be passed per-operation + if (language.getLanguageName().startsWith("CPP")) { + TestUtils.DecryptWithMaterialsDescription(client, clientOutput.getClientId(), + reEncryptedRsaToRsa, rsaKeyMaterial2, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else { + TestUtils.Decrypt(client, clientOutput.getClientId(), reEncryptedRsaToRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + } + + @ParameterizedTest(name = "{0}: Decrypt AES => RSA re-encrypted objects with custom suffix") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFileAndCustomSuffix") + void decrypt_reencrypted_aes_to_rsa_objects(TestUtils.LanguageServerTarget language) { + if (reEncryptedAesToRsa.isEmpty()) return; + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .build()) + .build()); + + // C++ clients require materials description to be passed per-operation + if (language.getLanguageName().startsWith("CPP")) { + TestUtils.DecryptWithMaterialsDescription(client, clientOutput.getClientId(), + reEncryptedAesToRsa, rsaKeyMaterial1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else { + TestUtils.Decrypt(client, clientOutput.getClientId(), reEncryptedAesToRsa, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + reEncryptedAesToRsa, ".instruction-rsa"); + } + } + + @ParameterizedTest(name = "{0}: Decrypt RSA => AES re-encrypted objects (default suffix)") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawAESWithInstructionFile") + void decrypt_reencrypted_rsa_to_aes_default_objects(TestUtils.LanguageServerTarget language) { + if (reEncryptedRsaToAesDefault.isEmpty()) return; + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(aesKeyMaterial1) + .build()) + .build()); + + // C++ clients require materials description to be passed per-operation + if (language.getLanguageName().startsWith("CPP")) { + TestUtils.DecryptWithMaterialsDescription(client, clientOutput.getClientId(), + reEncryptedRsaToAesDefault, aesKeyMaterial1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else { + TestUtils.Decrypt(client, clientOutput.getClientId(), reEncryptedRsaToAesDefault, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + } + + @ParameterizedTest(name = "{0}: Decrypt AES => RSA re-encrypted objects (default suffix)") + @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFile") + void decrypt_reencrypted_aes_to_rsa_default_objects(TestUtils.LanguageServerTarget language) { + if (reEncryptedAesToRsaDefault.isEmpty()) return; + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyMaterial1) + .build()) + .build()); + + // C++ clients require materials description to be passed per-operation + if (language.getLanguageName().startsWith("CPP")) { + TestUtils.DecryptWithMaterialsDescription(client, clientOutput.getClientId(), + reEncryptedAesToRsaDefault, rsaKeyMaterial1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } else { + TestUtils.Decrypt(client, clientOutput.getClientId(), reEncryptedAesToRsaDefault, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + } + } + } +} 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 73b363e2..dee9a266 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 @@ -37,6 +37,7 @@ 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.PutObjectOutput; import software.amazon.encryption.s3.model.S3EncryptionClientError; @@ -144,6 +145,18 @@ public class TestUtils { public static final Set INSTRUCTION_FILE_GET_UNSUPPORTED = Set.of(PYTHON_V3); + // Languages that support custom instruction file suffix on GetObject + // Only Java, Ruby, and PHP servers have been updated with this feature + public static final Set CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED = + Set.of( + JAVA_V3_TRANSITION, + JAVA_V4, + RUBY_V2_TRANSITION, + RUBY_V3, + PHP_V2_TRANSITION, + PHP_V3 + ); + public static final Set CURRENT_VERSIONS = Set.of( JAVA_V3_CURRENT, @@ -593,6 +606,105 @@ public static void Decrypt( EncryptionAlgorithm expectedEncryptionAlgorithm, List expectedPlaintexts ) { + Decrypt(client, S3ECId, crossLanguageObjects, expectedEncryptionAlgorithm, expectedPlaintexts, null); + } + + public static void Decrypt( + S3ECTestServerClient client, + String S3ECId, + List crossLanguageObjects, + EncryptionAlgorithm expectedEncryptionAlgorithm, + List expectedPlaintexts, + String instructionFileSuffix + ) { + if (crossLanguageObjects.isEmpty()) { + throw new AssertionError("There is nothing to decrypt"); + } + + List failures = new ArrayList<>(); + for (int i = 0; i < crossLanguageObjects.size(); i++) { + try { + String objectKey = crossLanguageObjects.get(i); + String expectedPlaintext = expectedPlaintexts.get(i); + + GetObjectInput.Builder builder = GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey); + + // Add custom instruction file suffix if provided + if (instructionFileSuffix != null && !instructionFileSuffix.isEmpty()) { + builder.instructionFileSuffix(instructionFileSuffix); + } + + GetObjectOutput output = client.getObject(builder.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 + ); + } 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) + )); + } + } + + /** + * Decrypt helper for C++ clients that require materials description per-operation. + * + * C++ SDK Design: Unlike Java/. NET/etc where materials description is embedded in the + * keyring during client creation, the C++ SDK requires passing materials description + * as a contextMap parameter to each GetObject/PutObject operation. + * + * This helper extracts materials description from KeyMaterial and passes it via the + * Content-Metadata header on each GetObject call, which the C++ server converts to + * the contextMap parameter required by the C++ SDK. + */ + public static void DecryptWithMaterialsDescription( + S3ECTestServerClient client, + String S3ECId, + List crossLanguageObjects, + KeyMaterial keyMaterial, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + DecryptWithMaterialsDescription(client, S3ECId, crossLanguageObjects, keyMaterial, + expectedEncryptionAlgorithm, crossLanguageObjects); + } + + /** + * Decrypt helper for C++ clients with custom expected plaintexts. + */ + public static void DecryptWithMaterialsDescription( + S3ECTestServerClient client, + String S3ECId, + List crossLanguageObjects, + KeyMaterial keyMaterial, + EncryptionAlgorithm expectedEncryptionAlgorithm, + List expectedPlaintexts + ) { + if (crossLanguageObjects.isEmpty()) { + throw new AssertionError("There is nothing to decrypt"); + } + + // Extract materials description from KeyMaterial + List metadata = (keyMaterial.getMaterialsDescription() != null) + ? metadataMapToList(keyMaterial.getMaterialsDescription()) + : new ArrayList<>(); + List failures = new ArrayList<>(); for (int i = 0; i < crossLanguageObjects.size(); i++) { try { @@ -603,6 +715,7 @@ public static void Decrypt( .clientID(S3ECId) .bucket(TestUtils.BUCKET) .key(objectKey) + .metadata(metadata) // Pass materials description for C++ .build()); // Then: Pass 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 e107401e..956f454b 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 @@ -38,9 +38,11 @@ public class CreateClientOperationImpl implements CreateClientOperation { private final Map clientCache_; + private final Map keyringCache_; - public CreateClientOperationImpl(Map clientCache) { + public CreateClientOperationImpl(Map clientCache, Map keyringCache) { clientCache_ = clientCache; + keyringCache_ = keyringCache; } // Copied from S3EC. @@ -156,6 +158,7 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); clientCache_.put(uuidString, s3Client); + keyringCache_.put(uuidString, keyring); return CreateClientOutput.builder() .clientId(uuidString) .build(); diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java index 9dd9c044..d3ab8289 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -38,8 +38,16 @@ public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { try { ResponseBytes resp = s3Client.getObjectAsBytes(builder -> { builder.bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap)); + .key(input.getKey()); + + // Add custom instruction file suffix if provided + if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { + builder.overrideConfiguration(config -> config + .putExecutionAttribute(S3EncryptionClient.CUSTOM_INSTRUCTION_FILE_SUFFIX, input.getInstructionFileSuffix()) + .applyMutation(c -> withAdditionalConfiguration(ecMap).accept(c))); + } else { + builder.overrideConfiguration(withAdditionalConfiguration(ecMap)); + } // Add range header if provided if (input.getRange() != null && !input.getRange().isEmpty()) { diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java new file mode 100644 index 00000000..bc4aeab5 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -0,0 +1,108 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; +import software.amazon.encryption.s3.materials.RawKeyring; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.ReEncryptInput; +import software.amazon.encryption.s3.model.ReEncryptOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.ReEncryptOperation; +import software.amazon.smithy.java.server.RequestContext; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +public class ReEncryptOperationImpl implements ReEncryptOperation { + private final Map clientCache_; + private final Map keyringCache_; + + public ReEncryptOperationImpl(Map clientCache, Map keyringCache) { + clientCache_ = clientCache; + keyringCache_ = keyringCache; + } + + @Override + public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { + try { + S3Client s3Client = clientCache_.get(input.getClientID()); + + // Ensure we have an S3EncryptionClient, not just a plain S3Client + if (!(s3Client instanceof S3EncryptionClient)) { + throw new IllegalStateException( + "Client " + input.getClientID() + " is not an S3EncryptionClient"); + } + + S3EncryptionClient s3EncryptionClient = (S3EncryptionClient) s3Client; + + // Get the keyring from cache and cast to RawKeyring + software.amazon.encryption.s3.materials.Keyring cachedKeyring = keyringCache_.get(input.getClientID()); + if (cachedKeyring == null) { + throw new IllegalStateException( + "No keyring found for client " + input.getClientID()); + } + + if (!(cachedKeyring instanceof RawKeyring)) { + throw new IllegalStateException( + "Keyring for client " + input.getClientID() + " is not a RawKeyring"); + } + + RawKeyring keyring = (RawKeyring) cachedKeyring; + + try { + // Build the ReEncryptInstructionFileRequest + ReEncryptInstructionFileRequest.Builder requestBuilder = + ReEncryptInstructionFileRequest.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .newKeyring(keyring); + + // Add optional instruction file suffix if provided + if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { + requestBuilder.instructionFileSuffix(input.getInstructionFileSuffix()); + } + + // Add optional enforceRotation if provided + if (input.isEnforceRotation() != null) { + requestBuilder.enforceRotation(input.isEnforceRotation()); + } + + ReEncryptInstructionFileRequest reEncryptRequest = requestBuilder.build(); + + // Perform the re-encryption + ReEncryptInstructionFileResponse response = + s3EncryptionClient.reEncryptInstructionFile(reEncryptRequest); + + // Build and return the output + return ReEncryptOutput.builder() + .bucket(response.bucket()) + .key(response.key()) + .instructionFileSuffix(response.instructionFileSuffix()) + .enforceRotation(response.enforceRotation()) + .build(); + + } catch (S3EncryptionClientException s3EncryptionClientException) { + // Modeled exceptions MUST be returned as such + StringWriter sw = new StringWriter(); + s3EncryptionClientException.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw S3EncryptionClientError.builder() + .message(stackTrace) + .build(); + } + } catch (Exception e) { + // Don't wrap modeled errors + if (e instanceof S3EncryptionClientError) { + throw e; + } + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java index a992cabd..c252991d 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -28,14 +28,16 @@ public void run() { // Obviously this can get messy in a real service. // Assume that the tests behave and don't induce weird race conditions. Map clientCache = new ConcurrentHashMap<>(); + Map keyringCache = new ConcurrentHashMap<>(); Server server = Server.builder() .endpoints(endpoint) .addService( S3ECTestServer.builder() - .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addCreateClientOperation(new CreateClientOperationImpl(clientCache, keyringCache)) .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) .build()) .build(); System.out.println("Starting server..."); 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 3fdb4b55..23f3a11d 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 @@ -9,6 +9,7 @@ import software.amazon.encryption.s3.materials.AesKeyring; import software.amazon.encryption.s3.materials.Keyring; import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; import software.amazon.encryption.s3.materials.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RsaKeyring; import software.amazon.encryption.s3.model.CreateClientInput; @@ -38,9 +39,11 @@ public class CreateClientOperationImpl implements CreateClientOperation { private final Map clientCache_; + private final Map keyringCache_; - public CreateClientOperationImpl(Map clientCache) { + public CreateClientOperationImpl(Map clientCache, Map keyringCache) { clientCache_ = clientCache; + keyringCache_ = keyringCache; } // Copied from S3EC. @@ -70,10 +73,21 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c if (key.getAesKey() != null) { byte[] keyBytes = new byte[key.getAesKey().remaining()]; key.getAesKey().get(keyBytes); - keyring = AesKeyring.builder() + + AesKeyring.Builder aesBuilder = AesKeyring.builder() .wrappingKey(new SecretKeySpec(keyBytes, "AES")) - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .build(); + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()); + + // Add materials description if provided + if (key.getMaterialsDescription() != null && !key.getMaterialsDescription().isEmpty()) { + MaterialsDescription.Builder matDescBuilder = MaterialsDescription.builder(); + for (Map.Entry entry : key.getMaterialsDescription().entrySet()) { + matDescBuilder.put(entry.getKey(), entry.getValue()); + } + aesBuilder.materialsDescription(matDescBuilder.build()); + } + + keyring = aesBuilder.build(); } else if (key.getRsaKey() != null) { try { byte[] keyBytes = new byte[key.getRsaKey().remaining()]; @@ -89,12 +103,22 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c // Generate public key PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - keyring = RsaKeyring.builder() + RsaKeyring.Builder rsaBuilder = RsaKeyring.builder() .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .wrappingKeyPair(PartialRsaKeyPair.builder() .publicKey(publicKey) - .privateKey(privateKey).build()) - .build(); + .privateKey(privateKey).build()); + + // Add materials description if provided + if (key.getMaterialsDescription() != null && !key.getMaterialsDescription().isEmpty()) { + MaterialsDescription.Builder matDescBuilder = MaterialsDescription.builder(); + for (Map.Entry entry : key.getMaterialsDescription().entrySet()) { + matDescBuilder.put(entry.getKey(), entry.getValue()); + } + rsaBuilder.materialsDescription(matDescBuilder.build()); + } + + keyring = rsaBuilder.build(); } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { throw GenericServerError.builder() .message(nse.getMessage()) @@ -155,6 +179,7 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); clientCache_.put(uuidString, s3Client); + keyringCache_.put(uuidString, keyring); return CreateClientOutput.builder() .clientId(uuidString) .build(); diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java index 23dcc944..a1964085 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -36,8 +36,16 @@ public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { try { ResponseBytes resp = s3Client.getObjectAsBytes(builder -> { builder.bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap)); + .key(input.getKey()); + + // Add custom instruction file suffix if provided + if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { + builder.overrideConfiguration(config -> config + .putExecutionAttribute(S3EncryptionClient.CUSTOM_INSTRUCTION_FILE_SUFFIX, input.getInstructionFileSuffix()) + .applyMutation(c -> withAdditionalConfiguration(ecMap).accept(c))); + } else { + builder.overrideConfiguration(withAdditionalConfiguration(ecMap)); + } // Add range header if provided if (input.getRange() != null && !input.getRange().isEmpty()) { diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java new file mode 100644 index 00000000..dd376429 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -0,0 +1,185 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RawKeyring; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.model.ReEncryptInput; +import software.amazon.encryption.s3.model.ReEncryptOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.ReEncryptOperation; +import software.amazon.smithy.java.server.RequestContext; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +public class ReEncryptOperationImpl implements ReEncryptOperation { + private final Map clientCache_; + private final Map keyringCache_; + + public ReEncryptOperationImpl(Map clientCache, Map keyringCache) { + clientCache_ = clientCache; + keyringCache_ = keyringCache; + } + + @Override + public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { + try { + S3Client s3Client = clientCache_.get(input.getClientID()); + + // Ensure we have an S3EncryptionClient, not just a plain S3Client + if (!(s3Client instanceof S3EncryptionClient)) { + throw new IllegalStateException( + "Client " + input.getClientID() + " is not an S3EncryptionClient"); + } + + S3EncryptionClient s3EncryptionClient = (S3EncryptionClient) s3Client; + + // Create a new keyring from the provided newKeyMaterial + KeyMaterial newKeyMaterial = input.getNewKeyMaterial(); + if (newKeyMaterial == null) { + throw new IllegalStateException( + "newKeyMaterial is required for ReEncrypt operation"); + } + + RawKeyring newKeyring = createKeyringFromMaterial(newKeyMaterial); + + try { + // Build the ReEncryptInstructionFileRequest + ReEncryptInstructionFileRequest.Builder requestBuilder = + ReEncryptInstructionFileRequest.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .newKeyring(newKeyring); + + // Add optional instruction file suffix if provided + if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { + requestBuilder.instructionFileSuffix(input.getInstructionFileSuffix()); + } + + // Add optional enforceRotation if provided + if (input.isEnforceRotation() != null) { + requestBuilder.enforceRotation(input.isEnforceRotation()); + } + + ReEncryptInstructionFileRequest reEncryptRequest = requestBuilder.build(); + + // Perform the re-encryption + ReEncryptInstructionFileResponse response = + s3EncryptionClient.reEncryptInstructionFile(reEncryptRequest); + + // Build and return the output + return ReEncryptOutput.builder() + .bucket(response.bucket()) + .key(response.key()) + .instructionFileSuffix(response.instructionFileSuffix()) + .enforceRotation(response.enforceRotation()) + .build(); + + } catch (S3EncryptionClientException s3EncryptionClientException) { + // Modeled exceptions MUST be returned as such + StringWriter sw = new StringWriter(); + s3EncryptionClientException.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw S3EncryptionClientError.builder() + .message(stackTrace) + .build(); + } + } catch (Exception e) { + // Don't wrap modeled errors + if (e instanceof S3EncryptionClientError) { + throw e; + } + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } + + /** + * Creates a RawKeyring from KeyMaterial. + * The KeyMaterial should have exactly one of: aesKey, rsaKey, or kmsKeyId set. + */ + private RawKeyring createKeyringFromMaterial(KeyMaterial keyMaterial) { + try { + // Get materials description from KeyMaterial if provided + MaterialsDescription materialsDescription = null; + if (keyMaterial.getMaterialsDescription() != null && !keyMaterial.getMaterialsDescription().isEmpty()) { + MaterialsDescription.Builder builder = MaterialsDescription.builder(); + for (Map.Entry entry : keyMaterial.getMaterialsDescription().entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + materialsDescription = builder.build(); + } + + // Check for AES key + if (keyMaterial.getAesKey() != null) { + byte[] aesKeyBytes = new byte[keyMaterial.getAesKey().remaining()]; + keyMaterial.getAesKey().get(aesKeyBytes); + SecretKey secretKey = new SecretKeySpec(aesKeyBytes, "AES"); + + AesKeyring.Builder keyringBuilder = AesKeyring.builder() + .wrappingKey(secretKey); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + // Check for RSA key + if (keyMaterial.getRsaKey() != null) { + byte[] rsaKeyBytes = new byte[keyMaterial.getRsaKey().remaining()]; + keyMaterial.getRsaKey().get(rsaKeyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rsaKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + + // Derive the public key from the private key + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + PartialRsaKeyPair keyPair = PartialRsaKeyPair.builder() + .privateKey(privateKey) + .publicKey(publicKey) + .build(); + + RsaKeyring.Builder keyringBuilder = RsaKeyring.builder() + .wrappingKeyPair(keyPair); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + throw new IllegalStateException( + "KeyMaterial must have either aesKey or rsaKey set"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create keyring from KeyMaterial: " + e.getMessage(), e); + } + } +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java index d394b72b..ea6e3777 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -27,14 +27,16 @@ public void run() { // Obviously this can get messy in a real service. // Assume that the tests behave and don't induce weird race conditions. Map clientCache = new ConcurrentHashMap<>(); + Map keyringCache = new ConcurrentHashMap<>(); Server server = Server.builder() .endpoints(endpoint) .addService( S3ECTestServer.builder() - .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addCreateClientOperation(new CreateClientOperationImpl(clientCache, keyringCache)) .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) .build()) .build(); System.out.println("Starting server..."); diff --git a/test-server/model/client.smithy b/test-server/model/client.smithy index bba19b62..5772e8c2 100644 --- a/test-server/model/client.smithy +++ b/test-server/model/client.smithy @@ -25,7 +25,16 @@ structure CreateClientOutput { structure KeyMaterial { rsaKey: Blob, aesKey: Blob, - kmsKeyId: String + kmsKeyId: String, + /// Optional materials description for keyring differentiation + /// Used to distinguish between different key materials for rotation enforcement + materialsDescription: MaterialsDescriptionMap +} + +/// Map of materials description key-value pairs +map MaterialsDescriptionMap { + key: String, + value: String } enum CommitmentPolicy { diff --git a/test-server/model/object.smithy b/test-server/model/object.smithy index 3ff1cf3c..93e78370 100644 --- a/test-server/model/object.smithy +++ b/test-server/model/object.smithy @@ -21,6 +21,7 @@ resource Object { } read: GetObject put: PutObject + operations: [ReEncrypt] } @idempotent @@ -35,6 +36,14 @@ operation PutObject { @required $key + /// Encryption context/materials description to use for this operation. + /// For most SDKs (Java, .NET, etc.), materials description is embedded in the keyring/materials + /// during client creation and this parameter is typically empty/unused. + /// + /// For C++ SDK: Materials description MUST be passed per-operation via this parameter + /// because the C++ SDK's EncryptionMaterials constructor does not accept materials description. + /// Instead, GetObject/PutObject operations accept a contextMap parameter that becomes the + /// materials description. @httpHeader("Content-Metadata") $metadata @@ -72,7 +81,14 @@ operation GetObject { @required $key - /// Should probably be renamed to be EC specific + /// Encryption context/materials description to use for this operation. + /// For most SDKs (Java, .NET, etc.), materials description is embedded in the keyring/materials + /// during client creation and this parameter is typically empty/unused. + /// + /// For C++ SDK: Materials description MUST be passed per-operation via this parameter + /// because the C++ SDK's EncryptionMaterials constructor does not accept materials description. + /// Instead, GetObject/PutObject operations accept a contextMap parameter that becomes the + /// materials description. @httpHeader("Content-Metadata") $metadata @@ -84,7 +100,12 @@ operation GetObject { @httpHeader("Range") @notProperty range: String - } + + /// Custom instruction file suffix to use when reading instruction files + @httpHeader("InstructionFileSuffix") + @notProperty + instructionFileSuffix: String + } output := for Object { @httpHeader("Content-Metadata") @@ -97,8 +118,7 @@ operation GetObject { } } -@readonly -@http(method: "GET", uri: "/object/{bucket}/{key}") +@http(method: "POST", uri: "/object/{bucket}/{key}/reencrypt") operation ReEncrypt { input := for Object { @httpLabel @@ -108,30 +128,41 @@ operation ReEncrypt { @httpLabel @required $key - - /// Should probably be renamed to be EC specific - @httpHeader("Content-Metadata") - $metadata @httpHeader("ClientID") @required @notProperty clientID: String - /// Custom instruction file suffix + /// New key material to use for re-encryption + @httpPayload + @required + @notProperty + newKeyMaterial: KeyMaterial + + /// Custom instruction file suffix for RSA keyring re-encryption @httpHeader("InstructionFileSuffix") @notProperty instructionFileSuffix: String - } - output := for Object { - @httpHeader("Content-Metadata") + /// Whether to enforce rotation by verifying the key has changed + @httpHeader("EnforceRotation") + @notProperty + enforceRotation: Boolean + } + + output := { @required - $metadata + bucket: String @required - @httpPayload - $body + key: String + + @notProperty + instructionFileSuffix: String + + @notProperty + enforceRotation: Boolean } } 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 dcf683b6..847fa9e3 100644 --- a/test-server/php-v2-transition-server/src/get_object.php +++ b/test-server/php-v2-transition-server/src/get_object.php @@ -20,6 +20,9 @@ function handleGetObject($params) $metadata = $_SERVER['HTTP_CONTENT_METADATA'] ?? ''; $encryptionContext = metadataStringToMap($metadata); + // Get custom instruction file suffix if provided + $instructionFileSuffix = $_SERVER['HTTP_INSTRUCTIONFILESUFFIX'] ?? null; + // Extract bucket and key from URL parameters $bucket = $params['bucket'] ?? null; $key = $params['key'] ?? null; @@ -44,14 +47,21 @@ function handleGetObject($params) // Start output buffering before the AWS call to capture any unwanted output ob_start(); - $result = $s3ec->getObject([ + $getObjectParams = [ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, '@CommitmentPolicy' => $commitmentPolicy, 'Bucket' => $bucket, 'Key' => $key, - ]); + ]; + + // Add custom instruction file suffix if provided + if (!is_null($instructionFileSuffix) && !empty($instructionFileSuffix)) { + $getObjectParams['@InstructionFileSuffix'] = $instructionFileSuffix; + } + + $result = $s3ec->getObject($getObjectParams); // Capture and discard any unwanted output from AWS SDK $unwantedOutput = ob_get_clean(); diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php index 6fb28551..25a88a02 100644 --- a/test-server/php-v3-server/src/get_object.php +++ b/test-server/php-v3-server/src/get_object.php @@ -20,6 +20,9 @@ function handleGetObject($params) $metadata = $_SERVER['HTTP_CONTENT_METADATA'] ?? ''; $encryptionContext = metadataStringToMap($metadata); + // Get custom instruction file suffix if provided + $instructionFileSuffix = $_SERVER['HTTP_INSTRUCTIONFILESUFFIX'] ?? null; + // Extract bucket and key from URL parameters $bucket = $params['bucket'] ?? null; $key = $params['key'] ?? null; @@ -44,14 +47,21 @@ function handleGetObject($params) // Start output buffering before the AWS call to capture any unwanted output ob_start(); - $result = $s3ec->getObject([ + $getObjectParams = [ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, '@CommitmentPolicy' => $commitmentPolicy, 'Bucket' => $bucket, 'Key' => $key, - ]); + ]; + + // Add custom instruction file suffix if provided + if (!is_null($instructionFileSuffix) && !empty($instructionFileSuffix)) { + $getObjectParams['@InstructionFileSuffix'] = $instructionFileSuffix; + } + + $result = $s3ec->getObject($getObjectParams); // Capture and discard any unwanted output from AWS SDK $unwantedOutput = ob_get_clean(); diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb index 5a39e2ea..96e55c8a 100644 --- a/test-server/ruby-v2-server/app.rb +++ b/test-server/ruby-v2-server/app.rb @@ -176,8 +176,15 @@ def initialize key: key } - # Add encryption context if present - get_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + # Add custom instruction file suffix if present + instruction_file_suffix = request.env['HTTP_INSTRUCTIONFILESUFFIX'] + if instruction_file_suffix && !instruction_file_suffix.empty? + get_params[:envelope_location] = :instruction_file + get_params[:instruction_file_suffix] = instruction_file_suffix + S3ECLogger.debug("GET_ENDPOINT [#{@request_id}]: Using custom instruction file suffix: #{instruction_file_suffix}") + elsif !encryption_context.empty? + get_params[:kms_encryption_context] = encryption_context + end # Log S3 operation S3ECLogger.log_s3_operation('get', bucket, key, encryption_context, "ClientID: #{client_id}") diff --git a/test-server/ruby-v3-server/app.rb b/test-server/ruby-v3-server/app.rb index 8ebceac0..80ac972f 100644 --- a/test-server/ruby-v3-server/app.rb +++ b/test-server/ruby-v3-server/app.rb @@ -176,8 +176,15 @@ def initialize key: key } - # Add encryption context if present - get_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + # Add custom instruction file suffix if present + instruction_file_suffix = request.env['HTTP_INSTRUCTIONFILESUFFIX'] + if instruction_file_suffix && !instruction_file_suffix.empty? + get_params[:envelope_location] = :instruction_file + get_params[:instruction_file_suffix] = instruction_file_suffix + S3ECLogger.debug("GET_ENDPOINT [#{@request_id}]: Using custom instruction file suffix: #{instruction_file_suffix}") + elsif !encryption_context.empty? + get_params[:kms_encryption_context] = encryption_context + end # Log S3 operation S3ECLogger.log_s3_operation('get', bucket, key, encryption_context, "ClientID: #{client_id}") From c52e9854c38c8829440090fd5975e54a97cf20fe Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Tue, 2 Dec 2025 13:48:20 -0800 Subject: [PATCH 37/46] update Ranged Gets and added more mutation tests for commitment --- .../amazon/encryption/s3/RangedGetTests.java | 721 +++++++++++++++--- .../amazon/encryption/s3/TestUtils.java | 15 + 2 files changed, 651 insertions(+), 85 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java index bf9219ab..13118c7a 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -51,6 +51,7 @@ 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.GetObjectInput; import software.amazon.encryption.s3.model.GetObjectOutput; import software.amazon.encryption.s3.model.KeyMaterial; @@ -98,13 +99,23 @@ * * Middle (100 bytes centered in file) * * Whole file (all bytes) * * Auth tag only (last 16 bytes for authenticated algorithms) + * - Storage Mode (KC-GCM only): + * * Object Metadata Storage (all metadata in object, no instruction file) + * * Instruction File Storage (c/d/i in metadata, x-amz-3/w/m/t in instruction file) * - Commitment State (KC-GCM only): - * * Valid (original and good-copy) - * * Commitment duplicated - left in metadata added to instruction file - * * No commitment - removed from metadata, only in instruction file - * * Mutated commitment - bit flipped in x-amz-c value - * * Mutated commitment - bit flipped in x-amz-d value - * * Mutated commitment - bit flipped in x-amz-i value + * * Valid - Object Metadata Storage (original and good-copy) + * * Valid - Instruction File Storage (original and good-copy) + * * Corrupted - Object Metadata Storage: + * - Mutated c/d/i: bit flipped in metadata values + * - Invalid c length: c < 28 bytes in metadata + * - Invalid c length: c > 28 bytes in metadata + * * Corrupted - Instruction File Storage: + * - Commitment duplicated: c/d/i in instruction file (already in metadata) + * - Commitment removed: c/d/i removed from metadata + * - Mutated c/d/i in metadata: bit flipped + * - Mutated c/d/i in instruction file: bit flipped + * - Invalid c length: c < 28 bytes in metadata + * - Invalid c length: c > 28 bytes in metadata * * EXPECTED RESULTS: * - Positive: Ranged gets on valid CBC, GCM, KC-GCM objects return correct partial content @@ -146,13 +157,33 @@ class EncryptTests { Collections.synchronizedList(new ArrayList<>()); private static final List gcmObjects = Collections.synchronizedList(new ArrayList<>()); - private static final List kcGcmObjects = + // KC-GCM with Object Metadata Storage (all metadata in object) + private static final List kcGcmObjectsMetadata = Collections.synchronizedList(new ArrayList<>()); - private static final List mutatedCObjects = + // KC-GCM with Instruction File Storage (c/d/i in metadata, rest in instruction file) + private static final List kcGcmObjectsInstruction = Collections.synchronizedList(new ArrayList<>()); - private static final List mutatedDObjects = + // Corruption test lists for metadata storage mode + private static final List mutatedCObjectsMetadata = Collections.synchronizedList(new ArrayList<>()); - private static final List mutatedIObjects = + private static final List mutatedDObjectsMetadata = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedIObjectsMetadata = + Collections.synchronizedList(new ArrayList<>()); + private static final List invalidDLengthShortMetadata = + Collections.synchronizedList(new ArrayList<>()); + private static final List invalidDLengthLongMetadata = + Collections.synchronizedList(new ArrayList<>()); + // Corruption test lists for instruction file storage mode + private static final List mutatedCObjectsInstruction = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedDObjectsInstruction = + Collections.synchronizedList(new ArrayList<>()); + private static final List mutatedIObjectsInstruction = + Collections.synchronizedList(new ArrayList<>()); + private static final List invalidDLengthShortInstruction = + Collections.synchronizedList(new ArrayList<>()); + private static final List invalidDLengthLongInstruction = Collections.synchronizedList(new ArrayList<>()); private static KeyMaterial RSA_KEY; @@ -188,20 +219,52 @@ static List getGcmObjects() { return new ArrayList<>(gcmObjects); } - static List getKcGcmObjects() { - return new ArrayList<>(kcGcmObjects); + static List getKcGcmObjectsMetadata() { + return new ArrayList<>(kcGcmObjectsMetadata); + } + + static List getKcGcmObjectsInstruction() { + return new ArrayList<>(kcGcmObjectsInstruction); + } + + static List getMutatedCObjectsMetadata() { + return new ArrayList<>(mutatedCObjectsMetadata); + } + + static List getMutatedDObjectsMetadata() { + return new ArrayList<>(mutatedDObjectsMetadata); + } + + static List getMutatedIObjectsMetadata() { + return new ArrayList<>(mutatedIObjectsMetadata); + } + + static List getInvalidDLengthShortMetadata() { + return new ArrayList<>(invalidDLengthShortMetadata); + } + + static List getInvalidDLengthLongMetadata() { + return new ArrayList<>(invalidDLengthLongMetadata); } - static List getMutatedCObjects() { - return new ArrayList<>(mutatedCObjects); + static List getMutatedCObjectsInstruction() { + return new ArrayList<>(mutatedCObjectsInstruction); } - static List getMutatedDObjects() { - return new ArrayList<>(mutatedDObjects); + static List getMutatedDObjectsInstruction() { + return new ArrayList<>(mutatedDObjectsInstruction); } - static List getMutatedIObjects() { - return new ArrayList<>(mutatedIObjects); + static List getMutatedIObjectsInstruction() { + return new ArrayList<>(mutatedIObjectsInstruction); + } + + static List getInvalidDLengthShortInstruction() { + return new ArrayList<>(invalidDLengthShortInstruction); + } + + static List getInvalidDLengthLongInstruction() { + return new ArrayList<>(invalidDLengthLongInstruction); } static KeyMaterial getKmsKeyArn() { @@ -229,6 +292,12 @@ public static Stream improvedClientsForKCGCM() { return improvedClientsForTest(); } + public static Stream improvedClientsCanPutKMSWithInstructionFile() { + return improvedClientsForTest() + .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) + .filter(target -> !KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())); + } + @org.junit.jupiter.api.Test void encrypt_cbc_for_ranged_gets() { // Use old V1 client for CBC encryption (legacy algorithm) @@ -272,9 +341,9 @@ void encrypt_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { ); } - @ParameterizedTest(name = "{0}: Encrypt KC-GCM for ranged get testing") + @ParameterizedTest(name = "{0}: Encrypt KC-GCM with Object Metadata Storage for ranged get testing") @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsForKCGCM") - void encrypt_kc_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { + void encrypt_kc_gcm_metadata_for_ranged_gets(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -286,8 +355,34 @@ void encrypt_kc_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { TestUtils.Encrypt( client, S3ECId, - appendTestSuffix(sharedObjectKeyBase + "-kc-gcm-" + language.getLanguageName()), - kcGcmObjects, + appendTestSuffix(sharedObjectKeyBase + "-kc-gcm-metadata-" + language.getLanguageName()), + kcGcmObjectsMetadata, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + + @ParameterizedTest(name = "{0}: Encrypt KC-GCM with Instruction file Storage for ranged get testing") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") + void encrypt_kc_gcm_instruction_file_for_ranged_gets(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBase + "-kc-gcm-instruction-java" + language.getLanguageName()), + kcGcmObjectsInstruction, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); } @@ -310,11 +405,14 @@ static int flipRandomBit(byte[] data) { /** * Creates corrupted copies of KC-GCM objects for failure testing + * Handles both object metadata storage and instruction file storage modes */ static void createCorruptedCopies() throws Exception { try (S3Client ptS3Client = S3Client.create()) { - for (String objectKey : kcGcmObjects) { - // Get the encrypted object + ObjectMapper mapper = new ObjectMapper(); + + // Process metadata storage mode objects (all V3 keys in metadata, no instruction file) + for (String objectKey : kcGcmObjectsMetadata) { ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder .bucket(TestUtils.BUCKET) .key(objectKey) @@ -331,38 +429,120 @@ static void createCorruptedCopies() throws Exception { String commitD = objectMetadata.get("x-amz-d"); String commitI = objectMetadata.get("x-amz-i"); - // Create copies with no commitment in metadata - Map noCommitMetadata = objectMetadata.entrySet().stream() - .filter(e -> !e.getKey().equals("x-amz-c") && !e.getKey().equals("x-amz-d") && !e.getKey().equals("x-amz-i")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + // Create mutated commitment copies in metadata + if (commitC != null) { + byte[] commitCBytes = Base64.getDecoder().decode(commitC); + int bitPos = flipRandomBit(commitCBytes); + String mutatedC = Base64.getEncoder().encodeToString(commitCBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-c", mutatedC); + String mutatedKey = objectKey + "-bad-mutated-c-bit-" + bitPos; + putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); + mutatedCObjectsMetadata.add(mutatedKey); + } + + if (commitD != null) { + byte[] commitDBytes = Base64.getDecoder().decode(commitD); + int bitPos = flipRandomBit(commitDBytes); + String mutatedD = Base64.getEncoder().encodeToString(commitDBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-d", mutatedD); + String mutatedKey = objectKey + "-bad-mutated-d-bit-" + bitPos; + putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); + mutatedDObjectsMetadata.add(mutatedKey); + } - // Create instruction file JSON with commitment - ObjectMapper mapper = new ObjectMapper(); - Map instructionFileMap = new java.util.HashMap<>(); - instructionFileMap.put("x-amz-c", commitC); - instructionFileMap.put("x-amz-d", commitD); - instructionFileMap.put("x-amz-i", commitI); - String instructionFileJson = mapper.writeValueAsString(instructionFileMap); + if (commitI != null) { + byte[] commitIBytes = Base64.getDecoder().decode(commitI); + int bitPos = flipRandomBit(commitIBytes); + String mutatedI = Base64.getEncoder().encodeToString(commitIBytes); + Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); + mutatedMetadata.put("x-amz-i", mutatedI); + String mutatedKey = objectKey + "-bad-mutated-i-bit-" + bitPos; + putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); + mutatedIObjectsMetadata.add(mutatedKey); + } - // No commitment - removed from metadata, added to instruction file + // Create invalid D length copies (metadata storage) + if (commitD != null) { + byte[] commitDBytes = Base64.getDecoder().decode(commitD); + + // Short D (< 28 bytes) - truncate to 20 bytes + int shortLength = Math.min(20, commitDBytes.length); + byte[] shortDBytes = new byte[shortLength]; + System.arraycopy(commitDBytes, 0, shortDBytes, 0, shortLength); + String shortD = Base64.getEncoder().encodeToString(shortDBytes); + Map shortDMetadata = new java.util.HashMap<>(objectMetadata); + shortDMetadata.put("x-amz-d", shortD); + String shortDKey = objectKey + "-bad-invalid-d-length-short"; + putObjectWithMetadata(ptS3Client, shortDKey, objectData, shortDMetadata); + invalidDLengthShortMetadata.add(shortDKey); + + // Long D (> 28 bytes) - extend to 40 bytes + byte[] longDBytes = new byte[40]; + System.arraycopy(commitDBytes, 0, longDBytes, 0, commitDBytes.length); + // Fill remaining bytes with zeros + for (int i = commitDBytes.length; i < 40; i++) { + longDBytes[i] = 0; + } + String longD = Base64.getEncoder().encodeToString(longDBytes); + Map longDMetadata = new java.util.HashMap<>(objectMetadata); + longDMetadata.put("x-amz-d", longD); + String longDKey = objectKey + "-bad-invalid-d-length-long"; + putObjectWithMetadata(ptS3Client, longDKey, objectData, longDMetadata); + invalidDLengthLongMetadata.add(longDKey); + } + } + + // Process instruction file storage mode objects (c/d/i in metadata, x-amz-3/w/m/t in instruction file) + for (String objectKey : kcGcmObjectsInstruction) { + // Get the encrypted object + ResponseBytes encryptedObject = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + byte[] objectData = encryptedObject.asByteArray(); + Map objectMetadata = encryptedObject.response().metadata(); + + // Get the instruction file + ResponseBytes instructionObject = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(TestUtils.BUCKET) + .key(objectKey + ".instruction") + .build()); + + String originalInstructionFileJson = new String(instructionObject.asByteArray(), StandardCharsets.UTF_8); + + // Create good copy (both object and instruction file) putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-commitment-add-to-instruction", + objectKey + "-good-copy", objectData, objectMetadata, - instructionFileJson + originalInstructionFileJson ); - // No commitment - removed from metadata, only in instruction file + // Extract commitment values from metadata + String commitC = objectMetadata.get("x-amz-c"); + String commitD = objectMetadata.get("x-amz-d"); + String commitI = objectMetadata.get("x-amz-i"); + + // Corruption: Add c/d/i to instruction file (duplication - should fail) + Map corruptedInstructionMap = mapper.readValue(originalInstructionFileJson, Map.class); + corruptedInstructionMap.put("x-amz-c", commitC); + corruptedInstructionMap.put("x-amz-d", commitD); + corruptedInstructionMap.put("x-amz-i", commitI); + String corruptedInstructionJson = mapper.writeValueAsString(corruptedInstructionMap); + putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-no-commitment-only-instruction", + objectKey + "-bad-commitment-in-instruction", objectData, - noCommitMetadata, - instructionFileJson + objectMetadata, + corruptedInstructionJson ); - // Create mutated commitment copies + // Create mutated commitment copies in metadata if (commitC != null) { byte[] commitCBytes = Base64.getDecoder().decode(commitC); int bitPos = flipRandomBit(commitCBytes); @@ -370,13 +550,8 @@ static void createCorruptedCopies() throws Exception { Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-c", mutatedC); String mutatedKey = objectKey + "-bad-mutated-c-bit-" + bitPos; - putObjectWithMetadata( - ptS3Client, - mutatedKey, - objectData, - mutatedMetadata - ); - mutatedCObjects.add(mutatedKey); + putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); + mutatedCObjectsInstruction.add(mutatedKey); } if (commitD != null) { @@ -386,13 +561,8 @@ static void createCorruptedCopies() throws Exception { Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-d", mutatedD); String mutatedKey = objectKey + "-bad-mutated-d-bit-" + bitPos; - putObjectWithMetadata( - ptS3Client, - mutatedKey, - objectData, - mutatedMetadata - ); - mutatedDObjects.add(mutatedKey); + putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); + mutatedDObjectsInstruction.add(mutatedKey); } if (commitI != null) { @@ -402,13 +572,38 @@ static void createCorruptedCopies() throws Exception { Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-i", mutatedI); String mutatedKey = objectKey + "-bad-mutated-i-bit-" + bitPos; - putObjectWithMetadata( - ptS3Client, - mutatedKey, - objectData, - mutatedMetadata - ); - mutatedIObjects.add(mutatedKey); + putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); + mutatedIObjectsInstruction.add(mutatedKey); + } + + // Create invalid D length copies (instruction file storage) + if (commitD != null) { + byte[] commitDBytes = Base64.getDecoder().decode(commitD); + + // Short D (< 28 bytes) - truncate to 20 bytes + int shortLength = Math.min(20, commitDBytes.length); + byte[] shortDBytes = new byte[shortLength]; + System.arraycopy(commitDBytes, 0, shortDBytes, 0, shortLength); + String shortD = Base64.getEncoder().encodeToString(shortDBytes); + Map shortDMetadata = new java.util.HashMap<>(objectMetadata); + shortDMetadata.put("x-amz-d", shortD); + String shortDKey = objectKey + "-bad-invalid-d-length-short"; + putObjectWithInstructionFile(ptS3Client, shortDKey, objectData, shortDMetadata, originalInstructionFileJson); + invalidDLengthShortInstruction.add(shortDKey); + + // Long D (> 28 bytes) - extend to 40 bytes + byte[] longDBytes = new byte[40]; + System.arraycopy(commitDBytes, 0, longDBytes, 0, commitDBytes.length); + // Fill remaining bytes with zeros + for (int i = commitDBytes.length; i < 40; i++) { + longDBytes[i] = 0; + } + String longD = Base64.getEncoder().encodeToString(longDBytes); + Map longDMetadata = new java.util.HashMap<>(objectMetadata); + longDMetadata.put("x-amz-d", longD); + String longDKey = objectKey + "-bad-invalid-d-length-long"; + putObjectWithInstructionFile(ptS3Client, longDKey, objectData, longDMetadata, originalInstructionFileJson); + invalidDLengthLongInstruction.add(longDKey); } } } @@ -473,9 +668,13 @@ class RangedGetTestsNested { private static List cbcObjects; private static List gcmObjects; private static List kcGcmObjects; + private static List kcGcmObjectsInstruction; private static List mutatedCObjects; private static List mutatedDObjects; private static List mutatedIObjects; + private static List mutatedCObjectsInstruction; + private static List mutatedDObjectsInstruction; + private static List mutatedIObjectsInstruction; private static KeyMaterial kmsKeyArn; private static KeyMaterial RSA_KEY; private static KeyMaterial AES_KEY; @@ -488,10 +687,17 @@ static void setup() throws InterruptedException { // Import encrypted objects from the encrypt phase cbcObjects = EncryptTests.getCbcObjects(); gcmObjects = EncryptTests.getGcmObjects(); - kcGcmObjects = EncryptTests.getKcGcmObjects(); - mutatedCObjects = EncryptTests.getMutatedCObjects(); - mutatedDObjects = EncryptTests.getMutatedDObjects(); - mutatedIObjects = EncryptTests.getMutatedIObjects(); + // Import KC-GCM objects for both storage modes + kcGcmObjects = EncryptTests.getKcGcmObjectsMetadata(); + kcGcmObjectsInstruction = EncryptTests.getKcGcmObjectsInstruction(); + // Import corrupted objects for metadata storage mode + mutatedCObjects = EncryptTests.getMutatedCObjectsMetadata(); + mutatedDObjects = EncryptTests.getMutatedDObjectsMetadata(); + mutatedIObjects = EncryptTests.getMutatedIObjectsMetadata(); + // Import corrupted objects for instruction file storage mode + mutatedCObjectsInstruction = EncryptTests.getMutatedCObjectsInstruction(); + mutatedDObjectsInstruction = EncryptTests.getMutatedDObjectsInstruction(); + mutatedIObjectsInstruction = EncryptTests.getMutatedIObjectsInstruction(); kmsKeyArn = EncryptTests.getKmsKeyArn(); RSA_KEY = EncryptTests.getRsaKey(); AES_KEY = EncryptTests.getAesKey(); @@ -999,11 +1205,11 @@ void ranged_get_kc_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget langua } } - // KC-GCM Ranged Get Tests - Failure Cases + // KC-GCM Instruction File Storage - Valid Object Tests - @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with no commitment - add to instruction") + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - start range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_no_commitment_add_to_instruction_fails(TestUtils.LanguageServerTarget language) { + void ranged_get_kc_gcm_instruction_start_succeeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1013,21 +1219,209 @@ void ranged_get_kc_gcm_no_commitment_add_to_instruction_fails(TestUtils.Language .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.RangedGet_fails( + TestUtils.RangedGet( client, S3ECId, - kcGcmObjects.stream() - .map(key -> key + "-bad-no-commitment-add-to-instruction") + kcGcmObjectsInstruction, + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjectsInstruction + .stream() + .map(key -> key + "-good-copy") .collect(Collectors.toList()), + 0, + 5, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - middle range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_middle_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjectsInstruction, 5, 10, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjectsInstruction + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - Include tag") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_tag_only_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjectsInstruction, + 10, + 1000, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + + TestUtils.RangedGet( + client, + S3ECId, + kcGcmObjectsInstruction + .stream() + .map(key -> key + "-good-copy") + .collect(Collectors.toList()), + 10, + 1000, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - end range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_end_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Test original objects + for (String objectKey : kcGcmObjectsInstruction) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // Test good-copy objects + for (String objectKey : kcGcmObjectsInstruction) { + String goodCopyKey = objectKey + "-good-copy"; + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(goodCopyKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + long rangeStart = Math.max(0, objectLength - 5); + long rangeEnd = objectLength - 1; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(goodCopyKey), + rangeStart, + rangeEnd, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + } + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - whole file") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Test original objects + for (String objectKey : kcGcmObjectsInstruction) { + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(objectKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // Test good-copy objects + for (String objectKey : kcGcmObjectsInstruction) { + String goodCopyKey = objectKey + "-good-copy"; + GetObjectOutput fullOutput = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(goodCopyKey) + .build()); + long objectLength = fullOutput.getBody().array().length; + + TestUtils.RangedGet( + client, + S3ECId, + java.util.Collections.singletonList(goodCopyKey), + 0, + objectLength - 1, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } } - @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with no commitment - only instruction") + // KC-GCM Ranged Get Tests - Failure Cases + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with commitment duplicated in instruction file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_no_commitment_only_instruction_fails(TestUtils.LanguageServerTarget language) { + void ranged_get_kc_gcm_instruction_commitment_in_instruction_fails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1037,11 +1431,12 @@ void ranged_get_kc_gcm_no_commitment_only_instruction_fails(TestUtils.LanguageSe .build()); String S3ECId = clientOutput.getClientId(); + // Test instruction file storage mode objects with c/d/i duplicated into instruction file TestUtils.RangedGet_fails( client, S3ECId, - kcGcmObjects.stream() - .map(key -> key + "-bad-no-commitment-only-instruction") + kcGcmObjectsInstruction.stream() + .map(key -> key + "-bad-commitment-in-instruction") .collect(Collectors.toList()), 5, 10, @@ -1061,8 +1456,6 @@ void ranged_get_kc_gcm_mutated_c_fails(TestUtils.LanguageServerTarget language) .build()); String S3ECId = clientOutput.getClientId(); - assertFalse(mutatedCObjects.isEmpty(), "Expected mutated C objects to be created but list is empty"); - TestUtils.RangedGet_fails( client, S3ECId, @@ -1084,9 +1477,7 @@ void ranged_get_kc_gcm_mutated_d_fails(TestUtils.LanguageServerTarget language) .build()) .build()); String S3ECId = clientOutput.getClientId(); - - assertFalse(mutatedDObjects.isEmpty(), "Expected mutated D objects to be created but list is empty"); - + TestUtils.RangedGet_fails( client, S3ECId, @@ -1108,13 +1499,173 @@ void ranged_get_kc_gcm_mutated_i_fails(TestUtils.LanguageServerTarget language) .build()) .build()); String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedIObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment C in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_mutated_c_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedCObjectsInstruction, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment D in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_mutated_d_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedDObjectsInstruction, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment I in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_mutated_i_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + mutatedIObjectsInstruction, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with invalid C length (too short) in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_metadata_invalid_c_length_short_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); - assertFalse(mutatedIObjects.isEmpty(), "Expected mutated I objects to be created but list is empty"); + List invalidDLengthShortObjects = EncryptTests.getInvalidDLengthShortMetadata(); TestUtils.RangedGet_fails( client, S3ECId, - mutatedIObjects, + invalidDLengthShortObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with invalid D length (too long) in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_metadata_invalid_d_length_long_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + List invalidDLengthLongObjects = EncryptTests.getInvalidDLengthLongMetadata(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + invalidDLengthLongObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with invalid D length (too short) in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_invalid_d_length_short_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + List invalidDLengthShortObjects = EncryptTests.getInvalidDLengthShortInstruction(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + invalidDLengthShortObjects, + 5, + 10, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with invalid D length (too long) in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void ranged_get_kc_gcm_instruction_invalid_d_length_long_fails(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + List invalidDLengthLongObjects = EncryptTests.getInvalidDLengthLongInstruction(); + + TestUtils.RangedGet_fails( + client, + S3ECId, + invalidDLengthLongObjects, 5, 10, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY 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 dee9a266..52bf6794 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 @@ -747,6 +747,11 @@ public static void Decrypt_fails( String S3ECId, List crossLanguageObjects, EncryptionAlgorithm expectedEncryptionAlgorithm ) { + + if (crossLanguageObjects.isEmpty()) { + throw new AssertionError("There is nothing to decrypt"); + } + List successfulDecrypt = new ArrayList<>(); for (String objectKey : crossLanguageObjects) { try { @@ -783,6 +788,11 @@ public static void RangedGet( long rangeEnd, EncryptionAlgorithm expectedEncryptionAlgorithm ) { + + if (objectKeys.isEmpty()) { + throw new AssertionError("There is nothing to get"); + } + List failures = new ArrayList<>(); for (String objectKey : objectKeys) { try { @@ -844,6 +854,11 @@ public static void RangedGet_fails( long rangeEnd, EncryptionAlgorithm expectedEncryptionAlgorithm ) { + + if (objectKeys.isEmpty()) { + throw new AssertionError("There is nothing to get"); + } + List successfulGets = new ArrayList<>(); for (String objectKey : objectKeys) { try { From c01cb12cd0681e602863de04fcc4a1ed359c4feb Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 3 Dec 2025 16:59:01 -0800 Subject: [PATCH 38/46] fix to C for constant time equal --- test-server/cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index 9fc3de92..72161a32 160000 --- a/test-server/cpp-v2-transition-server/aws-sdk-cpp +++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 9fc3de9203e72e0aa843c43657ff3afefdbfdfa5 +Subproject commit 72161a327a201737bbe8cc8b86941fdbf6ad1d5c diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 9fc3de92..72161a32 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 9fc3de9203e72e0aa843c43657ff3afefdbfdfa5 +Subproject commit 72161a327a201737bbe8cc8b86941fdbf6ad1d5c From f70745f8807242055f0d8332512897ecf22e1ea6 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 3 Dec 2025 17:15:44 -0800 Subject: [PATCH 39/46] Some updates --- .github/workflows/test.yml | 10 --- .../s3/InstructionFileFailures.java | 42 ++++++------ .../amazon/encryption/s3/RangedGetTests.java | 68 +++++++++---------- .../amazon/encryption/s3/ReEncryptTests.java | 30 ++++---- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6b7a9d0..182b7f47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,16 +41,6 @@ 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: Optimize git for performance run: | git config --global fetch.parallel ${{ steps.cpu-count.outputs.count }} diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 6096d59e..395af191 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -56,8 +56,6 @@ * Coordination is achieved using a CountDownLatch that EncryptTests signals upon completion * and DecryptTests awaits before proceeding. * -* Tests are based on the exhaustive test matrix defined at: -* https://tiny.amazon.com/3xnzwczl/loopcloumicrpeyJ3 */ public class InstructionFileFailures { // Synchronization latch - released when encrypt phase completes @@ -154,7 +152,7 @@ public static Stream improvedClientsCanPutRawAESWithInstructionFile() @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM with instruction files") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") - void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget language) { + void encryptWithInstructionFilesKmsKcGcm(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -179,7 +177,7 @@ void encrypt_with_instruction_files_kms_kc_gcm(TestUtils.LanguageServerTarget la @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction files") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") - void encrypt_with_instruction_files_rsa_kc_gcm(TestUtils.LanguageServerTarget language) { + void encryptWithInstructionFilesRsaKcGcm(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -205,7 +203,7 @@ void encrypt_with_instruction_files_rsa_kc_gcm(TestUtils.LanguageServerTarget la @ParameterizedTest(name = "{0}: Encrypt AES KC-GCM with instruction files") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") - void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget language) { + void encryptWithInstructionFilesAesKcGcm(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -229,7 +227,7 @@ void encrypt_with_instruction_files_aes_kc_gcm(TestUtils.LanguageServerTarget la ); } - static void make_copies_to_verify_things() throws Exception { + static void makeCopiesToVerifyThings() throws Exception { // Create a plaintext S3 client to copy objects with instruction files try (S3Client ptS3Client = S3Client.create()) { List allCrossLanguageObjects = Stream.of( @@ -319,7 +317,7 @@ static void putObjectWithInstructionFile( @AfterAll static void signalEncryptionComplete() throws Exception { - make_copies_to_verify_things(); + makeCopiesToVerifyThings(); // Signal that all encryption tests have completed encryptPhaseComplete.countDown(); @@ -396,7 +394,7 @@ public static Stream clientsCanGetRawAESWithInstructionFile() { @ParameterizedTest(name = "{0}: Successfully decrypt KMS encrypted original and good-copy objects") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + void decryptKmsOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -427,7 +425,7 @@ void decrypt_kms_original_and_good_copy_objects_succeeds(TestUtils.LanguageServe @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is duplicated in metadata and instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + void decryptKmsWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -450,7 +448,7 @@ void decrypt_kms_with_duplicate_commitment_in_metadata_and_instruction_fails(Tes @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is only in instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + void decryptKmsWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -473,7 +471,7 @@ void decrypt_kms_with_commitment_only_in_instruction_file_fails(TestUtils.Langua @ParameterizedTest(name = "{0}: Fail to decrypt KMS duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptKmsWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -498,7 +496,7 @@ void decrypt_kms_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La @ParameterizedTest(name = "{0}: Fail to decrypt KMS instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") - void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptKmsWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -525,7 +523,7 @@ void decrypt_kms_with_instruction_file_commitment_fails_with_forbid_policy(TestU @ParameterizedTest(name = "{0}: Successfully decrypt RSA encrypted original and good-copy objects") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_rsa_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + void decryptRsaOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -556,7 +554,7 @@ void decrypt_rsa_original_and_good_copy_objects_succeeds(TestUtils.LanguageServe @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is duplicated in metadata and instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_rsa_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + void decryptRsaWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -579,7 +577,7 @@ void decrypt_rsa_with_duplicate_commitment_in_metadata_and_instruction_fails(Tes @ParameterizedTest(name = "{0}: Fail to decrypt RSA when commitment is only in instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_rsa_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + void decryptRsaWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -602,7 +600,7 @@ void decrypt_rsa_with_commitment_only_in_instruction_file_fails(TestUtils.Langua @ParameterizedTest(name = "{0}: Fail to decrypt RSA duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptRsaWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -627,7 +625,7 @@ void decrypt_rsa_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La @ParameterizedTest(name = "{0}: Fail to decrypt RSA instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptRsaWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -654,7 +652,7 @@ void decrypt_rsa_with_instruction_file_commitment_fails_with_forbid_policy(TestU @ParameterizedTest(name = "{0}: Successfully decrypt AES encrypted original and good-copy objects") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_aes_original_and_good_copy_objects_succeeds(TestUtils.LanguageServerTarget language) { + void decryptAesOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -685,7 +683,7 @@ void decrypt_aes_original_and_good_copy_objects_succeeds(TestUtils.LanguageServe @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is duplicated in metadata and instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_aes_with_duplicate_commitment_in_metadata_and_instruction_fails(TestUtils.LanguageServerTarget language) { + void decryptAesWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -708,7 +706,7 @@ void decrypt_aes_with_duplicate_commitment_in_metadata_and_instruction_fails(Tes @ParameterizedTest(name = "{0}: Fail to decrypt AES when commitment is only in instruction file") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_aes_with_commitment_only_in_instruction_file_fails(TestUtils.LanguageServerTarget language) { + void decryptAesWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -731,7 +729,7 @@ void decrypt_aes_with_commitment_only_in_instruction_file_fails(TestUtils.Langua @ParameterizedTest(name = "{0}: Fail to decrypt AES duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptAesWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -756,7 +754,7 @@ void decrypt_aes_with_duplicate_commitment_fails_with_forbid_policy(TestUtils.La @ParameterizedTest(name = "{0}: Fail to decrypt AES instruction file commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_aes_with_instruction_file_commitment_fails_with_forbid_policy(TestUtils.LanguageServerTarget language) { + void decryptAesWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java index 13118c7a..89610287 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -299,7 +299,7 @@ public static Stream improvedClientsCanPutKMSWithInstructionFile() { } @org.junit.jupiter.api.Test - void encrypt_cbc_for_ranged_gets() { + void encryptCbcForRangedGets() { // Use old V1 client for CBC encryption (legacy algorithm) // Only Java V1 client is available - no V1 test servers for other languages EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); @@ -321,7 +321,7 @@ void encrypt_cbc_for_ranged_gets() { @ParameterizedTest(name = "{0}: Encrypt GCM for ranged get testing") @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#transitionAndImprovedForGCM") - void encrypt_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { + void encryptGcmForRangedGets(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -343,7 +343,7 @@ void encrypt_gcm_for_ranged_gets(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Encrypt KC-GCM with Object Metadata Storage for ranged get testing") @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsForKCGCM") - void encrypt_kc_gcm_metadata_for_ranged_gets(TestUtils.LanguageServerTarget language) { + void encryptKcGcmMetadataForRangedGets(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -364,7 +364,7 @@ void encrypt_kc_gcm_metadata_for_ranged_gets(TestUtils.LanguageServerTarget lang @ParameterizedTest(name = "{0}: Encrypt KC-GCM with Instruction file Storage for ranged get testing") @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") - void encrypt_kc_gcm_instruction_file_for_ranged_gets(TestUtils.LanguageServerTarget language) { + void encryptKcGcmInstructionFileForRangedGets(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -729,7 +729,7 @@ public static Stream rangedGetCBCSupportedClients() { @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - start range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") - void ranged_get_cbc_start_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetCbcStartSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -754,7 +754,7 @@ void ranged_get_cbc_start_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - end range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") - void ranged_get_cbc_end_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetCbcEndSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -791,7 +791,7 @@ void ranged_get_cbc_end_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - middle range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") - void ranged_get_cbc_middle_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetCbcMiddleSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -816,7 +816,7 @@ void ranged_get_cbc_middle_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get CBC objects - whole file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetCBCSupportedClients") - void ranged_get_cbc_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetCbcWholeFileSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -853,7 +853,7 @@ void ranged_get_cbc_whole_file_succeeds(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - start range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_gcm_start_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetGcmStartSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -877,7 +877,7 @@ void ranged_get_gcm_start_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - end range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_gcm_end_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetGcmEndSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -913,7 +913,7 @@ void ranged_get_gcm_end_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - middle range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_gcm_middle_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetGcmMiddleSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -937,7 +937,7 @@ void ranged_get_gcm_middle_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - whole file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetGcmWholeFileSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -971,7 +971,7 @@ void ranged_get_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Successfully ranged get GCM objects - Include tag") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_gcm_tag_only_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetGcmTagOnlySucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -997,7 +997,7 @@ void ranged_get_gcm_tag_only_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - start range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_start_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmStartSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1031,7 +1031,7 @@ void ranged_get_kc_gcm_start_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - middle range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_middle_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMiddleSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1065,7 +1065,7 @@ void ranged_get_kc_gcm_middle_succeeds(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - Include tag") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_tag_only_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmTagOnlySucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1099,7 +1099,7 @@ void ranged_get_kc_gcm_tag_only_succeeds(TestUtils.LanguageServerTarget language @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - end range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_end_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmEndSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1155,7 +1155,7 @@ void ranged_get_kc_gcm_end_succeeds(TestUtils.LanguageServerTarget language) { @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM objects - whole file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmWholeFileSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1209,7 +1209,7 @@ void ranged_get_kc_gcm_whole_file_succeeds(TestUtils.LanguageServerTarget langua @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - start range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_start_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionStartSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1243,7 +1243,7 @@ void ranged_get_kc_gcm_instruction_start_succeeds(TestUtils.LanguageServerTarget @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - middle range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_middle_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionMiddleSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1277,7 +1277,7 @@ void ranged_get_kc_gcm_instruction_middle_succeeds(TestUtils.LanguageServerTarge @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - Include tag") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_tag_only_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionTagOnlySucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1311,7 +1311,7 @@ void ranged_get_kc_gcm_instruction_tag_only_succeeds(TestUtils.LanguageServerTar @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - end range") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_end_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionEndSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1367,7 +1367,7 @@ void ranged_get_kc_gcm_instruction_end_succeeds(TestUtils.LanguageServerTarget l @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - whole file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_whole_file_succeeds(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionWholeFileSucceeds(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1421,7 +1421,7 @@ void ranged_get_kc_gcm_instruction_whole_file_succeeds(TestUtils.LanguageServerT @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with commitment duplicated in instruction file") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_commitment_in_instruction_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionCommitmentInInstructionFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1446,7 +1446,7 @@ void ranged_get_kc_gcm_instruction_commitment_in_instruction_fails(TestUtils.Lan @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment C") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_mutated_c_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMutatedCFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1468,7 +1468,7 @@ void ranged_get_kc_gcm_mutated_c_fails(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment D") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_mutated_d_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMutatedDFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1490,7 +1490,7 @@ void ranged_get_kc_gcm_mutated_d_fails(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with mutated commitment I") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_mutated_i_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMutatedIFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1512,7 +1512,7 @@ void ranged_get_kc_gcm_mutated_i_fails(TestUtils.LanguageServerTarget language) @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment C in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_mutated_c_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionMutatedCFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1534,7 +1534,7 @@ void ranged_get_kc_gcm_instruction_mutated_c_fails(TestUtils.LanguageServerTarge @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment D in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_mutated_d_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionMutatedDFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1556,7 +1556,7 @@ void ranged_get_kc_gcm_instruction_mutated_d_fails(TestUtils.LanguageServerTarge @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with mutated commitment I in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_mutated_i_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionMutatedIFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1578,7 +1578,7 @@ void ranged_get_kc_gcm_instruction_mutated_i_fails(TestUtils.LanguageServerTarge @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with invalid C length (too short) in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_metadata_invalid_c_length_short_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMetadataInvalidCLengthShortFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1602,7 +1602,7 @@ void ranged_get_kc_gcm_metadata_invalid_c_length_short_fails(TestUtils.LanguageS @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM with invalid D length (too long) in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_metadata_invalid_d_length_long_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmMetadataInvalidDLengthLongFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1626,7 +1626,7 @@ void ranged_get_kc_gcm_metadata_invalid_d_length_long_fails(TestUtils.LanguageSe @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with invalid D length (too short) in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_invalid_d_length_short_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionInvalidDLengthShortFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -1650,7 +1650,7 @@ void ranged_get_kc_gcm_instruction_invalid_d_length_short_fails(TestUtils.Langua @ParameterizedTest(name = "{0}: Fail to ranged get KC-GCM Instruction File with invalid D length (too long) in metadata") @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") - void ranged_get_kc_gcm_instruction_invalid_d_length_long_fails(TestUtils.LanguageServerTarget language) { + void rangedGetKcGcmInstructionInvalidDLengthLongFails(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java index 1fd46e21..ef32b919 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java @@ -220,7 +220,7 @@ public static Stream improvedClientsCanPutRawAESWithInstructionFile() @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => AES re-encryption") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") - void encrypt_aes_for_aes_to_aes_reencrypt(TestUtils.LanguageServerTarget language) { + void encryptAesForAesToAesReencrypt(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -239,7 +239,7 @@ void encrypt_aes_for_aes_to_aes_reencrypt(TestUtils.LanguageServerTarget languag @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => RSA custom suffix re-encryption") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") - void encrypt_aes_for_aes_to_rsa_custom_reencrypt(TestUtils.LanguageServerTarget language) { + void encryptAesForAesToRsaCustomReencrypt(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -258,7 +258,7 @@ void encrypt_aes_for_aes_to_rsa_custom_reencrypt(TestUtils.LanguageServerTarget @ParameterizedTest(name = "{0}: Encrypt AES objects for AES => RSA default suffix re-encryption") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawAESWithInstructionFile") - void encrypt_aes_for_aes_to_rsa_default_reencrypt(TestUtils.LanguageServerTarget language) { + void encryptAesForAesToRsaDefaultReencrypt(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -277,7 +277,7 @@ void encrypt_aes_for_aes_to_rsa_default_reencrypt(TestUtils.LanguageServerTarget @ParameterizedTest(name = "{0}: Encrypt RSA objects for RSA => RSA re-encryption") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") - void encrypt_rsa_for_rsa_to_rsa_reencrypt(TestUtils.LanguageServerTarget language) { + void encryptRsaForRsaToRsaReencrypt(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -296,7 +296,7 @@ void encrypt_rsa_for_rsa_to_rsa_reencrypt(TestUtils.LanguageServerTarget languag @ParameterizedTest(name = "{0}: Encrypt RSA objects for RSA => AES default suffix re-encryption") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") - void encrypt_rsa_for_rsa_to_aes_default_reencrypt(TestUtils.LanguageServerTarget language) { + void encryptRsaForRsaToAesDefaultReencrypt(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -346,7 +346,7 @@ public static Stream reencryptSupportedClients() { @ParameterizedTest(name = "{0}: ReEncrypt AES => AES instruction file") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") - void reencrypt_aes_to_aes_instruction_file(TestUtils.LanguageServerTarget language) { + void reencryptAesToAesInstructionFile(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); for (int i = 0; i < kcGcmObjectsAesToAes.size(); i++) { @@ -372,7 +372,7 @@ void reencrypt_aes_to_aes_instruction_file(TestUtils.LanguageServerTarget langua @ParameterizedTest(name = "{0}: ReEncrypt RSA => RSA instruction file") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") - void reencrypt_rsa_to_rsa_instruction_file(TestUtils.LanguageServerTarget language) { + void reencryptRsaToRsaInstructionFile(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); for (int i = 0; i < kcGcmObjectsRsaToRsa.size(); i++) { @@ -398,7 +398,7 @@ void reencrypt_rsa_to_rsa_instruction_file(TestUtils.LanguageServerTarget langua @ParameterizedTest(name = "{0}: ReEncrypt AES => RSA instruction file with custom suffix") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") - void reencrypt_aes_to_rsa_instruction_file(TestUtils.LanguageServerTarget language) { + void reencryptAesToRsaInstructionFile(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); for (int i = 0; i < kcGcmObjectsAesToRsaCustom.size(); i++) { @@ -427,7 +427,7 @@ void reencrypt_aes_to_rsa_instruction_file(TestUtils.LanguageServerTarget langua @ParameterizedTest(name = "{0}: ReEncrypt RSA => AES instruction file (default suffix)") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") - void reencrypt_rsa_to_aes_default_instruction_file(TestUtils.LanguageServerTarget language) { + void reencryptRsaToAesDefaultInstructionFile(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); for (int i = 0; i < kcGcmObjectsRsaToAesDefault.size(); i++) { @@ -454,7 +454,7 @@ void reencrypt_rsa_to_aes_default_instruction_file(TestUtils.LanguageServerTarge @ParameterizedTest(name = "{0}: ReEncrypt AES => RSA instruction file (default suffix)") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$ReEncryptTestsNested#reencryptSupportedClients") - void reencrypt_aes_to_rsa_default_instruction_file(TestUtils.LanguageServerTarget language) { + void reencryptAesToRsaDefaultInstructionFile(TestUtils.LanguageServerTarget language) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); for (int i = 0; i < kcGcmObjectsAesToRsaDefault.size(); i++) { @@ -536,7 +536,7 @@ public static Stream clientsCanGetRawAESWithInstructionFileAndCustomS @ParameterizedTest(name = "{0}: Decrypt AES => AES re-encrypted objects") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_reencrypted_aes_to_aes_objects(TestUtils.LanguageServerTarget language) { + void decryptReencryptedAesToAesObjects(TestUtils.LanguageServerTarget language) { if (reEncryptedAesToAes.isEmpty()) return; S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -556,7 +556,7 @@ void decrypt_reencrypted_aes_to_aes_objects(TestUtils.LanguageServerTarget langu @ParameterizedTest(name = "{0}: Decrypt RSA => RSA re-encrypted objects") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_reencrypted_rsa_to_rsa_objects(TestUtils.LanguageServerTarget language) { + void decryptReencryptedRsaToRsaObjects(TestUtils.LanguageServerTarget language) { if (reEncryptedRsaToRsa.isEmpty()) return; S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -576,7 +576,7 @@ void decrypt_reencrypted_rsa_to_rsa_objects(TestUtils.LanguageServerTarget langu @ParameterizedTest(name = "{0}: Decrypt AES => RSA re-encrypted objects with custom suffix") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFileAndCustomSuffix") - void decrypt_reencrypted_aes_to_rsa_objects(TestUtils.LanguageServerTarget language) { + void decryptReencryptedAesToRsaObjects(TestUtils.LanguageServerTarget language) { if (reEncryptedAesToRsa.isEmpty()) return; S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -599,7 +599,7 @@ void decrypt_reencrypted_aes_to_rsa_objects(TestUtils.LanguageServerTarget langu @ParameterizedTest(name = "{0}: Decrypt RSA => AES re-encrypted objects (default suffix)") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawAESWithInstructionFile") - void decrypt_reencrypted_rsa_to_aes_default_objects(TestUtils.LanguageServerTarget language) { + void decryptReencryptedRsaToAesDefaultObjects(TestUtils.LanguageServerTarget language) { if (reEncryptedRsaToAesDefault.isEmpty()) return; S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() @@ -621,7 +621,7 @@ void decrypt_reencrypted_rsa_to_aes_default_objects(TestUtils.LanguageServerTarg @ParameterizedTest(name = "{0}: Decrypt AES => RSA re-encrypted objects (default suffix)") @MethodSource("software.amazon.encryption.s3.ReEncryptTests$DecryptReEncryptedTests#clientsCanGetRawRSAWithInstructionFile") - void decrypt_reencrypted_aes_to_rsa_default_objects(TestUtils.LanguageServerTarget language) { + void decryptReencryptedAesToRsaDefaultObjects(TestUtils.LanguageServerTarget language) { if (reEncryptedAesToRsaDefault.isEmpty()) return; S3ECTestServerClient client = TestUtils.testServerClientFor(language); CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() From cde9779acfb3a1ec1f499b2fd203320a74b88354 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 3 Dec 2025 20:52:14 -0800 Subject: [PATCH 40/46] Add ReEncrypt to Java V3 --- .../s3/CreateClientOperationImpl.java | 5 +- .../encryption/s3/ReEncryptOperationImpl.java | 185 ++++++++++++++++++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- 3 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java 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 bdb0b30b..6a3da066 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 @@ -37,9 +37,11 @@ public class CreateClientOperationImpl implements CreateClientOperation { private Map clientCache_; + private Map keyringCache_; - public CreateClientOperationImpl(Map clientCache) { + public CreateClientOperationImpl(Map clientCache, Map keyringCache) { clientCache_ = clientCache; + keyringCache_ = keyringCache; } // Copied from S3EC. @@ -137,6 +139,7 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); clientCache_.put(uuidString, s3Client); + keyringCache_.put(uuidString, keyring); return CreateClientOutput.builder() .clientId(uuidString) .build(); diff --git a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java new file mode 100644 index 00000000..dd376429 --- /dev/null +++ b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -0,0 +1,185 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RawKeyring; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.model.ReEncryptInput; +import software.amazon.encryption.s3.model.ReEncryptOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.ReEncryptOperation; +import software.amazon.smithy.java.server.RequestContext; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +public class ReEncryptOperationImpl implements ReEncryptOperation { + private final Map clientCache_; + private final Map keyringCache_; + + public ReEncryptOperationImpl(Map clientCache, Map keyringCache) { + clientCache_ = clientCache; + keyringCache_ = keyringCache; + } + + @Override + public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { + try { + S3Client s3Client = clientCache_.get(input.getClientID()); + + // Ensure we have an S3EncryptionClient, not just a plain S3Client + if (!(s3Client instanceof S3EncryptionClient)) { + throw new IllegalStateException( + "Client " + input.getClientID() + " is not an S3EncryptionClient"); + } + + S3EncryptionClient s3EncryptionClient = (S3EncryptionClient) s3Client; + + // Create a new keyring from the provided newKeyMaterial + KeyMaterial newKeyMaterial = input.getNewKeyMaterial(); + if (newKeyMaterial == null) { + throw new IllegalStateException( + "newKeyMaterial is required for ReEncrypt operation"); + } + + RawKeyring newKeyring = createKeyringFromMaterial(newKeyMaterial); + + try { + // Build the ReEncryptInstructionFileRequest + ReEncryptInstructionFileRequest.Builder requestBuilder = + ReEncryptInstructionFileRequest.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .newKeyring(newKeyring); + + // Add optional instruction file suffix if provided + if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { + requestBuilder.instructionFileSuffix(input.getInstructionFileSuffix()); + } + + // Add optional enforceRotation if provided + if (input.isEnforceRotation() != null) { + requestBuilder.enforceRotation(input.isEnforceRotation()); + } + + ReEncryptInstructionFileRequest reEncryptRequest = requestBuilder.build(); + + // Perform the re-encryption + ReEncryptInstructionFileResponse response = + s3EncryptionClient.reEncryptInstructionFile(reEncryptRequest); + + // Build and return the output + return ReEncryptOutput.builder() + .bucket(response.bucket()) + .key(response.key()) + .instructionFileSuffix(response.instructionFileSuffix()) + .enforceRotation(response.enforceRotation()) + .build(); + + } catch (S3EncryptionClientException s3EncryptionClientException) { + // Modeled exceptions MUST be returned as such + StringWriter sw = new StringWriter(); + s3EncryptionClientException.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw S3EncryptionClientError.builder() + .message(stackTrace) + .build(); + } + } catch (Exception e) { + // Don't wrap modeled errors + if (e instanceof S3EncryptionClientError) { + throw e; + } + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } + + /** + * Creates a RawKeyring from KeyMaterial. + * The KeyMaterial should have exactly one of: aesKey, rsaKey, or kmsKeyId set. + */ + private RawKeyring createKeyringFromMaterial(KeyMaterial keyMaterial) { + try { + // Get materials description from KeyMaterial if provided + MaterialsDescription materialsDescription = null; + if (keyMaterial.getMaterialsDescription() != null && !keyMaterial.getMaterialsDescription().isEmpty()) { + MaterialsDescription.Builder builder = MaterialsDescription.builder(); + for (Map.Entry entry : keyMaterial.getMaterialsDescription().entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + materialsDescription = builder.build(); + } + + // Check for AES key + if (keyMaterial.getAesKey() != null) { + byte[] aesKeyBytes = new byte[keyMaterial.getAesKey().remaining()]; + keyMaterial.getAesKey().get(aesKeyBytes); + SecretKey secretKey = new SecretKeySpec(aesKeyBytes, "AES"); + + AesKeyring.Builder keyringBuilder = AesKeyring.builder() + .wrappingKey(secretKey); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + // Check for RSA key + if (keyMaterial.getRsaKey() != null) { + byte[] rsaKeyBytes = new byte[keyMaterial.getRsaKey().remaining()]; + keyMaterial.getRsaKey().get(rsaKeyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rsaKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + + // Derive the public key from the private key + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + PartialRsaKeyPair keyPair = PartialRsaKeyPair.builder() + .privateKey(privateKey) + .publicKey(publicKey) + .build(); + + RsaKeyring.Builder keyringBuilder = RsaKeyring.builder() + .wrappingKeyPair(keyPair); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + throw new IllegalStateException( + "KeyMaterial must have either aesKey or rsaKey set"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create keyring from KeyMaterial: " + e.getMessage(), e); + } + } +} diff --git a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java index 8ad437f4..be53f20c 100644 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -27,14 +27,16 @@ public void run() { // Obviously this can get messy in a real service. // Assume that the tests behave and don't induce weird race conditions. Map clientCache = new ConcurrentHashMap<>(); + Map keyringCache = new ConcurrentHashMap<>(); Server server = Server.builder() .endpoints(endpoint) .addService( S3ECTestServer.builder() - .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addCreateClientOperation(new CreateClientOperationImpl(clientCache, keyringCache)) .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) .build()) .build(); System.out.println("Starting server..."); From 05a0cfb668d0380fafd58e75056e8e36af00fead Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 3 Dec 2025 21:02:00 -0800 Subject: [PATCH 41/46] Strings to constants --- .../s3/InstructionFileFailures.java | 41 +++++++++++-------- .../amazon/encryption/s3/RangedGetTests.java | 35 ++++++++++------ 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 395af191..7b534a4f 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -60,6 +60,11 @@ public class InstructionFileFailures { // Synchronization latch - released when encrypt phase completes private static final CountDownLatch encryptPhaseComplete = new CountDownLatch(1); + + // Object key suffixes for test copies + private static final String SUFFIX_GOOD_COPY = "-good-copy"; + private static final String SUFFIX_BAD_BOTH_META_AND_INSTRUCTION = "-bad-both-meta-and-instruction"; + private static final String SUFFIX_BAD_ONLY_INSTRUCTION = "-bad-only-instruction"; /** * Encryption Tests - Encrypt Phase @@ -255,7 +260,7 @@ static void makeCopiesToVerifyThings() throws Exception { // Put a strict copy, to verify that we know how to do this putObjectWithInstructionFile( ptS3Client, - objectKey + "-good-copy", + objectKey + SUFFIX_GOOD_COPY, encryptedObject.asByteArray(), objectMetadata, instructionFileJson @@ -273,7 +278,7 @@ static void makeCopiesToVerifyThings() throws Exception { // Put instruction files that should fail: putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-both-meta-and-instruction", + objectKey + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION, encryptedObject.asByteArray(), objectMetadata, instructionFileWithCommitmentValues @@ -281,7 +286,7 @@ static void makeCopiesToVerifyThings() throws Exception { putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-only-instruction", + objectKey + SUFFIX_BAD_ONLY_INSTRUCTION, encryptedObject.asByteArray(), Map.of(), instructionFileWithCommitmentValues @@ -416,7 +421,7 @@ void decryptKmsOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget S3ECId, crossLanguageObjectsKms .stream() - .map(key -> key + "-good-copy") + .map(key -> key + SUFFIX_GOOD_COPY) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, crossLanguageObjectsKms @@ -440,7 +445,7 @@ void decryptKmsWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.La S3ECId, crossLanguageObjectsKms .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -463,7 +468,7 @@ void decryptKmsWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServer S3ECId, crossLanguageObjectsKms .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -488,7 +493,7 @@ void decryptKmsWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageSe S3ECId, crossLanguageObjectsKms .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -513,7 +518,7 @@ void decryptKmsWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.Lang S3ECId, crossLanguageObjectsKms .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -545,7 +550,7 @@ void decryptRsaOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget S3ECId, crossLanguageObjectsRsa .stream() - .map(key -> key + "-good-copy") + .map(key -> key + SUFFIX_GOOD_COPY) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, crossLanguageObjectsRsa @@ -569,7 +574,7 @@ void decryptRsaWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.La S3ECId, crossLanguageObjectsRsa .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -592,7 +597,7 @@ void decryptRsaWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServer S3ECId, crossLanguageObjectsRsa .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -617,7 +622,7 @@ void decryptRsaWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageSe S3ECId, crossLanguageObjectsRsa .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -642,7 +647,7 @@ void decryptRsaWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.Lang S3ECId, crossLanguageObjectsRsa .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -674,7 +679,7 @@ void decryptAesOriginalAndGoodCopyObjectsSucceeds(TestUtils.LanguageServerTarget S3ECId, crossLanguageObjectsAes .stream() - .map(key -> key + "-good-copy") + .map(key -> key + SUFFIX_GOOD_COPY) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, crossLanguageObjectsAes @@ -698,7 +703,7 @@ void decryptAesWithDuplicateCommitmentInMetadataAndInstructionFails(TestUtils.La S3ECId, crossLanguageObjectsAes .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -721,7 +726,7 @@ void decryptAesWithCommitmentOnlyInInstructionFileFails(TestUtils.LanguageServer S3ECId, crossLanguageObjectsAes .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -746,7 +751,7 @@ void decryptAesWithDuplicateCommitmentFailsWithForbidPolicy(TestUtils.LanguageSe S3ECId, crossLanguageObjectsAes .stream() - .map(key -> key + "-bad-both-meta-and-instruction") + .map(key -> key + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); @@ -771,7 +776,7 @@ void decryptAesWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.Lang S3ECId, crossLanguageObjectsAes .stream() - .map(key -> key + "-bad-only-instruction") + .map(key -> key + SUFFIX_BAD_ONLY_INSTRUCTION) .collect(Collectors.toList()), EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java index 89610287..bd87fe89 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -138,6 +138,15 @@ public class RangedGetTests { // Random number generator for bit flipping (seeded for reproducibility) private static final Random random = new Random(System.currentTimeMillis()); + + // Object key suffixes for test copies + private static final String SUFFIX_GOOD_COPY = "-good-copy"; + private static final String SUFFIX_BAD_MUTATED_C = "-bad-mutated-c-bit-"; + private static final String SUFFIX_BAD_MUTATED_D = "-bad-mutated-d-bit-"; + private static final String SUFFIX_BAD_MUTATED_I = "-bad-mutated-i-bit-"; + private static final String SUFFIX_BAD_INVALID_D_LENGTH_SHORT = "-bad-invalid-d-length-short"; + private static final String SUFFIX_BAD_INVALID_D_LENGTH_LONG = "-bad-invalid-d-length-long"; + private static final String SUFFIX_BAD_COMMITMENT_IN_INSTRUCTION = "-bad-commitment-in-instruction"; /** * Encryption Tests - Encrypt Phase @@ -422,7 +431,7 @@ static void createCorruptedCopies() throws Exception { Map objectMetadata = encryptedObject.response().metadata(); // Create good copy - putObjectWithMetadata(ptS3Client, objectKey + "-good-copy", objectData, objectMetadata); + putObjectWithMetadata(ptS3Client, objectKey + SUFFIX_GOOD_COPY, objectData, objectMetadata); // Extract commitment values from metadata String commitC = objectMetadata.get("x-amz-c"); @@ -436,7 +445,7 @@ static void createCorruptedCopies() throws Exception { String mutatedC = Base64.getEncoder().encodeToString(commitCBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-c", mutatedC); - String mutatedKey = objectKey + "-bad-mutated-c-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_C + bitPos; putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); mutatedCObjectsMetadata.add(mutatedKey); } @@ -447,7 +456,7 @@ static void createCorruptedCopies() throws Exception { String mutatedD = Base64.getEncoder().encodeToString(commitDBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-d", mutatedD); - String mutatedKey = objectKey + "-bad-mutated-d-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_D + bitPos; putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); mutatedDObjectsMetadata.add(mutatedKey); } @@ -458,7 +467,7 @@ static void createCorruptedCopies() throws Exception { String mutatedI = Base64.getEncoder().encodeToString(commitIBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-i", mutatedI); - String mutatedKey = objectKey + "-bad-mutated-i-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_I + bitPos; putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); mutatedIObjectsMetadata.add(mutatedKey); } @@ -474,7 +483,7 @@ static void createCorruptedCopies() throws Exception { String shortD = Base64.getEncoder().encodeToString(shortDBytes); Map shortDMetadata = new java.util.HashMap<>(objectMetadata); shortDMetadata.put("x-amz-d", shortD); - String shortDKey = objectKey + "-bad-invalid-d-length-short"; + String shortDKey = objectKey + SUFFIX_BAD_INVALID_D_LENGTH_SHORT; putObjectWithMetadata(ptS3Client, shortDKey, objectData, shortDMetadata); invalidDLengthShortMetadata.add(shortDKey); @@ -488,7 +497,7 @@ static void createCorruptedCopies() throws Exception { String longD = Base64.getEncoder().encodeToString(longDBytes); Map longDMetadata = new java.util.HashMap<>(objectMetadata); longDMetadata.put("x-amz-d", longD); - String longDKey = objectKey + "-bad-invalid-d-length-long"; + String longDKey = objectKey + SUFFIX_BAD_INVALID_D_LENGTH_LONG; putObjectWithMetadata(ptS3Client, longDKey, objectData, longDMetadata); invalidDLengthLongMetadata.add(longDKey); } @@ -516,7 +525,7 @@ static void createCorruptedCopies() throws Exception { // Create good copy (both object and instruction file) putObjectWithInstructionFile( ptS3Client, - objectKey + "-good-copy", + objectKey + SUFFIX_GOOD_COPY, objectData, objectMetadata, originalInstructionFileJson @@ -536,7 +545,7 @@ static void createCorruptedCopies() throws Exception { putObjectWithInstructionFile( ptS3Client, - objectKey + "-bad-commitment-in-instruction", + objectKey + SUFFIX_BAD_COMMITMENT_IN_INSTRUCTION, objectData, objectMetadata, corruptedInstructionJson @@ -549,7 +558,7 @@ static void createCorruptedCopies() throws Exception { String mutatedC = Base64.getEncoder().encodeToString(commitCBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-c", mutatedC); - String mutatedKey = objectKey + "-bad-mutated-c-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_C + bitPos; putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); mutatedCObjectsInstruction.add(mutatedKey); } @@ -560,7 +569,7 @@ static void createCorruptedCopies() throws Exception { String mutatedD = Base64.getEncoder().encodeToString(commitDBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-d", mutatedD); - String mutatedKey = objectKey + "-bad-mutated-d-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_D + bitPos; putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); mutatedDObjectsInstruction.add(mutatedKey); } @@ -571,7 +580,7 @@ static void createCorruptedCopies() throws Exception { String mutatedI = Base64.getEncoder().encodeToString(commitIBytes); Map mutatedMetadata = new java.util.HashMap<>(objectMetadata); mutatedMetadata.put("x-amz-i", mutatedI); - String mutatedKey = objectKey + "-bad-mutated-i-bit-" + bitPos; + String mutatedKey = objectKey + SUFFIX_BAD_MUTATED_I + bitPos; putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); mutatedIObjectsInstruction.add(mutatedKey); } @@ -587,7 +596,7 @@ static void createCorruptedCopies() throws Exception { String shortD = Base64.getEncoder().encodeToString(shortDBytes); Map shortDMetadata = new java.util.HashMap<>(objectMetadata); shortDMetadata.put("x-amz-d", shortD); - String shortDKey = objectKey + "-bad-invalid-d-length-short"; + String shortDKey = objectKey + SUFFIX_BAD_INVALID_D_LENGTH_SHORT; putObjectWithInstructionFile(ptS3Client, shortDKey, objectData, shortDMetadata, originalInstructionFileJson); invalidDLengthShortInstruction.add(shortDKey); @@ -601,7 +610,7 @@ static void createCorruptedCopies() throws Exception { String longD = Base64.getEncoder().encodeToString(longDBytes); Map longDMetadata = new java.util.HashMap<>(objectMetadata); longDMetadata.put("x-amz-d", longD); - String longDKey = objectKey + "-bad-invalid-d-length-long"; + String longDKey = objectKey + SUFFIX_BAD_INVALID_D_LENGTH_LONG; putObjectWithInstructionFile(ptS3Client, longDKey, objectData, longDMetadata, originalInstructionFileJson); invalidDLengthLongInstruction.add(longDKey); } From 12d8fec0d31af9f8240e81121044e402cd46a72c Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 3 Dec 2025 21:02:42 -0800 Subject: [PATCH 42/46] make the makefile fail if it fails to build --- test-server/Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index 0ae81124..94a76b3f 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -27,8 +27,8 @@ build-all-servers: $(BUILD_SERVER_TARGETS): build-%: @if [ -f $*/Makefile ]; then \ - echo "[`date +%H:%M:%S`] Building server in $*..."; \ - $(MAKE) -C $* build-server; \ + 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 $*"; \ @@ -52,12 +52,12 @@ start-all-servers: $(START_SERVER_TARGETS): start-%: @if [ -f $*/Makefile ]; then \ - echo "Starting server in $*..."; \ + echo "Starting server in $*..." && \ $(MAKE) -C $* start-server; \ else \ echo "❌ Error: no Makefile found in $*"; \ exit 1; \ - fi; + fi wait-all-servers: @echo "Waiting for all servers to be ready..." @@ -66,12 +66,12 @@ wait-all-servers: $(WAIT_SERVER_TARGETS): wait-%: @if [ -f $*/Makefile ]; then \ - echo "Waiting server in $*..."; \ + echo "Waiting server in $*..." && \ $(MAKE) -C $* wait-for-server; \ else \ echo "❌ Error: no Makefile found in $*"; \ exit 1; \ - fi; + fi # Run the Java tests From a49ff98221083292e316490a0325d49cec2e4cd0 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 8 Dec 2025 10:58:29 -0800 Subject: [PATCH 43/46] Updates to merged code submodules --- test-server/cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- test-server/go-v3-transition-server/local-go-s3ec | 2 +- test-server/go-v4-server/local-go-s3ec | 2 +- test-server/java-v3-transition-server/s3ec-staging | 2 +- test-server/java-v4-server/s3ec-staging | 2 +- test-server/net-v3-transition-server/s3ec-v3-transition-branch | 2 +- test-server/net-v4-server/s3ec-net-v4-improved | 2 +- test-server/php-v3-server/local-php-sdk | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index 72161a32..cec1f193 160000 --- a/test-server/cpp-v2-transition-server/aws-sdk-cpp +++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 72161a327a201737bbe8cc8b86941fdbf6ad1d5c +Subproject commit cec1f1933be672f65627c11ff2d853e07c5b3ff4 diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 72161a32..cec1f193 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 72161a327a201737bbe8cc8b86941fdbf6ad1d5c +Subproject commit cec1f1933be672f65627c11ff2d853e07c5b3ff4 diff --git a/test-server/go-v3-transition-server/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec index e59a38ca..912914ad 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 e59a38caeddfcfbf41e064e125b5783cdfce3878 +Subproject commit 912914ad1c14942c3ea8638fb37f9e2c46445a84 diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index e59a38ca..912914ad 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit e59a38caeddfcfbf41e064e125b5783cdfce3878 +Subproject commit 912914ad1c14942c3ea8638fb37f9e2c46445a84 diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging index 597d5b49..183f1984 160000 --- a/test-server/java-v3-transition-server/s3ec-staging +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -1 +1 @@ -Subproject commit 597d5b491ac578f5d03d3fa757201eb48690cd00 +Subproject commit 183f1984ed1679e8aa4cb368aeda66f2131a2061 diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging index 2626ed6e..7a1899bb 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit 2626ed6e312c1c5e01abea2f30727ea0f2af299d +Subproject commit 7a1899bb8be6f137a3031ff76f2a1bf3f278e98d 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 d099cfd1..c3bf38b9 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 d099cfd151e2c61fb97dcd417828fb1dd5468b0c +Subproject commit c3bf38b93c25f7169982073b1ffd1f3d00f59073 diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index 8ce8983b..04f70c8b 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 8ce8983bd0edf973651aee0c29894df9091cf97a +Subproject commit 04f70c8b70e25c7a1a36fcd5a420c40806157c66 diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index 88ee9515..3acb3ad4 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 88ee95156f2884767b72f9219736e976d98a9c96 +Subproject commit 3acb3ad4d98debcfc2148290cd6fcea83962fe08 From eb00a7b56f559e879fe61daabc809f6da371ad50 Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Fri, 5 Dec 2025 16:36:23 -0800 Subject: [PATCH 44/46] update Display name --- .../it/java/software/amazon/encryption/s3/GCMTestSuite.java | 3 +++ .../amazon/encryption/s3/InstructionFileFailures.java | 3 +++ .../java/software/amazon/encryption/s3/KC_GCMTestSuite.java | 3 +++ .../it/java/software/amazon/encryption/s3/RangedGetTests.java | 3 +++ .../it/java/software/amazon/encryption/s3/ReEncryptTests.java | 4 ++++ 5 files changed, 16 insertions(+) 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 index 4be4d434..ca495f56 100644 --- 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 @@ -14,6 +14,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -47,6 +48,7 @@ public class GCMTestSuite { * The encrypted objects are stored in thread-safe lists for use by DecryptTests. */ @Nested + @DisplayName("GCMTestSuite - Encrypt") class EncryptTests { private static final String sharedObjectKeyBase = "test-gcm-kms"; private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -140,6 +142,7 @@ static void signalEncryptionComplete() { * They depend on EncryptTests completing first (enforced by @Order). */ @Nested + @DisplayName("GCMTestSuite - Decrypt") class DecryptTests { private static List crossLanguageObjects; private static final KeyMaterial kmsKeyArn = KeyMaterial.builder() diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java index 7b534a4f..6460fbad 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -74,6 +75,7 @@ public class InstructionFileFailures { * The encrypted objects are stored in thread-safe lists for use by DecryptTests. */ @Nested + @DisplayName("InstructionFileFailures - Encrypt") class EncryptTests { private static final String sharedObjectKeyBaseMetaDataMode = "test-instruction-files-cases"; private static KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -337,6 +339,7 @@ static void signalEncryptionComplete() throws Exception { * They depend on EncryptTests completing first. */ @Nested + @DisplayName("InstructionFileFailures - Decrypt") class DecryptTests { private static List crossLanguageObjectsKms; private static List crossLanguageObjectsRsa; 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 index c367315f..0fdc3438 100644 --- 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 @@ -17,6 +17,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -52,6 +53,7 @@ public class KC_GCMTestSuite { * The encrypted objects are stored in thread-safe lists for use by DecryptTests. */ @Nested + @DisplayName("KC_GCMTestSuite - Encrypt") class EncryptTests { private static final String sharedObjectKeyBaseMetaDataMode = "test-kc-gcm-kms"; private static final String sharedObjectKeyBaseInsFileMode = "test-kc-gcm-rsa-instruction-file"; @@ -195,6 +197,7 @@ static void signalEncryptionComplete() { * They depend on EncryptTests completing first (enforced by @Order). */ @Nested + @DisplayName("KC_GCMTestSuite - Decrypt") class DecryptTests { private static List crossLanguageObjectsMetaDataMode; private static List crossLanguageObjectsInstructionFiles; diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java index bd87fe89..5a954e1f 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -155,6 +156,7 @@ public class RangedGetTests { * corrupted copies for failure testing. All tests in this class can run in parallel. */ @Nested + @DisplayName("RangedGetTests - Encrypt") class EncryptTests { private static final String sharedObjectKeyBase = "test-ranged-get"; private static KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -673,6 +675,7 @@ static void signalEncryptionComplete() throws Exception { * They depend on EncryptTests completing first. */ @Nested + @DisplayName("RangedGetTests - RangedGet") class RangedGetTestsNested { private static List cbcObjects; private static List gcmObjects; diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java index ef32b919..3053afb6 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -140,6 +141,7 @@ public class ReEncryptTests { private static final List reEncryptedAesToRsaDefault = Collections.synchronizedList(new ArrayList<>()); @Nested + @DisplayName("ReEncryptTests - Encrypt") class EncryptTests { private static final String sharedObjectKeyBase = "test-reencrypt"; @@ -320,6 +322,7 @@ static void signalEncryptionComplete() { } @Nested + @DisplayName("ReEncryptTests - ReEncrypt") class ReEncryptTestsNested { private static List kcGcmObjectsAesToAes, kcGcmObjectsAesToRsaCustom, kcGcmObjectsAesToRsaDefault; private static List kcGcmObjectsRsaToRsa, kcGcmObjectsRsaToAesDefault; @@ -486,6 +489,7 @@ static void signalReEncryptionComplete() { } @Nested + @DisplayName("ReEncryptTests - DecryptReEncrypted") class DecryptReEncryptedTests { private static KeyMaterial aesKeyMaterial1, aesKeyMaterial2, rsaKeyMaterial1, rsaKeyMaterial2; From c3d9b675a77e04e7452d6d341a8cb4d2753e4ebc Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 8 Dec 2025 09:31:36 -0800 Subject: [PATCH 45/46] names and feedback --- .../amazon/encryption/s3/KC_GCMTestSuite.java | 11 +++++------ .../java/software/amazon/encryption/s3/TestUtils.java | 4 +++- 2 files changed, 8 insertions(+), 7 deletions(-) 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 index 0fdc3438..d256f909 100644 --- 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 @@ -93,7 +93,7 @@ static KeyPair getRsaKeyPair() { @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( + void improved_configured_with_require_encrypt_allow_decrypt_should_encrypt_kc_gcm_kms( TestUtils.LanguageServerTarget language ) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); @@ -113,7 +113,7 @@ void improved_configured_with_require_encrypt_allow_decrypt_should_encrypt_kc_gc @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( + void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm_ins_file_rsa( TestUtils.LanguageServerTarget language ) { if (!RAW_SUPPORTED.contains(language.getLanguageName())) { @@ -144,7 +144,7 @@ void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_ @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( + void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_gcm_kms( TestUtils.LanguageServerTarget language ) { S3ECTestServerClient client = TestUtils.testServerClientFor(language); @@ -225,14 +225,13 @@ static void setup() throws InterruptedException { @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( + void transition_configured_with_the_default_should_decrypt_kc_gcm_kms( 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(); @@ -363,7 +362,7 @@ void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_ @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( + void transition_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_gcm_ins_file_rsa( final TestUtils.LanguageServerTarget language ) { if (!RAW_SUPPORTED.contains(language.getLanguageName())) { 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 52bf6794..f2065115 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 @@ -6,6 +6,7 @@ package software.amazon.encryption.s3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import java.net.Socket; @@ -147,6 +148,7 @@ public class TestUtils { // Languages that support custom instruction file suffix on GetObject // Only Java, Ruby, and PHP servers have been updated with this feature + // This is a current gap. public static final Set CUSTOM_INSTRUCTION_SUFFIX_GET_SUPPORTED = Set.of( JAVA_V3_TRANSITION, @@ -618,7 +620,7 @@ public static void Decrypt( String instructionFileSuffix ) { if (crossLanguageObjects.isEmpty()) { - throw new AssertionError("There is nothing to decrypt"); + fail("There is nothing to decrypt"); } List failures = new ArrayList<>(); From 91a24a5594e3ed519bbc3c8037d680a0b54a1d0e Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Mon, 8 Dec 2025 12:03:48 -0800 Subject: [PATCH 46/46] ReEncrypt does not cache keyrings --- .../encryption/s3/ReEncryptOperationImpl.java | 103 +++++++++++++++--- .../encryption/s3/S3ECJavaTestServer.java | 2 +- .../encryption/s3/ReEncryptOperationImpl.java | 4 +- .../encryption/s3/S3ECJavaTestServer.java | 2 +- 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java index bc4aeab5..7a809761 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -3,8 +3,13 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RawKeyring; +import software.amazon.encryption.s3.materials.RsaKeyring; import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; import software.amazon.encryption.s3.model.ReEncryptInput; import software.amazon.encryption.s3.model.ReEncryptOutput; import software.amazon.encryption.s3.model.S3EncryptionClientError; @@ -13,15 +18,21 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + public class ReEncryptOperationImpl implements ReEncryptOperation { private final Map clientCache_; - private final Map keyringCache_; - public ReEncryptOperationImpl(Map clientCache, Map keyringCache) { + public ReEncryptOperationImpl(Map clientCache) { clientCache_ = clientCache; - keyringCache_ = keyringCache; } @Override @@ -37,19 +48,14 @@ public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { S3EncryptionClient s3EncryptionClient = (S3EncryptionClient) s3Client; - // Get the keyring from cache and cast to RawKeyring - software.amazon.encryption.s3.materials.Keyring cachedKeyring = keyringCache_.get(input.getClientID()); - if (cachedKeyring == null) { - throw new IllegalStateException( - "No keyring found for client " + input.getClientID()); - } - - if (!(cachedKeyring instanceof RawKeyring)) { + // Create a new keyring from the provided newKeyMaterial + KeyMaterial newKeyMaterial = input.getNewKeyMaterial(); + if (newKeyMaterial == null) { throw new IllegalStateException( - "Keyring for client " + input.getClientID() + " is not a RawKeyring"); + "newKeyMaterial is required for ReEncrypt operation"); } - RawKeyring keyring = (RawKeyring) cachedKeyring; + RawKeyring newKeyring = createKeyringFromMaterial(newKeyMaterial); try { // Build the ReEncryptInstructionFileRequest @@ -57,7 +63,7 @@ public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { ReEncryptInstructionFileRequest.builder() .bucket(input.getBucket()) .key(input.getKey()) - .newKeyring(keyring); + .newKeyring(newKeyring); // Add optional instruction file suffix if provided if (input.getInstructionFileSuffix() != null && !input.getInstructionFileSuffix().isEmpty()) { @@ -105,4 +111,73 @@ public ReEncryptOutput reEncrypt(ReEncryptInput input, RequestContext context) { .build(); } } + + /** + * Creates a RawKeyring from KeyMaterial. + * The KeyMaterial should have exactly one of: aesKey, rsaKey, or kmsKeyId set. + */ + private RawKeyring createKeyringFromMaterial(KeyMaterial keyMaterial) { + try { + // Get materials description from KeyMaterial if provided + MaterialsDescription materialsDescription = null; + if (keyMaterial.getMaterialsDescription() != null && !keyMaterial.getMaterialsDescription().isEmpty()) { + MaterialsDescription.Builder builder = MaterialsDescription.builder(); + for (Map.Entry entry : keyMaterial.getMaterialsDescription().entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + materialsDescription = builder.build(); + } + + // Check for AES key + if (keyMaterial.getAesKey() != null) { + byte[] aesKeyBytes = new byte[keyMaterial.getAesKey().remaining()]; + keyMaterial.getAesKey().get(aesKeyBytes); + SecretKey secretKey = new SecretKeySpec(aesKeyBytes, "AES"); + + AesKeyring.Builder keyringBuilder = AesKeyring.builder() + .wrappingKey(secretKey); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + // Check for RSA key + if (keyMaterial.getRsaKey() != null) { + byte[] rsaKeyBytes = new byte[keyMaterial.getRsaKey().remaining()]; + keyMaterial.getRsaKey().get(rsaKeyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rsaKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + + // Derive the public key from the private key + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + PartialRsaKeyPair keyPair = PartialRsaKeyPair.builder() + .privateKey(privateKey) + .publicKey(publicKey) + .build(); + + RsaKeyring.Builder keyringBuilder = RsaKeyring.builder() + .wrappingKeyPair(keyPair); + + if (materialsDescription != null) { + keyringBuilder.materialsDescription(materialsDescription); + } + + return keyringBuilder.build(); + } + + throw new IllegalStateException( + "KeyMaterial must have either aesKey or rsaKey set"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create keyring from KeyMaterial: " + e.getMessage(), e); + } + } } diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java index c252991d..78c84dff 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -37,7 +37,7 @@ public void run() { .addCreateClientOperation(new CreateClientOperationImpl(clientCache, keyringCache)) .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) - .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) + .addReEncryptOperation(new ReEncryptOperationImpl(clientCache)) .build()) .build(); System.out.println("Starting server..."); diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java index dd376429..6a7cd5b6 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -30,11 +30,9 @@ public class ReEncryptOperationImpl implements ReEncryptOperation { private final Map clientCache_; - private final Map keyringCache_; - public ReEncryptOperationImpl(Map clientCache, Map keyringCache) { + public ReEncryptOperationImpl(Map clientCache) { clientCache_ = clientCache; - keyringCache_ = keyringCache; } @Override diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java index ea6e3777..88d5b981 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -36,7 +36,7 @@ public void run() { .addCreateClientOperation(new CreateClientOperationImpl(clientCache, keyringCache)) .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) - .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) + .addReEncryptOperation(new ReEncryptOperationImpl(clientCache)) .build()) .build(); System.out.println("Starting server...");