diff --git a/.gitmodules b/.gitmodules index a88a83f9..27d4960b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,7 @@ [submodule "test-server/cpp-v2-server/aws-sdk-cpp"] path = test-server/cpp-v2-server/aws-sdk-cpp url = git@github.com:awslabs/aws-sdk-cpp-staging.git +[submodule "test-server/net-v2-transition-server/s3ec-v2-transition-branch"] + path = test-server/net-v2-transition-server/s3ec-v2-transition-branch + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = s3ec-v2-transition-integ 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 cf45006e..b94e9010 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 @@ -423,7 +423,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.getLanguageName().equals(NET_V3_CURRENT) || language.getLanguageName().equals(NET_V2_CURRENT) || language.getLanguageName().equals(NET_V3_TRANSITION) || language.getLanguageName().equals(NET_V4) + if (language.getLanguageName().equals(NET_V3_CURRENT) || language.getLanguageName().equals(NET_V2_CURRENT) || language.getLanguageName().equals(NET_V2_TRANSITION) || language.getLanguageName().equals(NET_V3_TRANSITION) || language.getLanguageName().equals(NET_V4) || 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" 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 f2065115..51e4ff90 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 @@ -97,10 +97,10 @@ public class TestUtils { // Sets of unsupported features by language public static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3, NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3, NET_V2_CURRENT, NET_V3_CURRENT, NET_V2_TRANSITION, NET_V3_TRANSITION, NET_V4); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = - Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); + Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V2_TRANSITION, NET_V3_TRANSITION, NET_V4); public static final Set RE_ENCRYPT_SUPPORTED = Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4); @@ -114,7 +114,7 @@ public class TestUtils { // 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 + , NET_V2_CURRENT, NET_V2_TRANSITION, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4 , RUBY_V2_TRANSITION, RUBY_V3 , CPP_V2_CURRENT, CPP_V2_TRANSITION, CPP_V3 ); @@ -134,7 +134,7 @@ public class TestUtils { // .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 = - Set.of(NET_V2_CURRENT, NET_V2_TRANSITION, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); + Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V2_TRANSITION, NET_V3_TRANSITION, NET_V4); // Go does not write with instruction files public static final Set INSTRUCTION_FILE_PUT_UNSUPPORTED = @@ -174,7 +174,7 @@ public class TestUtils { Set.of( JAVA_V3_TRANSITION, GO_V3_TRANSITION, - // NET_V2_TRANSITION, + NET_V2_TRANSITION, NET_V3_TRANSITION, CPP_V2_TRANSITION, PHP_V2_TRANSITION, @@ -212,7 +212,7 @@ public class TestUtils { servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); servers.put(GO_V3_TRANSITION, new LanguageServerTarget(GO_V3_TRANSITION, "8095")); - // servers.put(NET_V2_TRANSITION, new LanguageServerTarget(NET_V2_TRANSITION, "8096")); + servers.put(NET_V2_TRANSITION, new LanguageServerTarget(NET_V2_TRANSITION, "8096")); servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); servers.put(JAVA_V4, new LanguageServerTarget(JAVA_V4, "8088")); diff --git a/test-server/net-v2-transition-server/.duvet/.gitignore b/test-server/net-v2-transition-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/net-v2-transition-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/net-v2-transition-server/.duvet/config.toml b/test-server/net-v2-transition-server/.duvet/config.toml new file mode 100644 index 00000000..416dcfb9 --- /dev/null +++ b/test-server/net-v2-transition-server/.duvet/config.toml @@ -0,0 +1,27 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "**/*.cs" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/client.md" +[[specification]] +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 + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/net-v2-transition-server/.gitignore b/test-server/net-v2-transition-server/.gitignore new file mode 100644 index 00000000..4c20cbc8 --- /dev/null +++ b/test-server/net-v2-transition-server/.gitignore @@ -0,0 +1,44 @@ +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# NuGet Packages +*.nupkg +*.snupkg +packages/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# VS Code +.vscode/ + +# macOS +.DS_Store + +# Temporary files +*.tmp +*.temp diff --git a/test-server/net-v2-transition-server/Controllers/ClientController.cs b/test-server/net-v2-transition-server/Controllers/ClientController.cs new file mode 100644 index 00000000..135a7db9 --- /dev/null +++ b/test-server/net-v2-transition-server/Controllers/ClientController.cs @@ -0,0 +1,128 @@ +using System.Net; +using System.Security.Cryptography; +using System.Text.Json; +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Microsoft.AspNetCore.Mvc; +using NetV2TransitionServer.Models; +using NetV2TransitionServer.Services; + +namespace NetV2TransitionServer.Controllers; + +[ApiController] +[Route("[controller]")] +public class ClientController(IClientCacheService clientCacheService, ILogger logger) : ControllerBase +{ + [HttpPost] + public IActionResult CreateClient([FromBody] ClientRequest request) + { + // Return 501 for not implemented features by the server + if (request.Config.EnableDelayedAuthenticationMode) + return StatusCode(501, new GenericServerError { Message = "EnableDelayedAuthenticationMode not supported" }); + if (request.Config.SetBufferSize.HasValue) + return StatusCode(501, new GenericServerError { Message = "SetBufferSize not supported" }); + + try + { + EncryptionMaterialsV2 encryptionMaterial; + if (request.Config.KeyMaterial.KmsKeyId != null) + { + // The POST request does not contain encryption context. + // However, encryption context is a required field when using KMS. + // So, we are passing empty dictionary. + var encryptionContext = new Dictionary(); + var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); + logger.LogInformation( + "[NET-V2-Transitional] Created EncryptionMaterialsV2: KMS={KmsKeyId}", + kmsKeyId); + } + 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-V2-Transitional] Created EncryptionMaterialsV2: AES"); + } + else if (request.Config.KeyMaterial.RsaKey != null) + { + var rsaKeyBytes = request.Config.KeyMaterial.RsaKey; + var rsaKey = RSA.Create(); + rsaKey.ImportPkcs8PrivateKey(new ReadOnlySpan(rsaKeyBytes), out _); + encryptionMaterial = new EncryptionMaterialsV2(rsaKey, AsymmetricAlgorithmType.RsaOaepSha1); + logger.LogInformation( + "Created EncryptionMaterialsV2: RSA"); + } else + { + return StatusCode(501, new GenericServerError { Message = "Unknown or missing key material!" }); + } + + var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; + var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; + var commitmentPolicy = MapCommitmentPolicy(request.Config.CommitmentPolicy); + + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot + var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; + var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; + logger.LogInformation("[NET-V2-Transitional] Created securityProfile= {securityProfile}", securityProfile.ToString()); + + var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); + // var encryptionAlgorithm = commitmentPolicy == Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt ? ContentEncryptionAlgorithm.AesGcm : ContentEncryptionAlgorithm.AesGcmWithCommitment; + logger.LogInformation("[NET-V2-Transitional] Created commitmentPolicy= {commitmentPolicy}", commitmentPolicy); + logger.LogInformation("[NET-V2-Transitional] Created encryptionAlgorithm= {encryptionAlgorithm}", encryptionAlgorithm); + + var configuration = new AmazonS3CryptoConfigurationV2(securityProfile, commitmentPolicy, encryptionAlgorithm); + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) + { + configuration.StorageMode = CryptoStorageMode.InstructionFile; + logger.LogInformation("[NET-V2-Transitional] Created StorageMode= InstructionFile"); + } + // Create S3 encryption client + var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); + // Add to cache and return client ID + var clientId = clientCacheService.AddClient(encryptionClient); + var response = new ClientResponse { ClientId = clientId }; + + logger.LogInformation("[NET-V2-Transitional] Created S3EC client with ID: {clientId}", clientId); + + return new ContentResult + { + Content = JsonSerializer.Serialize(response), + ContentType = "application/json", + StatusCode = 200 + }; + } + catch (Exception ex) + { + logger.LogError(ex, "[NET-V2-Transitional] Failed to create S3EC client"); + return StatusCode(500, new S3EncryptionClientError + { + Message = $"Failed to create client: {ex.Message}" + }); + } + } + + private static Amazon.Extensions.S3.Encryption.CommitmentPolicy MapCommitmentPolicy(Models.CommitmentPolicy? policy) + { + return policy switch + { + Models.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT => Amazon.Extensions.S3.Encryption.CommitmentPolicy.RequireEncryptRequireDecrypt, + Models.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT => Amazon.Extensions.S3.Encryption.CommitmentPolicy.RequireEncryptAllowDecrypt, + Models.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT => Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt, + _ => Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt + }; + } + + // This is redundant but useful when tests starts sending EncryptionAlgorithm + private static ContentEncryptionAlgorithm MapEncryptionAlgorithm(Models.EncryptionAlgorithm? algorithm) + { + return algorithm switch + { + Models.EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF => ContentEncryptionAlgorithm.AesGcm, + Models.EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => ContentEncryptionAlgorithm.AesGcmWithCommitment, + _ => ContentEncryptionAlgorithm.AesGcm + }; + } +} \ No newline at end of file diff --git a/test-server/net-v2-transition-server/Controllers/ObjectController.cs b/test-server/net-v2-transition-server/Controllers/ObjectController.cs new file mode 100644 index 00000000..d9eda828 --- /dev/null +++ b/test-server/net-v2-transition-server/Controllers/ObjectController.cs @@ -0,0 +1,105 @@ +using System.Text.Json; +using Amazon.S3.Model; +using Microsoft.AspNetCore.Mvc; +using NetV2TransitionServer.Models; +using NetV2TransitionServer.Services; + +namespace NetV2TransitionServer.Controllers; + +[ApiController] +[Route("[controller]")] +public class ObjectController(IClientCacheService clientCacheService, ILogger logger) : ControllerBase +{ + [HttpPut("{bucket}/{key}")] + public async Task PutObject(string bucket, string key) + { + logger.LogInformation("Starting PutObject"); + var clientId = Request.Headers["clientId"].FirstOrDefault(); + if (string.IsNullOrEmpty(clientId)) + return BadRequest(new GenericServerError { Message = "[NET-V2-Transitional] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V2-Transitional] No client found for ClientID: {clientId}" }); + + try + { + // Read raw body data + using var memoryStream = new MemoryStream(); + // Request is the HTTP request this method is currently handling + await Request.Body.CopyToAsync(memoryStream); + var bodyBytes = memoryStream.ToArray(); + + // Create put request + var putRequest = new PutObjectRequest + { + BucketName = bucket, + Key = key, + InputStream = new MemoryStream(bodyBytes) + }; + + await client.PutObjectAsync(putRequest); + + var response = new { bucket, key }; + + logger.LogInformation( + "[NET-V2-Transitional] Put object succeeded for bucket={bucket}, key={key} and clientId = {clientId}", + bucket, key, clientId); + return new ContentResult + { + Content = JsonSerializer.Serialize(response), + ContentType = "application/json", + StatusCode = 200 + }; + } + catch (Exception ex) + { + logger.LogError(ex, "[NET-V2-Transitional] Failed to put object from S3 for bucket={bucket}, key={key}", bucket, key); + return StatusCode(500, new S3EncryptionClientError { Message = $"[NET-V2-Transitional] Failed to put object: {ex.Message}" }); + } + } + + [HttpGet("{bucket}/{key}")] + public async Task GetObject(string bucket, string key) + { + logger.LogInformation("Starting GetObject"); + var clientId = Request.Headers["clientId"].FirstOrDefault(); + if (string.IsNullOrEmpty(clientId)) + return BadRequest(new GenericServerError { Message = "[NET-V2-Transitional] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V2-Transitional] No client found for ClientID: {clientId}" }); + + try + { + var getRequest = new GetObjectRequest + { + BucketName = bucket, + Key = key + }; + var response = await client.GetObjectAsync(getRequest); + logger.LogInformation("[NET-V2-Transitional] Got object from S3 for bucket={bucket}, key={key}", bucket, key); + // Read response body + using var memoryStream = new MemoryStream(); + await response.ResponseStream.CopyToAsync(memoryStream); + var bodyBytes = memoryStream.ToArray(); + + // Convert metadata to content-metadata header format + var metadataList = response.Metadata.Keys + .Select(metaDataKey => $"{metaDataKey}={response.Metadata[metaDataKey]}") + .ToList(); + var metadataStr = string.Join(",", metadataList); + + // Set response headers + Response.Headers["Content-Metadata"] = metadataStr; + + return File(bodyBytes, "application/octet-stream"); + } + catch (Exception ex) + { + logger.LogError(ex, "[NET-V2-Transitional] Failed to get object from S3 for bucket={bucket}, key={key}", bucket, key); + return StatusCode(500, new S3EncryptionClientError { Message = ex.Message }); + } + } +} \ No newline at end of file diff --git a/test-server/net-v2-transition-server/Makefile b/test-server/net-v2-transition-server/Makefile new file mode 100644 index 00000000..65a4339d --- /dev/null +++ b/test-server/net-v2-transition-server/Makefile @@ -0,0 +1,43 @@ +# Makefile for S3 Encryption Client .NET Testing + +.PHONY: build-server start-server stop-server wait-for-server + +PID_FILE_NET_V2_TRANSITION := net-v2-transition-server.pid +PORT_NET_V2_TRANSITION := 8096 + +build-server: + @echo "Building .NET V2 transition server..." + dotnet build + +start-server: + $(MAKE) start-net-v2-transition-server + +stop-server: + @echo "Stopping .NET V2 Transition server on port $(PORT_NET_V2_TRANSITION)..." + @lsof -ti:$(PORT_NET_V2_TRANSITION) | xargs kill -9 2>/dev/null || true + @if [ -f $(PID_FILE_NET_V2_TRANSITION) ]; then \ + pkill -P $$(cat $(PID_FILE_NET_V2_TRANSITION)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE_NET_V2_TRANSITION)) 2>/dev/null || true; \ + rm -f $(PID_FILE_NET_V2_TRANSITION); \ + fi + @rm -f server.log + @echo "Server stopped" + +# Start .NET V2 transition server in background +start-net-v2-transition-server: + @echo "Starting .NET V2 transition server..." + AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \ + AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ + AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ + AWS_REGION="us-west-2" \ + dotnet run > server.log 2>&1 & echo $$! > $(PID_FILE_NET_V2_TRANSITION) + @echo ".NET V2 transition server starting..." + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2_TRANSITION) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/net-v2-transition-server/Models/ClientRequest.cs b/test-server/net-v2-transition-server/Models/ClientRequest.cs new file mode 100644 index 00000000..7cdde249 --- /dev/null +++ b/test-server/net-v2-transition-server/Models/ClientRequest.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace NetV2TransitionServer.Models; + +public class ClientRequest +{ + [Required] + public ClientConfig Config { get; set; } = new(); +} + +public class ClientConfig +{ + public bool EnableLegacyUnauthenticatedModes { get; set; } = false; + public bool EnableLegacyWrappingAlgorithms { get; set; } = false; + public bool EnableDelayedAuthenticationMode { get; set; } = false; + public long? SetBufferSize { get; set; } + [Required] + public KeyMaterial KeyMaterial { get; set; } = new(); + [JsonPropertyName("commitmentPolicy")] + public CommitmentPolicy? CommitmentPolicy { get; set; } + [JsonPropertyName("encryptionAlgorithm")] + public EncryptionAlgorithm? EncryptionAlgorithm { get; set; } + public InstructionFileConfig? InstructionFileConfig { get; set; } +} + +public class KeyMaterial +{ + public byte[]? RsaKey { get; set; } + public byte[]? AesKey { get; set; } + public string? KmsKeyId { get; set; } +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CommitmentPolicy +{ + REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + REQUIRE_ENCRYPT_ALLOW_DECRYPT, + FORBID_ENCRYPT_ALLOW_DECRYPT +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum EncryptionAlgorithm +{ + ALG_AES_256_CBC_IV16_NO_KDF, + ALG_AES_256_GCM_IV12_TAG16_NO_KDF, + ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY +} + +public class InstructionFileConfig +{ + public string? ClientId { get; set; } + public bool EnableInstructionFilePutObject { get; set; } = false; + public bool DisableInstructionFile { get; set; } = false; +} \ No newline at end of file diff --git a/test-server/net-v2-transition-server/Models/ClientResponse.cs b/test-server/net-v2-transition-server/Models/ClientResponse.cs new file mode 100644 index 00000000..cb91c1f5 --- /dev/null +++ b/test-server/net-v2-transition-server/Models/ClientResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace NetV2TransitionServer.Models; + +public class ClientResponse +{ + [JsonPropertyName("clientId")] public string ClientId { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/test-server/net-v2-transition-server/Models/ErrorModels.cs b/test-server/net-v2-transition-server/Models/ErrorModels.cs new file mode 100644 index 00000000..11b45599 --- /dev/null +++ b/test-server/net-v2-transition-server/Models/ErrorModels.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace NetV2TransitionServer.Models; + +public class GenericServerError +{ + [JsonPropertyName("__type")] + public string Type { get; set; } = "software.amazon.encryption.s3#GenericServerError"; + public string Message { get; set; } = string.Empty; +} + +public class S3EncryptionClientError +{ + [JsonPropertyName("__type")] + public string Type { get; set; } = "software.amazon.encryption.s3#S3EncryptionClientError"; + public string Message { get; set; } = string.Empty; +} diff --git a/test-server/net-v2-transition-server/NetV2TransitionServer.csproj b/test-server/net-v2-transition-server/NetV2TransitionServer.csproj new file mode 100644 index 00000000..1f91ea9a --- /dev/null +++ b/test-server/net-v2-transition-server/NetV2TransitionServer.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + false + + + + false + + + + + + + + + + + + + + + + diff --git a/test-server/net-v2-transition-server/Program.cs b/test-server/net-v2-transition-server/Program.cs new file mode 100644 index 00000000..d905e470 --- /dev/null +++ b/test-server/net-v2-transition-server/Program.cs @@ -0,0 +1,17 @@ +using NetV2TransitionServer.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddSingleton(); + +const int port = 8096; + +builder.WebHost.UseUrls($"http://localhost:{port}"); + +var app = builder.Build(); + +app.MapControllers(); + +Console.WriteLine($"Starting server on port {port}"); +app.Run(); diff --git a/test-server/net-v2-transition-server/README.md b/test-server/net-v2-transition-server/README.md new file mode 100644 index 00000000..aeb6e8d5 --- /dev/null +++ b/test-server/net-v2-transition-server/README.md @@ -0,0 +1,65 @@ +# Net-V2-transition-Server + +A .NET test server for Amazon S3 encryption client .NET v2 transition. + +## Project Structure + +``` +├── Controllers/ # API controllers +├── Models/ # Data models +├── Services/ # Business logic services +├── Program.cs # Application entry point +├── NetV2TransitionServer.csproj # Project file +└── README.md # This file +``` + +## Running the Server + +For S3 Encryption Client v2 transition (runs on port 8096): + +```bash +dotnet run +``` + +## API Endpoints + +### Client Management + +- `POST /Client` - Create a new S3 encryption client + +### Object Operations + +- `PUT /{bucket}/{key}` - Upload an encrypted object to S3 +- `GET /{bucket}/{key}` - Download and decrypt an object from S3 + +All object operations require a `clientId` header to specify which client to use. + +## Example Usage + +### Create a Client + +```bash +curl -i -X POST \ + -H "Content-Type: application/json" \ + -H "User-Agent: smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2" \ + -d '{"config":{"enableLegacyUnauthenticatedModes":true,"enableLegacyWrappingAlgorithms":true,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}, "encryptionContext": {"abc": "b"}, "CommitmentPolicy":"REQUIRE_ENCRYPT_REQUIRE_DECRYPT"}}' \ + http://localhost:8096/client +``` + +### Upload an Object + +```bash +curl -X PUT \ + -H "clientid: 7978763a-a02b-4dea-a5d4-78ef11d13d12" \ + -H "content-type: application/octet-stream" \ + -d "simple-test-input-net" \ + http://localhost:8083/object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-dotnet +``` + +### Download an Object + +```bash +curl -X GET \ + -H "clientid: 7978763a-a02b-4dea-a5d4-78ef11d13d12" \ + http://localhost:8083/object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-dotnet +``` diff --git a/test-server/net-v2-transition-server/Services/ClientCacheService.cs b/test-server/net-v2-transition-server/Services/ClientCacheService.cs new file mode 100644 index 00000000..e8f09b89 --- /dev/null +++ b/test-server/net-v2-transition-server/Services/ClientCacheService.cs @@ -0,0 +1,28 @@ +using Amazon.Extensions.S3.Encryption; +using System.Collections.Concurrent; + +namespace NetV2TransitionServer.Services; + +public interface IClientCacheService +{ + string AddClient(AmazonS3EncryptionClientV2 client); + AmazonS3EncryptionClientV2? GetClient(string clientId); +} + +public class ClientCacheService : IClientCacheService +{ + private readonly ConcurrentDictionary _clients = new(); + + public string AddClient(AmazonS3EncryptionClientV2 client) + { + var clientId = Guid.NewGuid().ToString(); + _clients[clientId] = client; + return clientId; + } + + public AmazonS3EncryptionClientV2? GetClient(string clientId) + { + _clients.TryGetValue(clientId, out var client); + return client; + } +} diff --git a/test-server/net-v2-transition-server/s3ec-v2-transition-branch b/test-server/net-v2-transition-server/s3ec-v2-transition-branch new file mode 160000 index 00000000..034a2686 --- /dev/null +++ b/test-server/net-v2-transition-server/s3ec-v2-transition-branch @@ -0,0 +1 @@ +Subproject commit 034a2686c8d08cdd8079cc3f95fe1aaf8e3f1a77