From 0d652b921b8bb1bfcd4dc5629b013dcab9e702e0 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Mon, 15 Sep 2025 16:02:10 -0700 Subject: [PATCH 001/201] docs --- test-server/net-v3-server/.gitignore | 44 ++++++++++++++++++++++++++++ test-server/net-v3-server/README.md | 0 2 files changed, 44 insertions(+) create mode 100644 test-server/net-v3-server/.gitignore create mode 100644 test-server/net-v3-server/README.md diff --git a/test-server/net-v3-server/.gitignore b/test-server/net-v3-server/.gitignore new file mode 100644 index 00000000..4c20cbc8 --- /dev/null +++ b/test-server/net-v3-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-v3-server/README.md b/test-server/net-v3-server/README.md new file mode 100644 index 00000000..e69de29b From fe1e1ffbfca85a8cd766fdbfe03f4260f7599c79 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Mon, 15 Sep 2025 16:03:27 -0700 Subject: [PATCH 002/201] init server --- test-server/net-v3-server/NetV3Server.sln | 27 ++++++++++++ .../src/NetV3Server/Models/ClientRequest.cs | 19 +++++++++ .../src/NetV3Server/Models/ClientResponse.cs | 6 +++ .../src/NetV3Server/Models/ErrorModels.cs | 13 ++++++ .../src/NetV3Server/NetV3Server.csproj | 15 +++++++ .../src/NetV3Server/NetV3Server.http | 6 +++ .../net-v3-server/src/NetV3Server/Program.cs | 42 +++++++++++++++++++ .../Properties/launchSettings.json | 31 ++++++++++++++ .../Services/ClientCacheService.cs | 29 +++++++++++++ .../NetV3Server/appsettings.Development.json | 8 ++++ .../src/NetV3Server/appsettings.json | 10 +++++ 11 files changed, 206 insertions(+) create mode 100644 test-server/net-v3-server/NetV3Server.sln create mode 100644 test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs create mode 100644 test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs create mode 100644 test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs create mode 100644 test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj create mode 100644 test-server/net-v3-server/src/NetV3Server/NetV3Server.http create mode 100644 test-server/net-v3-server/src/NetV3Server/Program.cs create mode 100644 test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json create mode 100644 test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs create mode 100644 test-server/net-v3-server/src/NetV3Server/appsettings.Development.json create mode 100644 test-server/net-v3-server/src/NetV3Server/appsettings.json diff --git a/test-server/net-v3-server/NetV3Server.sln b/test-server/net-v3-server/NetV3Server.sln new file mode 100644 index 00000000..8b1dce15 --- /dev/null +++ b/test-server/net-v3-server/NetV3Server.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6AD6877B-F15F-4061-B27F-9687964F5565}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetV3Server", "src\NetV3Server\NetV3Server.csproj", "{6D8C57A3-9343-42EF-8631-5B76808B8D4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6D8C57A3-9343-42EF-8631-5B76808B8D4E} = {6AD6877B-F15F-4061-B27F-9687964F5565} + EndGlobalSection +EndGlobal diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs new file mode 100644 index 00000000..ed578b70 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs @@ -0,0 +1,19 @@ +namespace NetV3Server.Models; + +public class ClientRequest +{ + public ClientConfig Config { get; set; } = new(); +} + +public class ClientConfig +{ + public bool EnableLegacyUnauthenticatedModes { get; set; } + public bool EnableDelayedAuthenticationMode { get; set; } + public bool EnableLegacyWrappingAlgorithms { get; set; } + public KeyMaterial KeyMaterial { get; set; } = new(); +} + +public class KeyMaterial +{ + public string KmsKeyId { get; set; } = string.Empty; +} diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs new file mode 100644 index 00000000..a56c0d56 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs @@ -0,0 +1,6 @@ +namespace NetV3Server.Models; + +public class ClientResponse +{ + public string ClientId { get; set; } = string.Empty; +} diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs b/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs new file mode 100644 index 00000000..755747fc --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs @@ -0,0 +1,13 @@ +namespace NetV3Server.Models; + +public class GenericServerError +{ + public string __type { get; set; } = "software.amazon.encryption.s3#GenericServerError"; + public string Message { get; set; } = string.Empty; +} + +public class S3EncryptionClientError +{ + public string __type { get; set; } = "software.amazon.encryption.s3#S3EncryptionClientError"; + public string Message { get; set; } = string.Empty; +} diff --git a/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj b/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj new file mode 100644 index 00000000..7c674279 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/test-server/net-v3-server/src/NetV3Server/NetV3Server.http b/test-server/net-v3-server/src/NetV3Server/NetV3Server.http new file mode 100644 index 00000000..987266e6 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/NetV3Server.http @@ -0,0 +1,6 @@ +@NetV3Server_HostAddress = http://localhost:5251 + +GET {{NetV3Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/test-server/net-v3-server/src/NetV3Server/Program.cs b/test-server/net-v3-server/src/NetV3Server/Program.cs new file mode 100644 index 00000000..9f135aaf --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Program.cs @@ -0,0 +1,42 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json b/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json new file mode 100644 index 00000000..347d5a85 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39730", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs b/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs new file mode 100644 index 00000000..00e14a33 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs @@ -0,0 +1,29 @@ +using Amazon.S3; +using Amazon.Extensions.S3.Encryption; +using System.Collections.Concurrent; + +namespace NetV3Server.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-v3-server/src/NetV3Server/appsettings.Development.json b/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json new file mode 100644 index 00000000..ff66ba6b --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test-server/net-v3-server/src/NetV3Server/appsettings.json b/test-server/net-v3-server/src/NetV3Server/appsettings.json new file mode 100644 index 00000000..215db1d6 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Urls": "http://localhost:8084" +} From 8522b26c682d566743c7439a046afd99f19ab238 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 10:31:18 -0700 Subject: [PATCH 003/201] m --- test-server/Makefile | 33 +- test-server/go-server/.gitignore | 48 +++ test-server/go-server/Makefile | 69 +++ test-server/go-server/README.md | 148 +++++++ test-server/go-server/go.mod | 31 ++ test-server/go-server/go.sum | 46 ++ test-server/go-server/main.go | 395 ++++++++++++++++++ .../amazon/encryption/s3/RoundTripTests.java | 77 +++- 8 files changed, 833 insertions(+), 14 deletions(-) create mode 100644 test-server/go-server/.gitignore create mode 100644 test-server/go-server/Makefile create mode 100644 test-server/go-server/README.md create mode 100644 test-server/go-server/go.mod create mode 100644 test-server/go-server/go.sum create mode 100644 test-server/go-server/main.go diff --git a/test-server/Makefile b/test-server/Makefile index afbe97b5..60326393 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-server start-java-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers start-python-server start-java-server start-go-server run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -35,16 +35,28 @@ start-java-server: ./gradlew --build-cache --parallel run & echo $$! > ../java-server.pid @echo "Java server starting..." -# Start both servers in parallel +# Start Go server in background +start-go-server: + @echo "Starting Go server..." + cd go-server && \ + go mod tidy && \ + 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" \ + go run . & echo $$! > ../go-server.pid + @echo "Go server starting..." + +# Start all servers in parallel start-servers: @echo "Starting servers in parallel..." - @$(MAKE) -j2 start-python-server start-java-server + @$(MAKE) -j3 start-python-server start-java-server start-go-server @echo "Waiting for servers to be ready..." @for i in $$(seq 1 360); do \ - if nc -z localhost 8080 && nc -z localhost 8081; then \ + if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082; then \ echo "Ports are open, waiting for servers to initialize..."; \ sleep 5; \ - echo "Both servers are ready!"; \ + echo "All servers are ready!"; \ break; \ fi; \ if [ $$i -eq 360 ]; then \ @@ -66,7 +78,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 --parallel integ + ./gradlew --build-cache --parallel -i integ @echo "Tests completed successfully" # Stop the servers @@ -80,12 +92,16 @@ stop-servers: kill $$(cat java-server.pid) 2>/dev/null || true; \ rm java-server.pid; \ fi + @if [ -f go-server.pid ]; then \ + kill $$(cat go-server.pid) 2>/dev/null || true; \ + rm go-server.pid; \ + fi @echo "Servers stopped" # Clean up logs and pid files clean: stop-servers @echo "Cleaning up..." - @rm -f python-server.log java-server.log + @rm -f python-server.log java-server.log go-server.log @echo "Cleanup complete" # Help target @@ -93,9 +109,10 @@ help: @echo "Available targets:" @echo " all : Start servers and run tests (default, output to stdout)" @echo " ci : Run in CI mode (start servers, run tests, stop servers)" - @echo " start-servers : Start Python and Java servers in parallel (output to stdout)" + @echo " start-servers : Start Python, Java, and Go servers in parallel (output to stdout)" @echo " start-python-server: Start only the Python server" @echo " start-java-server : Start only the Java server" + @echo " start-go-server : Start only the Go server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" @echo " clean : Stop servers and clean up logs" diff --git a/test-server/go-server/.gitignore b/test-server/go-server/.gitignore new file mode 100644 index 00000000..211699c4 --- /dev/null +++ b/test-server/go-server/.gitignore @@ -0,0 +1,48 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Build output +bin/ +dist/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log + +# Coverage reports +coverage.out +coverage.html + +# Air (live reload) temporary files +tmp/ diff --git a/test-server/go-server/Makefile b/test-server/go-server/Makefile new file mode 100644 index 00000000..bb7eff2d --- /dev/null +++ b/test-server/go-server/Makefile @@ -0,0 +1,69 @@ +# Go Server Makefile + +.PHONY: build run clean test deps + +# Default target +all: build + +# Build the Go server +build: + go build -o bin/go-server . + +# Run the server +run: build + ./bin/go-server + +# Install dependencies +deps: + go mod tidy + go mod download + +# Clean build artifacts +clean: + rm -rf bin/ + go clean + +# Run tests (when we add them) +test: + go test ./... + +# Format code +fmt: + go fmt ./... + +# Vet code +vet: + go vet ./... + +# Run linter (requires golangci-lint) +lint: + golangci-lint run + +# Development server with auto-reload (requires air) +dev: + air + +# Check for security vulnerabilities +security: + gosec ./... + +# Generate coverage report +coverage: + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +# Help +help: + @echo "Available targets:" + @echo " build - Build the Go server" + @echo " run - Build and run the server" + @echo " deps - Install dependencies" + @echo " clean - Clean build artifacts" + @echo " test - Run tests" + @echo " fmt - Format code" + @echo " vet - Vet code" + @echo " lint - Run linter" + @echo " dev - Run development server with auto-reload" + @echo " security - Check for security vulnerabilities" + @echo " coverage - Generate test coverage report" + @echo " help - Show this help message" diff --git a/test-server/go-server/README.md b/test-server/go-server/README.md new file mode 100644 index 00000000..b6cfef17 --- /dev/null +++ b/test-server/go-server/README.md @@ -0,0 +1,148 @@ +# Go Server for S3 Encryption Client Test Framework + +This is a Go implementation of the S3 Encryption Client test server, part of the S3EC Generalized Robust Test Framework Machine (G-RTFM). + +## Overview + +The Go server implements the same Smithy-defined API as the Java and Python servers, providing: + +- **CreateClient**: Creates and configures S3 encryption clients +- **PutObject**: Handles encrypted object uploads to S3 +- **GetObject**: Handles encrypted object downloads from S3 + +## Architecture + +The server is built using: + +- **HTTP Framework**: Gorilla Mux for routing +- **AWS SDK**: AWS SDK for Go v2 for S3 and KMS operations +- **Concurrency**: Thread-safe client caching with sync.RWMutex +- **Error Handling**: Smithy-compliant error responses + +## API Endpoints + +### POST /client +Creates a new S3 encryption client with the provided configuration. + +**Request Body:** +```json +{ + "config": { + "enableLegacyUnauthenticatedModes": false, + "enableDelayedAuthenticationMode": false, + "enableLegacyWrappingAlgorithms": false, + "setBufferSize": 1024, + "keyMaterial": { + "rsaKey": "...", + "aesKey": "...", + "kmsKeyId": "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012" + } + } +} +``` + +**Response:** +```json +{ + "clientId": "uuid-string" +} +``` + +### PUT /object/{bucket}/{key} +Uploads an encrypted object to S3 using the specified client. + +**Headers:** +- `ClientID`: The client ID returned from CreateClient +- `Content-Metadata`: Encryption context metadata (optional) + +**Request Body:** Raw object data + +**Response:** +```json +{ + "bucket": "bucket-name", + "key": "object-key", + "metadata": [] +} +``` + +### GET /object/{bucket}/{key} +Downloads and decrypts an object from S3 using the specified client. + +**Headers:** +- `ClientID`: The client ID returned from CreateClient +- `Content-Metadata`: Encryption context metadata (optional) + +**Response:** Raw object data with `Content-Metadata` header + +## Building and Running + +### Prerequisites + +- Go 1.21 or later +- AWS credentials configured (via AWS CLI, environment variables, or IAM roles) + +### Build + +```bash +# Install dependencies +make deps + +# Build the server +make build + +# Or build and run +make run +``` + +### Development + +```bash +# Format code +make fmt + +# Vet code +make vet + +# Run tests +make test + +# Clean build artifacts +make clean +``` + +## Configuration + +The server runs on port 8082 by default and uses the `us-west-2` AWS region. These can be modified in the source code if needed. + +## Error Handling + +The server implements Smithy-compliant error responses: + +- **GenericServerError**: For internal server errors +- **S3EncryptionClientError**: For S3 encryption client specific errors + +## Implementation Notes + +- **Client Caching**: Clients are stored in memory with UUID keys for thread-safe access +- **Metadata Handling**: Follows the same metadata string format as Java/Python servers +- **AWS Integration**: Uses AWS SDK v2 for modern Go AWS operations +- **Concurrency**: Safe for concurrent requests with proper mutex usage + +## Limitations + +- This is a basic implementation that uses standard S3 clients rather than full S3 encryption clients +- In a production implementation, you would integrate with the actual S3 encryption client library +- Memory-based client storage (not persistent across restarts) + +## Testing + +The server is designed to work with the existing test framework. It should be compatible with the Java tests that validate the Smithy API contract. + +## Future Enhancements + +- Integration with actual S3 encryption client library +- Persistent client storage +- Enhanced logging and metrics +- Configuration file support +- Health check endpoints diff --git a/test-server/go-server/go.mod b/test-server/go-server/go.mod new file mode 100644 index 00000000..014a64da --- /dev/null +++ b/test-server/go-server/go.mod @@ -0,0 +1,31 @@ +module github.com/aws/amazon-s3-encryption-client-python/test-server/go-server + +go 1.21 + +require ( + github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 + github.com/google/uuid v1.5.0 + github.com/gorilla/mux v1.8.1 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) diff --git a/test-server/go-server/go.sum b/test-server/go-server/go.sum new file mode 100644 index 00000000..4fc073e0 --- /dev/null +++ b/test-server/go-server/go.sum @@ -0,0 +1,46 @@ +github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 h1:P4dOTmTkEb8Dj/LuAoA4bqRZZrDq4DqZQI88vdMaj18= +github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0/go.mod h1:olnwkBTbWjaJCaGOHohvJu98q40GiJZuDHLXj751mII= +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/test-server/go-server/main.go b/test-server/go-server/main.go new file mode 100644 index 00000000..23025cbd --- /dev/null +++ b/test-server/go-server/main.go @@ -0,0 +1,395 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/aws/amazon-s3-encryption-client-go/v3/client" + "github.com/aws/amazon-s3-encryption-client-go/v3/materials" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" + "github.com/gorilla/mux" +) + +// Server represents the Go test server +type Server struct { + clientCache map[string]*client.S3EncryptionClientV3 + kmsClient *kms.Client +} + +// CreateClientInput represents the input for creating a client +type CreateClientInput struct { + Config S3ECConfig `json:"config"` +} + +// CreateClientOutput represents the output for creating a client +type CreateClientOutput struct { + ClientID string `json:"clientId"` +} + +// S3ECConfig represents the S3 encryption client configuration +type S3ECConfig struct { + EnableLegacyUnauthenticatedModes bool `json:"enableLegacyUnauthenticatedModes"` + EnableDelayedAuthenticationMode bool `json:"enableDelayedAuthenticationMode"` + EnableLegacyWrappingAlgorithms bool `json:"enableLegacyWrappingAlgorithms"` + SetBufferSize int64 `json:"setBufferSize"` + KeyMaterial KeyMaterial `json:"keyMaterial"` +} + +// KeyMaterial represents the key material for encryption +type KeyMaterial struct { + RSAKey []byte `json:"rsaKey"` + AESKey []byte `json:"aesKey"` + KMSKeyID string `json:"kmsKeyId"` +} + +// PutObjectOutput represents the output for put object operation +type PutObjectOutput struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + Metadata []string `json:"metadata"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Type string `json:"__type"` + Message string `json:"message"` +} + +// NewServer creates a new server instance +func NewServer() (*Server, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + return &Server{ + clientCache: make(map[string]*client.S3EncryptionClientV3), + kmsClient: kms.NewFromConfig(cfg), + }, nil +} + +// createGenericServerError creates a generic server error response +func (s *Server) createGenericServerError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("GenericServerError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#GenericServerError", + Message: message, + }) +} + +// createS3EncryptionClientError creates an S3 encryption client error response +func (s *Server) createS3EncryptionClientError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("S3EncryptionClientError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#S3EncryptionClientError", + Message: message, + }) +} + +// metadataStringToMap converts metadata string to map +func metadataStringToMap(mdString string) (map[string]string, error) { + md := make(map[string]string) + if mdString == "" { + return md, nil + } + + mdList := strings.Split(mdString, ",") + for _, entry := range mdList { + // Split on "]:[" to separate key and value + parts := strings.Split(entry, "]:[") + if len(parts) == 2 { + // Remove remaining brackets from start and end + key := parts[0][1:] // Remove first character + value := parts[1][:len(parts[1])-1] // Remove last character + md[key] = value + } else { + return nil, fmt.Errorf("malformed metadata list entry: %s", entry) + } + } + return md, nil +} + +// createClient handles POST /client +func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { + log.Printf("CreateClient: Received POST /client request") + + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var input CreateClientInput + if err := json.Unmarshal(body, &input); err != nil { + s.createGenericServerError(w, "Invalid JSON in request body", http.StatusBadRequest) + return + } + + log.Printf("CreateClient: Parsed config - KMSKeyID: %s, EnableLegacyWrappingAlgorithms: %t", + input.Config.KeyMaterial.KMSKeyID, input.Config.EnableLegacyWrappingAlgorithms) + + // Load AWS config + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) + return + } + + // Create KMS keyring based on the provided KMS key ID if available + log.Printf("CreateClient: Creating KMS keyring with key ID: %s", input.Config.KeyMaterial.KMSKeyID) + kmsClient := kms.NewFromConfig(cfg) + keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { + options.EnableLegacyWrappingAlgorithms = input.Config.EnableLegacyWrappingAlgorithms + }) + cmm, err := materials.NewCryptographicMaterialsManager(keyring) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create CMM: %v", err), http.StatusInternalServerError) + return + } + + // Create S3 encryption client + log.Printf("CreateClient: Creating S3 encryption client") + var s3EncryptionClient *client.S3EncryptionClientV3 + s3PlaintextClient := s3.NewFromConfig(cfg) + s3EncryptionClient, err = client.New(s3PlaintextClient, cmm) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create S3EC: %v", err), http.StatusInternalServerError) + return + } + + // Generate client ID + clientID := uuid.New().String() + log.Printf("CreateClient: Generated client ID: %s", clientID) + + // Store client in cache + s.clientCache[clientID] = s3EncryptionClient + log.Printf("CreateClient: Stored client in cache. Total clients: %d", len(s.clientCache)) + + // Return response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CreateClientOutput{ + ClientID: clientID, + }) + log.Printf("CreateClient: Successfully created client %s", clientID) +} + +// putObject handles PUT /object/{bucket}/{key} +func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + log.Printf("PutObject: Received PUT /object/%s/%s request", bucket, key) + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + log.Printf("PutObject: Using client ID: %s", clientID) + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Read body + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + log.Printf("PutObject: Read body of size: %d bytes", len(body)) + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + // Create context with encryption context + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + if len(encCtx) > 0 { + metadataJSON, _ := json.Marshal(encCtx) + log.Printf("PutObject: Using encryption context: %s", string(metadataJSON)) + } else { + log.Printf("PutObject: No encryption context provided") + } + + // Create put object input + putInput := &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + Body: strings.NewReader(string(body)), + } + + // Add metadata if present + if len(encCtx) > 0 { + putInput.Metadata = encCtx + } + + log.Printf("PutObject: Making S3 PutObject request") + // Make the put object request using the encryption client + _, err = client.PutObject(encryptionContext, putInput) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to put object: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + + // Return response + w.Header().Set("Content-Type", "application/json") + response := PutObjectOutput{ + Bucket: bucket, + Key: key, + Metadata: []string{}, // Return empty metadata list as per the model + } + json.NewEncoder(w).Encode(response) + log.Printf("PutObject: Response sent successfully") +} + +// getObject handles GET /object/{bucket}/{key} +func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + log.Printf("GetObject: Received GET /object/%s/%s request", bucket, key) + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + log.Printf("GetObject: Using client ID: %s", clientID) + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + // Create context with encryption context + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + if len(encCtx) > 0 { + metadataJSON, _ := json.Marshal(encCtx) + log.Printf("GetObject: Using encryption context: %s", string(metadataJSON)) + } else { + log.Printf("GetObject: No encryption context provided") + } + + // Create get object input + getInput := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + + log.Printf("GetObject: Making S3 GetObject request") + // Make the get object request using the encryption client + result, err := client.GetObject(encryptionContext, getInput) + if err != nil { + errMsg := err.Error() + // Shim the S3EC error message to the error message expected by the test server + if strings.Contains(errMsg, "to decrypt x-amz-cek-alg value `kms` you must enable legacyWrappingAlgorithms on the keyring") { + s.createS3EncryptionClientError(w, "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms", http.StatusInternalServerError) + return + } + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to get object: %v", err), http.StatusInternalServerError) + return + } + defer result.Body.Close() + + // Read the body + body, err := io.ReadAll(result.Body) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to read object body: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("GetObject: Read body of size: %d bytes", len(body)) + + // Convert metadata to string format + var metadataList []string + if result.Metadata != nil { + for k, v := range result.Metadata { + metadataList = append(metadataList, fmt.Sprintf("%s=%s", k, v)) + } + } + + metadataStr := strings.Join(metadataList, ",") + + if len(metadataList) > 0 { + log.Printf("GetObject: Retrieved metadata: %s", metadataStr) + } else { + log.Printf("GetObject: No metadata found in object") + } + + log.Printf("GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + log.Printf("GetObject: Body content: %s", string(body)) + + // Set response headers + w.Header().Set("Content-Metadata", metadataStr) + + // Return the body as response + w.Write(body) + log.Printf("GetObject: Response sent successfully") +} + +func main() { + server, err := NewServer() + if err != nil { + log.Fatalf("Failed to create server: %v", err) + } + + r := mux.NewRouter() + + // Register routes + r.HandleFunc("/client", server.createClient).Methods("POST") + r.HandleFunc("/object/{bucket}/{key}", server.putObject).Methods("PUT") + r.HandleFunc("/object/{bucket}/{key}", server.getObject).Methods("GET") + + fmt.Println("Starting Go server on :8082...") + log.Fatal(http.ListenAndServe(":8082", r)) +} 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 211269d7..325f43db 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 @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Stream; import com.amazonaws.services.s3.model.KMSEncryptionMaterials; @@ -64,14 +65,23 @@ public class RoundTripTests { static { serverList = new ArrayList<>(2); - serverList.add(new LanguageServerTarget("Java", "8080")); - serverList.add(new LanguageServerTarget("Python", "8081")); + serverList.add(new LanguageServerTarget("Java-V3", "8080")); + serverList.add(new LanguageServerTarget("Python-V3", "8081")); + serverList.add(new LanguageServerTarget("Go-V3", "8082")); serverMap = new HashMap<>(2); - serverMap.put("Java", new LanguageServerTarget("Java", "8080")); - serverMap.put("Python", new LanguageServerTarget("Python", "8081")); + serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080")); + serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081")); + serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); } + // These languages' S3EC implementations do not validate encryption context provided to getObject. + // If the encryption context provided to getObject does not match the encryption context provided to putObject, + // these languages' implementations will not raise an error as expected. + // For now, skip tests that require this validation behavior. + private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = + Set.of("Go-V3"); + static public class LanguageServerTarget { public String getLanguageName() { return languageName; @@ -255,9 +265,59 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("crossLanguageClients") - public void crossLanguageTestKmsWithEncCtxMismatchFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { + public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { + if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + return; + } + S3ECTestServerClient encClient = testServerClientFor(encLang); + final String objectKey = "cross-lang-test-key-kms-ec-subset-fails" + encLang; + final String input = "simple-test-input"; + final Map encCtx = new HashMap<>(); + encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); + encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); + final List mdAsList = metadataMapToList(encCtx); + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(KMS_KEY_ARN) + .build(); + CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn).build()) + .build()); + String encS3ECId = encClientOutput.getClientId(); + + encClient.putObject(PutObjectInput.builder() + .clientID(encS3ECId) + .key(objectKey) + .bucket(BUCKET) + .metadata(mdAsList) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); + S3ECTestServerClient decClient = testServerClientFor(decLang); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn).build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + try { + decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + fail("Expected exception!"); + } catch (S3EncryptionClientError e) { + assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + } + } + + @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") + @MethodSource("crossLanguageClients") + public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { + if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + return; + } S3ECTestServerClient encClient = testServerClientFor(encLang); - final String objectKey = "cross-lang-test-key-kms-ec-mismatch-fails" + encLang; + final String objectKey = "cross-lang-test-key-kms-ec-incorrect-fails" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); @@ -285,11 +345,16 @@ public void crossLanguageTestKmsWithEncCtxMismatchFails(LanguageServerTarget enc .keyMaterial(kmsKeyArn).build()) .build()); String decS3ECId = decClientOutput.getClientId(); + + final Map incorrectEncCtx = new HashMap<>(); + incorrectEncCtx.put("this-is-wrong-ec-key", "bad-value"); + var incorrectMdAsList = metadataMapToList(incorrectEncCtx); try { decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) .bucket(BUCKET) .key(objectKey) + .metadata(incorrectMdAsList) .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { From 833e20a5d9118e05694107a689fc862718fb38ea Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 16 Sep 2025 10:49:22 -0700 Subject: [PATCH 004/201] auto commit --- .../net-v3-server/src/NetV3Server/Models/ClientRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs index ed578b70..3d9729d6 100644 --- a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs @@ -8,8 +8,6 @@ public class ClientRequest public class ClientConfig { public bool EnableLegacyUnauthenticatedModes { get; set; } - public bool EnableDelayedAuthenticationMode { get; set; } - public bool EnableLegacyWrappingAlgorithms { get; set; } public KeyMaterial KeyMaterial { get; set; } = new(); } From 4d44e4f84fb998363e7ebe956e8f51c5eb52d84b Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 11:00:52 -0700 Subject: [PATCH 005/201] m --- test-server/Makefile | 72 ++++----- test-server/README.md | 11 +- test-server/go-server/.gitignore | 48 ------ test-server/go-server/Makefile | 69 -------- test-server/go-server/README.md | 148 ------------------ test-server/go-v3-server/README.md | 32 ++++ .../{go-server => go-v3-server}/go.mod | 0 .../{go-server => go-v3-server}/go.sum | 0 .../{go-server => go-v3-server}/main.go | 10 +- .../amazon/encryption/s3/RoundTripTests.java | 8 +- .../{java-server => java-v3-server}/README.md | 4 +- .../build.gradle.kts | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../{java-server => java-v3-server}/gradlew | 0 .../gradlew.bat | 0 .../license.txt | 0 .../settings.gradle.kts | 0 .../smithy-build.json | 0 .../s3/CreateClientOperationImpl.java | 0 .../encryption/s3/GetObjectOperationImpl.java | 0 .../amazon/encryption/s3/MetadataUtils.java | 0 .../encryption/s3/PutObjectOperationImpl.java | 0 .../encryption/s3/S3ECJavaTestServer.java | 0 .../.gitignore | 0 .../README.md | 0 .../poetry.lock | 0 .../pyproject.toml | 0 .../src/__init__.py | 0 .../src/main.py | 0 .../tests/__init__.py | 0 32 files changed, 88 insertions(+), 314 deletions(-) delete mode 100644 test-server/go-server/.gitignore delete mode 100644 test-server/go-server/Makefile delete mode 100644 test-server/go-server/README.md create mode 100644 test-server/go-v3-server/README.md rename test-server/{go-server => go-v3-server}/go.mod (100%) rename test-server/{go-server => go-v3-server}/go.sum (100%) rename test-server/{go-server => go-v3-server}/main.go (96%) rename test-server/{java-server => java-v3-server}/README.md (70%) rename test-server/{java-server => java-v3-server}/build.gradle.kts (100%) rename test-server/{java-server => java-v3-server}/gradle.properties (100%) rename test-server/{java-server => java-v3-server}/gradle/wrapper/gradle-wrapper.jar (100%) rename test-server/{java-server => java-v3-server}/gradle/wrapper/gradle-wrapper.properties (100%) rename test-server/{java-server => java-v3-server}/gradlew (100%) rename test-server/{java-server => java-v3-server}/gradlew.bat (100%) rename test-server/{java-server => java-v3-server}/license.txt (100%) rename test-server/{java-server => java-v3-server}/settings.gradle.kts (100%) rename test-server/{java-server => java-v3-server}/smithy-build.json (100%) rename test-server/{java-server => java-v3-server}/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java (100%) rename test-server/{java-server => java-v3-server}/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java (100%) rename test-server/{java-server => java-v3-server}/src/main/java/software/amazon/encryption/s3/MetadataUtils.java (100%) rename test-server/{java-server => java-v3-server}/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java (100%) rename test-server/{java-server => java-v3-server}/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java (100%) rename test-server/{python-server => python-v3-server}/.gitignore (100%) rename test-server/{python-server => python-v3-server}/README.md (100%) rename test-server/{python-server => python-v3-server}/poetry.lock (100%) rename test-server/{python-server => python-v3-server}/pyproject.toml (100%) rename test-server/{python-server => python-v3-server}/src/__init__.py (100%) rename test-server/{python-server => python-v3-server}/src/main.py (100%) rename test-server/{python-server => python-v3-server}/tests/__init__.py (100%) diff --git a/test-server/Makefile b/test-server/Makefile index 60326393..4831d68d 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-server start-java-server start-go-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -10,9 +10,9 @@ ci: start-servers run-tests stop-servers # Start Python server in background -start-python-server: - @echo "Starting Python server..." - cd python-server && \ +start-python-v3-server: + @echo "Starting Python V3 server..." + cd python-v3-server && \ python -m venv .venv && \ .venv/bin/python -m ensurepip && \ .venv/bin/python -m pip install -e . && \ @@ -21,36 +21,36 @@ start-python-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - .venv/bin/python src/main.py & echo $$! > ../python-server.pid + .venv/bin/python src/main.py & echo $$! > ../python-v3-server.pid @echo "Python server starting..." # Start Java server in background -start-java-server: - @echo "Starting Java server..." - cd java-server && \ +start-java-v3-server: + @echo "Starting Java V3 server..." + cd java-v3-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" \ - ./gradlew --build-cache --parallel run & echo $$! > ../java-server.pid + ./gradlew --build-cache --parallel run & echo $$! > ../java-v3-server.pid @echo "Java server starting..." # Start Go server in background -start-go-server: - @echo "Starting Go server..." - cd go-server && \ +start-go-v3-server: + @echo "Starting Go V3 server..." + cd go-v3-server && \ go mod tidy && \ 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" \ - go run . & echo $$! > ../go-server.pid + go run . & echo $$! > ../go-v3-server.pid @echo "Go server starting..." # Start all servers in parallel start-servers: @echo "Starting servers in parallel..." - @$(MAKE) -j3 start-python-server start-java-server start-go-server + @$(MAKE) -j3 start-python-v3-server start-java-v3-server start-go-v3-server @echo "Waiting for servers to be ready..." @for i in $$(seq 1 360); do \ if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082; then \ @@ -78,46 +78,46 @@ run-tests: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./gradlew --build-cache --parallel -i integ + ./gradlew --build-cache --rerun-tasks --parallel integ @echo "Tests completed successfully" # Stop the servers stop-servers: @echo "Stopping servers..." - @if [ -f python-server.pid ]; then \ - kill $$(cat python-server.pid) 2>/dev/null || true; \ - rm python-server.pid; \ + @if [ -f python-v3-server.pid ]; then \ + kill $$(cat python-v3-server.pid) 2>/dev/null || true; \ + rm python-v3-server.pid; \ fi - @if [ -f java-server.pid ]; then \ - kill $$(cat java-server.pid) 2>/dev/null || true; \ - rm java-server.pid; \ + @if [ -f java-v3-server.pid ]; then \ + kill $$(cat java-v3-server.pid) 2>/dev/null || true; \ + rm java-v3-server.pid; \ fi - @if [ -f go-server.pid ]; then \ - kill $$(cat go-server.pid) 2>/dev/null || true; \ - rm go-server.pid; \ + @if [ -f go-v3-server.pid ]; then \ + kill $$(cat go-v3-server.pid) 2>/dev/null || true; \ + rm go-v3-server.pid; \ fi @echo "Servers stopped" # Clean up logs and pid files clean: stop-servers @echo "Cleaning up..." - @rm -f python-server.log java-server.log go-server.log + @rm -f python-v3-server.log java-v3-server.log go-v3-server.log @echo "Cleanup complete" # Help target help: @echo "Available targets:" - @echo " all : Start servers and run tests (default, output to stdout)" - @echo " ci : Run in CI mode (start servers, run tests, stop servers)" - @echo " start-servers : Start Python, Java, and Go servers in parallel (output to stdout)" - @echo " start-python-server: Start only the Python server" - @echo " start-java-server : Start only the Java server" - @echo " start-go-server : Start only the Go server" - @echo " run-tests : Run Java tests" - @echo " stop-servers : Stop running servers" - @echo " clean : Stop servers and clean up logs" - @echo " check-env : Check if required environment variables are set" - @echo " help : Show this help message" + @echo " all : Start servers and run tests (default, output to stdout)" + @echo " ci : Run in CI mode (start servers, run tests, stop servers)" + @echo " start-servers : Start all servers in parallel" + @echo " start-python-v3-server : Start only the Python V3 server" + @echo " start-java-v3-server : Start only the Java V3 server" + @echo " start-go-v3-server : Start only the Go V3 server" + @echo " run-tests : Run Java tests" + @echo " stop-servers : Stop running servers" + @echo " clean : Stop servers and clean up logs" + @echo " check-env : Check if required environment variables are set" + @echo " help : Show this help message" # Check if required environment variables are set check-env: diff --git a/test-server/README.md b/test-server/README.md index a320d1d1..4f43f1bf 100644 --- a/test-server/README.md +++ b/test-server/README.md @@ -28,11 +28,14 @@ make ci # Start Python and Java servers in parallel make start-servers -# Start only the Python server -make start-python-server +# Start only the Python S3EC V3 server +make start-python-v3-server -# Start only the Java server -make start-java-server +# Start only the Java S3EC V3 server +make start-java-v3-server + +# Start only the Go S3EC V3 server +make start-go-v3-server # Run Java tests make run-tests diff --git a/test-server/go-server/.gitignore b/test-server/go-server/.gitignore deleted file mode 100644 index 211699c4..00000000 --- a/test-server/go-server/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - -# Build output -bin/ -dist/ - -# IDE files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Logs -*.log - -# Coverage reports -coverage.out -coverage.html - -# Air (live reload) temporary files -tmp/ diff --git a/test-server/go-server/Makefile b/test-server/go-server/Makefile deleted file mode 100644 index bb7eff2d..00000000 --- a/test-server/go-server/Makefile +++ /dev/null @@ -1,69 +0,0 @@ -# Go Server Makefile - -.PHONY: build run clean test deps - -# Default target -all: build - -# Build the Go server -build: - go build -o bin/go-server . - -# Run the server -run: build - ./bin/go-server - -# Install dependencies -deps: - go mod tidy - go mod download - -# Clean build artifacts -clean: - rm -rf bin/ - go clean - -# Run tests (when we add them) -test: - go test ./... - -# Format code -fmt: - go fmt ./... - -# Vet code -vet: - go vet ./... - -# Run linter (requires golangci-lint) -lint: - golangci-lint run - -# Development server with auto-reload (requires air) -dev: - air - -# Check for security vulnerabilities -security: - gosec ./... - -# Generate coverage report -coverage: - go test -coverprofile=coverage.out ./... - go tool cover -html=coverage.out -o coverage.html - -# Help -help: - @echo "Available targets:" - @echo " build - Build the Go server" - @echo " run - Build and run the server" - @echo " deps - Install dependencies" - @echo " clean - Clean build artifacts" - @echo " test - Run tests" - @echo " fmt - Format code" - @echo " vet - Vet code" - @echo " lint - Run linter" - @echo " dev - Run development server with auto-reload" - @echo " security - Check for security vulnerabilities" - @echo " coverage - Generate test coverage report" - @echo " help - Show this help message" diff --git a/test-server/go-server/README.md b/test-server/go-server/README.md deleted file mode 100644 index b6cfef17..00000000 --- a/test-server/go-server/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Go Server for S3 Encryption Client Test Framework - -This is a Go implementation of the S3 Encryption Client test server, part of the S3EC Generalized Robust Test Framework Machine (G-RTFM). - -## Overview - -The Go server implements the same Smithy-defined API as the Java and Python servers, providing: - -- **CreateClient**: Creates and configures S3 encryption clients -- **PutObject**: Handles encrypted object uploads to S3 -- **GetObject**: Handles encrypted object downloads from S3 - -## Architecture - -The server is built using: - -- **HTTP Framework**: Gorilla Mux for routing -- **AWS SDK**: AWS SDK for Go v2 for S3 and KMS operations -- **Concurrency**: Thread-safe client caching with sync.RWMutex -- **Error Handling**: Smithy-compliant error responses - -## API Endpoints - -### POST /client -Creates a new S3 encryption client with the provided configuration. - -**Request Body:** -```json -{ - "config": { - "enableLegacyUnauthenticatedModes": false, - "enableDelayedAuthenticationMode": false, - "enableLegacyWrappingAlgorithms": false, - "setBufferSize": 1024, - "keyMaterial": { - "rsaKey": "...", - "aesKey": "...", - "kmsKeyId": "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012" - } - } -} -``` - -**Response:** -```json -{ - "clientId": "uuid-string" -} -``` - -### PUT /object/{bucket}/{key} -Uploads an encrypted object to S3 using the specified client. - -**Headers:** -- `ClientID`: The client ID returned from CreateClient -- `Content-Metadata`: Encryption context metadata (optional) - -**Request Body:** Raw object data - -**Response:** -```json -{ - "bucket": "bucket-name", - "key": "object-key", - "metadata": [] -} -``` - -### GET /object/{bucket}/{key} -Downloads and decrypts an object from S3 using the specified client. - -**Headers:** -- `ClientID`: The client ID returned from CreateClient -- `Content-Metadata`: Encryption context metadata (optional) - -**Response:** Raw object data with `Content-Metadata` header - -## Building and Running - -### Prerequisites - -- Go 1.21 or later -- AWS credentials configured (via AWS CLI, environment variables, or IAM roles) - -### Build - -```bash -# Install dependencies -make deps - -# Build the server -make build - -# Or build and run -make run -``` - -### Development - -```bash -# Format code -make fmt - -# Vet code -make vet - -# Run tests -make test - -# Clean build artifacts -make clean -``` - -## Configuration - -The server runs on port 8082 by default and uses the `us-west-2` AWS region. These can be modified in the source code if needed. - -## Error Handling - -The server implements Smithy-compliant error responses: - -- **GenericServerError**: For internal server errors -- **S3EncryptionClientError**: For S3 encryption client specific errors - -## Implementation Notes - -- **Client Caching**: Clients are stored in memory with UUID keys for thread-safe access -- **Metadata Handling**: Follows the same metadata string format as Java/Python servers -- **AWS Integration**: Uses AWS SDK v2 for modern Go AWS operations -- **Concurrency**: Safe for concurrent requests with proper mutex usage - -## Limitations - -- This is a basic implementation that uses standard S3 clients rather than full S3 encryption clients -- In a production implementation, you would integrate with the actual S3 encryption client library -- Memory-based client storage (not persistent across restarts) - -## Testing - -The server is designed to work with the existing test framework. It should be compatible with the Java tests that validate the Smithy API contract. - -## Future Enhancements - -- Integration with actual S3 encryption client library -- Persistent client storage -- Enhanced logging and metrics -- Configuration file support -- Health check endpoints diff --git a/test-server/go-v3-server/README.md b/test-server/go-v3-server/README.md new file mode 100644 index 00000000..2452dc93 --- /dev/null +++ b/test-server/go-v3-server/README.md @@ -0,0 +1,32 @@ +# S3EC Go V3 Test Server + +This is the Go implementation of the S3ECTestServer framework for S3EC Go V3. It provides a server implementation for testing Go S3 Encryption Client V3 functionality. + +## Overview + +The S3EC Go test server implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Architecture + +The server is built using: + +- **HTTP Framework**: Gorilla Mux for routing +- **AWS SDK**: AWS SDK for Go v2 for S3 and KMS operations +- **Concurrency**: Thread-safe client caching with sync.RWMutex +- **Error Handling**: Smithy-compliant error responses + +## Usage + +To run the server: + +```console +gradle run +``` + +This will start the server running on port `8082`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/go-server/go.mod b/test-server/go-v3-server/go.mod similarity index 100% rename from test-server/go-server/go.mod rename to test-server/go-v3-server/go.mod diff --git a/test-server/go-server/go.sum b/test-server/go-v3-server/go.sum similarity index 100% rename from test-server/go-server/go.sum rename to test-server/go-v3-server/go.sum diff --git a/test-server/go-server/main.go b/test-server/go-v3-server/main.go similarity index 96% rename from test-server/go-server/main.go rename to test-server/go-v3-server/main.go index 23025cbd..e79f0415 100644 --- a/test-server/go-server/main.go +++ b/test-server/go-v3-server/main.go @@ -130,6 +130,7 @@ func metadataStringToMap(mdString string) (map[string]string, error) { func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { log.Printf("CreateClient: Received POST /client request") + // Read body body, err := io.ReadAll(r.Body) if err != nil { s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) @@ -145,14 +146,13 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { log.Printf("CreateClient: Parsed config - KMSKeyID: %s, EnableLegacyWrappingAlgorithms: %t", input.Config.KeyMaterial.KMSKeyID, input.Config.EnableLegacyWrappingAlgorithms) - // Load AWS config cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) return } - // Create KMS keyring based on the provided KMS key ID if available + // Create KMS keyring log.Printf("CreateClient: Creating KMS keyring with key ID: %s", input.Config.KeyMaterial.KMSKeyID) kmsClient := kms.NewFromConfig(cfg) keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { @@ -306,6 +306,8 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { encCtx, err := metadataStringToMap(metadataHeader) // Create context with encryption context + // Note: S3EC Go V3 does not validate encryption context on decrypt, so the value provided here + // will not be validated against the encryption context stored on the object. ctx := context.Background() encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) if err != nil { @@ -331,7 +333,9 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { result, err := client.GetObject(encryptionContext, getInput) if err != nil { errMsg := err.Error() - // Shim the S3EC error message to the error message expected by the test server + // Shim the S3EC error message to the error message expected by the test server. + // We don't want to change the S3EC error message but the test server expects a specific error message; + // This is the appropriate place to rewrite the error message. if strings.Contains(errMsg, "to decrypt x-amz-cek-alg value `kms` you must enable legacyWrappingAlgorithms on the keyring") { s.createS3EncryptionClientError(w, "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms", http.StatusInternalServerError) return 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 325f43db..3207af35 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 @@ -75,10 +75,10 @@ public class RoundTripTests { serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); } - // These languages' S3EC implementations do not validate encryption context provided to getObject. - // If the encryption context provided to getObject does not match the encryption context provided to putObject, - // these languages' implementations will not raise an error as expected. - // For now, skip tests that require this validation behavior. + // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt). + // If the encryption context provided to getObject does not match the encryption context on the stored object, + // these implementations will not raise an error as expected. + // For now, skip tests that expect encryption context validation on decrypt. private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = Set.of("Go-V3"); diff --git a/test-server/java-server/README.md b/test-server/java-v3-server/README.md similarity index 70% rename from test-server/java-server/README.md rename to test-server/java-v3-server/README.md index b2f5bb1b..e00eb496 100644 --- a/test-server/java-server/README.md +++ b/test-server/java-v3-server/README.md @@ -1,6 +1,6 @@ -# S3EC Java Test Server +# S3EC Java V3 Test Server -This is the Java implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality. +This is the Java implementation of the S3ECTestServer framework for S3EC Java V3. It provides a server implementation for testing Java S3 Encryption Client V3 functionality. ## Overview diff --git a/test-server/java-server/build.gradle.kts b/test-server/java-v3-server/build.gradle.kts similarity index 100% rename from test-server/java-server/build.gradle.kts rename to test-server/java-v3-server/build.gradle.kts diff --git a/test-server/java-server/gradle.properties b/test-server/java-v3-server/gradle.properties similarity index 100% rename from test-server/java-server/gradle.properties rename to test-server/java-v3-server/gradle.properties diff --git a/test-server/java-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v3-server/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from test-server/java-server/gradle/wrapper/gradle-wrapper.jar rename to test-server/java-v3-server/gradle/wrapper/gradle-wrapper.jar diff --git a/test-server/java-server/gradle/wrapper/gradle-wrapper.properties b/test-server/java-v3-server/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from test-server/java-server/gradle/wrapper/gradle-wrapper.properties rename to test-server/java-v3-server/gradle/wrapper/gradle-wrapper.properties diff --git a/test-server/java-server/gradlew b/test-server/java-v3-server/gradlew similarity index 100% rename from test-server/java-server/gradlew rename to test-server/java-v3-server/gradlew diff --git a/test-server/java-server/gradlew.bat b/test-server/java-v3-server/gradlew.bat similarity index 100% rename from test-server/java-server/gradlew.bat rename to test-server/java-v3-server/gradlew.bat diff --git a/test-server/java-server/license.txt b/test-server/java-v3-server/license.txt similarity index 100% rename from test-server/java-server/license.txt rename to test-server/java-v3-server/license.txt diff --git a/test-server/java-server/settings.gradle.kts b/test-server/java-v3-server/settings.gradle.kts similarity index 100% rename from test-server/java-server/settings.gradle.kts rename to test-server/java-v3-server/settings.gradle.kts diff --git a/test-server/java-server/smithy-build.json b/test-server/java-v3-server/smithy-build.json similarity index 100% rename from test-server/java-server/smithy-build.json rename to test-server/java-v3-server/smithy-build.json diff --git a/test-server/java-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 similarity index 100% rename from test-server/java-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java rename to test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java diff --git a/test-server/java-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 similarity index 100% rename from test-server/java-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java rename to test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java diff --git a/test-server/java-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java similarity index 100% rename from test-server/java-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java rename to test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java diff --git a/test-server/java-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java similarity index 100% rename from test-server/java-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java rename to test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java diff --git a/test-server/java-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 similarity index 100% rename from test-server/java-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java rename to test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java diff --git a/test-server/python-server/.gitignore b/test-server/python-v3-server/.gitignore similarity index 100% rename from test-server/python-server/.gitignore rename to test-server/python-v3-server/.gitignore diff --git a/test-server/python-server/README.md b/test-server/python-v3-server/README.md similarity index 100% rename from test-server/python-server/README.md rename to test-server/python-v3-server/README.md diff --git a/test-server/python-server/poetry.lock b/test-server/python-v3-server/poetry.lock similarity index 100% rename from test-server/python-server/poetry.lock rename to test-server/python-v3-server/poetry.lock diff --git a/test-server/python-server/pyproject.toml b/test-server/python-v3-server/pyproject.toml similarity index 100% rename from test-server/python-server/pyproject.toml rename to test-server/python-v3-server/pyproject.toml diff --git a/test-server/python-server/src/__init__.py b/test-server/python-v3-server/src/__init__.py similarity index 100% rename from test-server/python-server/src/__init__.py rename to test-server/python-v3-server/src/__init__.py diff --git a/test-server/python-server/src/main.py b/test-server/python-v3-server/src/main.py similarity index 100% rename from test-server/python-server/src/main.py rename to test-server/python-v3-server/src/main.py diff --git a/test-server/python-server/tests/__init__.py b/test-server/python-v3-server/tests/__init__.py similarity index 100% rename from test-server/python-server/tests/__init__.py rename to test-server/python-v3-server/tests/__init__.py From 139b88ad9e26e3df4cf043eb56808680cedda629 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 16 Sep 2025 11:00:53 -0700 Subject: [PATCH 006/201] Add context for q --- test-server/net-v3-server/.temp/qSaved.json | 24793 ++++++++++++++++ .../Controllers/ClientController.cs | 51 + 2 files changed, 24844 insertions(+) create mode 100644 test-server/net-v3-server/.temp/qSaved.json create mode 100644 test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs diff --git a/test-server/net-v3-server/.temp/qSaved.json b/test-server/net-v3-server/.temp/qSaved.json new file mode 100644 index 00000000..b9a7f462 --- /dev/null +++ b/test-server/net-v3-server/.temp/qSaved.json @@ -0,0 +1,24793 @@ +{ + "conversation_id": "3014dce3-837b-4e62-b504-d959305aeffa", + "next_message": null, + "history": [ + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "read /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server. I want to to know what this test server does. src/main.py is the starting point of the program. I want to to know what this test server does. src/main.py is the starting point of the program." + } + }, + "timestamp": "2025-09-15T11:22:35.366168-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "64d3a3d3-3433-4ef4-ae28-229d095c1ad7", + "content": "I'll read the test server directory and examine the main.py file to understand what this test server does.", + "tool_uses": [ + { + "id": "tooluse_wn9kaWDNRhmo0GRVVjns-g", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "8258c309-c7e7-443c-936c-e72aa45f251c", + "message_id": "64d3a3d3-3433-4ef4-ae28-229d095c1ad7", + "request_start_timestamp_ms": 1757960555371, + "stream_end_timestamp_ms": 1757960565021, + "time_to_first_chunk": { + "secs": 8, + "nanos": 612063541 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 31875 + }, + { + "secs": 0, + "nanos": 21042 + }, + { + "secs": 0, + "nanos": 789750 + }, + { + "secs": 0, + "nanos": 2013750 + }, + { + "secs": 0, + "nanos": 2938125 + }, + { + "secs": 0, + "nanos": 2310500 + }, + { + "secs": 0, + "nanos": 2651458 + }, + { + "secs": 0, + "nanos": 2903583 + }, + { + "secs": 0, + "nanos": 15166 + }, + { + "secs": 0, + "nanos": 2752542 + }, + { + "secs": 0, + "nanos": 138983084 + }, + { + "secs": 0, + "nanos": 814864125 + }, + { + "secs": 0, + "nanos": 16042 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 0, + "nanos": 8959 + }, + { + "secs": 0, + "nanos": 8500 + }, + { + "secs": 0, + "nanos": 2501708 + }, + { + "secs": 0, + "nanos": 11750 + }, + { + "secs": 0, + "nanos": 5166 + }, + { + "secs": 0, + "nanos": 16792 + }, + { + "secs": 0, + "nanos": 1965709 + }, + { + "secs": 0, + "nanos": 3598959 + }, + { + "secs": 0, + "nanos": 15625 + }, + { + "secs": 0, + "nanos": 5917 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 1904667 + }, + { + "secs": 0, + "nanos": 7750 + }, + { + "secs": 0, + "nanos": 2292 + }, + { + "secs": 0, + "nanos": 251125 + }, + { + "secs": 0, + "nanos": 2077292 + }, + { + "secs": 0, + "nanos": 5041 + }, + { + "secs": 0, + "nanos": 54208292 + }, + { + "secs": 0, + "nanos": 163583 + }, + { + "secs": 0, + "nanos": 22875 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 461, + "response_size": 259, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_wn9kaWDNRhmo0GRVVjns-g", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_wn9kaWDNRhmo0GRVVjns-g", + "content": [ + { + "Text": "-rw-r--r-- 1 504 20 473 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/pyproject.toml\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/tests\n-rw-r--r-- 1 504 20 762 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md\n-rw-r--r-- 1 504 20 302 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/.gitignore\ndrwxr-xr-x 6 504 20 192 Sep 15 17:48 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/.venv\n-rw-r--r-- 1 504 20 63517 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/poetry.lock\ndrwxr-xr-x 5 504 20 160 Sep 15 17:49 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "4469cafc-b106-4691-b79b-6b1ac9a72bc3", + "content": "", + "tool_uses": [ + { + "id": "tooluse_OwieXzrlRPybnv0uEJiBLQ", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "d45b8b2f-092b-486f-bb3c-2003d300b227", + "message_id": "4469cafc-b106-4691-b79b-6b1ac9a72bc3", + "request_start_timestamp_ms": 1757960565440, + "stream_end_timestamp_ms": 1757960569373, + "time_to_first_chunk": { + "secs": 3, + "nanos": 918900875 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 21625 + }, + { + "secs": 0, + "nanos": 10375 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 6083 + }, + { + "secs": 0, + "nanos": 1417 + }, + { + "secs": 0, + "nanos": 2875 + }, + { + "secs": 0, + "nanos": 2333 + }, + { + "secs": 0, + "nanos": 2167042 + }, + { + "secs": 0, + "nanos": 7250 + }, + { + "secs": 0, + "nanos": 3458 + }, + { + "secs": 0, + "nanos": 844208 + }, + { + "secs": 0, + "nanos": 2259125 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 2917 + }, + { + "secs": 0, + "nanos": 2601833 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1067375 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 448750 + }, + { + "secs": 0, + "nanos": 1139916 + }, + { + "secs": 0, + "nanos": 17458 + }, + { + "secs": 0, + "nanos": 3188542 + }, + { + "secs": 0, + "nanos": 17125 + }, + { + "secs": 0, + "nanos": 41 + } + ], + "user_prompt_length": 0, + "response_size": 157, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_OwieXzrlRPybnv0uEJiBLQ", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_OwieXzrlRPybnv0uEJiBLQ", + "content": [ + { + "Text": "-rw-r--r-- 1 504 20 46 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/__init__.py\n-rwxr-xr-x 1 504 20 7206 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py\ndrwxr-xr-x 7 504 20 224 Sep 15 17:49 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/python_server.egg-info" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "8f3bb12c-4325-4801-8715-b84057879efc", + "content": "", + "tool_uses": [ + { + "id": "tooluse_zAwEy_xQTPSKfc4SstAz7Q", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "1f29e3af-a438-4194-b265-248b75feca18", + "message_id": "8f3bb12c-4325-4801-8715-b84057879efc", + "request_start_timestamp_ms": 1757960570048, + "stream_end_timestamp_ms": 1757960574542, + "time_to_first_chunk": { + "secs": 3, + "nanos": 690460292 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 33125 + }, + { + "secs": 0, + "nanos": 10792 + }, + { + "secs": 0, + "nanos": 670367333 + }, + { + "secs": 0, + "nanos": 55600125 + }, + { + "secs": 0, + "nanos": 40265375 + }, + { + "secs": 0, + "nanos": 19194292 + }, + { + "secs": 0, + "nanos": 9083 + }, + { + "secs": 0, + "nanos": 2260458 + }, + { + "secs": 0, + "nanos": 11375 + }, + { + "secs": 0, + "nanos": 681333 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 1578584 + }, + { + "secs": 0, + "nanos": 5208 + }, + { + "secs": 0, + "nanos": 1286875 + }, + { + "secs": 0, + "nanos": 3958 + }, + { + "secs": 0, + "nanos": 451333 + }, + { + "secs": 0, + "nanos": 293125 + }, + { + "secs": 0, + "nanos": 2185458 + }, + { + "secs": 0, + "nanos": 4459 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 163667 + }, + { + "secs": 0, + "nanos": 1635459 + }, + { + "secs": 0, + "nanos": 4833 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 1836083 + }, + { + "secs": 0, + "nanos": 4042 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 346167 + }, + { + "secs": 0, + "nanos": 868458 + }, + { + "secs": 0, + "nanos": 5209 + }, + { + "secs": 0, + "nanos": 617375 + }, + { + "secs": 0, + "nanos": 3051375 + }, + { + "secs": 0, + "nanos": 15125 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 160, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_zAwEy_xQTPSKfc4SstAz7Q", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_zAwEy_xQTPSKfc4SstAz7Q", + "content": [ + { + "Text": "\"\"\"\nMain entry point for the Python server.\n\"\"\"\n\nfrom fastapi import FastAPI, Request, HTTPException, Response, status\nfrom fastapi.responses import JSONResponse\nfrom s3_encryption import S3EncryptionClient, S3EncryptionClientConfig\nfrom s3_encryption.exceptions import S3EncryptionClientError\nfrom s3_encryption.materials.kms_keyring import KmsKeyring\nimport boto3\nimport uvicorn\nimport json\nimport uuid\n\napp = FastAPI(title=\"Python Server\")\n\n# Dictionary to store clients with their UUIDs as keys\nclient_cache = {}\n\n\n# Java gets a list, but since there's no Smithy Python Server,\n# this is just a string.\ndef metadata_string_to_map(md_string):\n md = {}\n if md_string == \"\":\n return md\n md_list = md_string.split(\",\")\n for entry in md_list:\n # Split on \"]:[\" to separate key and value\n parts = entry.split(\"]:[\")\n if len(parts) == 2:\n # Remove remaining brackets from start and end\n key = parts[0][1:] # Remove first character\n value = parts[1][:-1] # Remove last character\n md[key] = value\n else:\n raise ValueError(f\"Malformed metadata list entry: {entry}\")\n return md\n\n\ndef create_generic_server_error(\n message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n):\n \"\"\"\n Create a response that matches the GenericServerError type from the Smithy model.\n Used for internal server errors.\n \"\"\"\n return JSONResponse(\n status_code=status_code,\n content={\"__type\": \"software.amazon.encryption.s3#GenericServerError\", \"message\": message},\n )\n\n\ndef create_s3_encryption_client_error(\n message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n):\n \"\"\"\n Create a response that matches the S3EncryptionClientError type from the Smithy model.\n Used for errors thrown by the S3 Encryption Client.\n \"\"\"\n return JSONResponse(\n status_code=status_code,\n content={\n \"__type\": \"software.amazon.encryption.s3#S3EncryptionClientError\",\n \"message\": message,\n },\n )\n\n\n@app.put(\"/object/{bucket}/{key}\")\nasync def put_object(bucket: str, key: str, request: Request):\n \"\"\"\n Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient\n to make a PutObject request to S3.\n \"\"\"\n client_id = request.headers.get(\"ClientID\")\n body = await request.body()\n\n if not client_id:\n return create_generic_server_error(\n \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n try:\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n # Make the PutObject request\n response = client.put_object(\n **{\"Bucket\": bucket, \"Key\": key, \"Body\": body, \"EncryptionContext\": enc_ctx}\n )\n\n # Return the appropriate response\n return {\n \"bucket\": bucket,\n \"key\": key,\n \"metadata\": metadata if isinstance(metadata, list) else [],\n }\n except Exception as e:\n return create_s3_encryption_client_error(f\"Failed to put object: {str(e)}\")\n\n\n@app.get(\"/object/{bucket}/{key}\")\nasync def get_object(bucket: str, key: str, request: Request):\n \"\"\"\n Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient\n to make a GetObject request to S3.\n \"\"\"\n client_id = request.headers.get(\"ClientID\")\n\n if not client_id:\n return create_generic_server_error(\n \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n try:\n # Use the client to make a GetObject request to S3\n response = client.get_object(**{\"Bucket\": bucket, \"Key\": key, \"EncryptionContext\": enc_ctx})\n\n # Extract the body and metadata from the response\n body = response.get(\"Body\").read() if response.get(\"Body\") else b\"\"\n metadata = response.get(\"Metadata\", [])\n\n # Convert metadata dictionary to a list of key-value pairs if it's a dict\n if isinstance(metadata, dict):\n metadata_list = [f\"{key}={value}\" for key, value in metadata.items()]\n else:\n metadata_list = metadata if isinstance(metadata, list) else []\n\n # Set the Content-Metadata header in the response\n # Convert metadata_list to a comma-separated string\n metadata_str = \",\".join(metadata_list) if metadata_list else \"\"\n headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n \"\"\"\n Handle POST requests to /client by creating an S3EncryptionClient.\n \"\"\"\n body = await request.body()\n\n # Parse the bytes object as JSON\n try:\n # Decode bytes to string and parse as JSON\n parsed_data = json.loads(body.decode(\"utf-8\"))\n\n # Extract config from the parsed data\n config_data = parsed_data.get(\"config\", {})\n # Extract key material if provided\n key_material = config_data.get(\"keyMaterial\", {})\n\n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n\n # TODO pull region from ARN\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n\n # Create S3EncryptionClient\n client = S3EncryptionClient(wrapped_client, client_config)\n\n # Generate a client ID using UUID\n client_id = str(uuid.uuid4())\n\n # Add the client to the client_cache dictionary\n client_cache[client_id] = client\n\n return {\"clientId\": client_id}\n except json.JSONDecodeError as e:\n return create_generic_server_error(\n \"Invalid JSON in request body\", status.HTTP_400_BAD_REQUEST\n )\n except Exception as e:\n return create_s3_encryption_client_error(f\"Failed to create client: {str(e)}\")\n\n\ndef main():\n \"\"\"\n Main function to start the server.\n \"\"\"\n uvicorn.run(app, host=\"localhost\", port=8081)\n\n\nif __name__ == \"__main__\":\n main()" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "eecc6b5e-0499-4780-85c0-d03b88b688af", + "content": "", + "tool_uses": [ + { + "id": "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "8c33c92c-1fc1-47d2-9c9a-a0c3c78778ce", + "message_id": "eecc6b5e-0499-4780-85c0-d03b88b688af", + "request_start_timestamp_ms": 1757960575161, + "stream_end_timestamp_ms": 1757960579976, + "time_to_first_chunk": { + "secs": 4, + "nanos": 208416250 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 27083 + }, + { + "secs": 0, + "nanos": 18156917 + }, + { + "secs": 0, + "nanos": 564608875 + }, + { + "secs": 0, + "nanos": 11875 + }, + { + "secs": 0, + "nanos": 4334 + }, + { + "secs": 0, + "nanos": 650834 + }, + { + "secs": 0, + "nanos": 1246666 + }, + { + "secs": 0, + "nanos": 6917 + }, + { + "secs": 0, + "nanos": 1087250 + }, + { + "secs": 0, + "nanos": 477042 + }, + { + "secs": 0, + "nanos": 2046375 + }, + { + "secs": 0, + "nanos": 17334 + }, + { + "secs": 0, + "nanos": 2389709 + }, + { + "secs": 0, + "nanos": 124291 + }, + { + "secs": 0, + "nanos": 2041834 + }, + { + "secs": 0, + "nanos": 16084 + }, + { + "secs": 0, + "nanos": 1479250 + }, + { + "secs": 0, + "nanos": 16417 + }, + { + "secs": 0, + "nanos": 9639041 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 2583 + }, + { + "secs": 0, + "nanos": 2194750 + }, + { + "secs": 0, + "nanos": 8084 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2667 + }, + { + "secs": 0, + "nanos": 2583 + }, + { + "secs": 0, + "nanos": 24375 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 158, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", + "content": [ + { + "Text": "# Python Server\n\nA FastAPI-based Python server implementation.\n\n## Setup\n\n1. Install uv (if not already installed):\n```bash\npip install uv\n```\n\n2. Create a virtual environment and install dependencies:\n```bash\nuv venv\nsource .venv/bin/activate\nuv pip install -e .\nuv pip install -e ../..\n```\n\n## Development\n\n- Source code is in the `src` directory\n- Tests are in the `tests` directory\n- Use `source .venv/bin/activate` to activate the virtual environment\n- Use `uv pip install {package}` to add new dependencies\n- Use `uv pip install {package} --dev` to add new development dependencies\n\n## Running the Server\n\n```bash\n.venv/bin/python src/main.py\n```\n\nThe server will start on `http://localhost:8081`.\n\n## Running Tests\n\n```bash\n.venv/bin/python -m pytest\n```" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "62ef600e-1f68-494d-9bb0-00a7ce6521bd", + "content": "This test server is a **FastAPI-based HTTP server that provides a REST API wrapper around the S3 Encryption Client**. Here's what it does:\n\n## Purpose\nThe server acts as a test harness for the Amazon S3 Encryption Client Python library, allowing HTTP-based testing of client-side encryption functionality.\n\n## Key Functionality\n\n**Client Management:**\n- `POST /client` - Creates S3EncryptionClient instances with KMS keyring configuration\n- Stores clients in memory cache with UUID identifiers\n- Supports legacy wrapping algorithms configuration\n\n**S3 Operations:**\n- `PUT /object/{bucket}/{key}` - Encrypts and uploads objects to S3\n- `GET /object/{bucket}/{key}` - Downloads and decrypts objects from S3\n- Handles encryption context via `Content-Metadata` headers\n\n**Architecture:**\n- Uses FastAPI framework running on localhost:8081\n- Integrates with AWS KMS for key management (hardcoded to us-west-2 region)\n- Converts metadata between string format and dictionary for encryption context\n- Provides structured error responses matching Smithy model types\n\nThe server essentially allows you to test S3 client-side encryption operations through HTTP requests rather than direct Python API calls, making it useful for integration testing or cross-language compatibility testing." + } + }, + "request_metadata": { + "request_id": "b547db21-e620-4a26-9e5d-9111c2afd95f", + "message_id": "62ef600e-1f68-494d-9bb0-00a7ce6521bd", + "request_start_timestamp_ms": 1757960580622, + "stream_end_timestamp_ms": 1757960588576, + "time_to_first_chunk": { + "secs": 1, + "nanos": 898907916 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 31500 + }, + { + "secs": 0, + "nanos": 8458 + }, + { + "secs": 0, + "nanos": 267761083 + }, + { + "secs": 0, + "nanos": 222552417 + }, + { + "secs": 0, + "nanos": 112580750 + }, + { + "secs": 0, + "nanos": 380233875 + }, + { + "secs": 0, + "nanos": 130621583 + }, + { + "secs": 0, + "nanos": 286579125 + }, + { + "secs": 0, + "nanos": 167468167 + }, + { + "secs": 0, + "nanos": 337833083 + }, + { + "secs": 0, + "nanos": 125466291 + }, + { + "secs": 0, + "nanos": 322277375 + }, + { + "secs": 0, + "nanos": 484840708 + }, + { + "secs": 0, + "nanos": 46890459 + }, + { + "secs": 0, + "nanos": 114786125 + }, + { + "secs": 0, + "nanos": 102866542 + }, + { + "secs": 0, + "nanos": 163200541 + }, + { + "secs": 0, + "nanos": 91320500 + }, + { + "secs": 0, + "nanos": 112123500 + }, + { + "secs": 0, + "nanos": 337346375 + }, + { + "secs": 0, + "nanos": 155788083 + }, + { + "secs": 0, + "nanos": 178513791 + }, + { + "secs": 0, + "nanos": 117490625 + }, + { + "secs": 0, + "nanos": 190261417 + }, + { + "secs": 0, + "nanos": 161779000 + }, + { + "secs": 0, + "nanos": 234609125 + }, + { + "secs": 0, + "nanos": 161274125 + }, + { + "secs": 0, + "nanos": 282359750 + }, + { + "secs": 0, + "nanos": 450667000 + }, + { + "secs": 0, + "nanos": 243907292 + }, + { + "secs": 0, + "nanos": 53718917 + }, + { + "secs": 0, + "nanos": 17418375 + }, + { + "secs": 0, + "nanos": 42 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 1279, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Now, I want to start writing similar implementation but in .Net. I have already created net-v3-server where I plan to test S3-EC net v3 like python. Don't write any code but I want to prepare first. where do you think I should start? I don't know dotnet much, how should project structure look like?" + } + }, + "timestamp": "2025-09-15T11:27:34.848706-07:00", + "images": null + }, + "assistant": { + "Response": { + "message_id": "8fd1cb75-b685-4896-8ddc-5a8b849c40dd", + "content": "Based on the Python test server structure, here's how you should approach the .NET implementation:\n\n## Project Structure for .NET\n\n```\nnet-v3-server/\n├── src/\n│ ├── NetV3Server/\n│ │ ├── Controllers/\n│ │ │ ├── ClientController.cs\n│ │ │ └── ObjectController.cs\n│ │ ├── Models/\n│ │ │ ├── ClientRequest.cs\n│ │ │ ├── ClientResponse.cs\n│ │ │ └── ErrorModels.cs\n│ │ ├── Services/\n│ │ │ └── ClientCacheService.cs\n│ │ ├── Program.cs\n│ │ └── NetV3Server.csproj\n├── tests/\n│ └── NetV3Server.Tests/\n│ └── NetV3Server.Tests.csproj\n├── NetV3Server.sln\n└── README.md\n```\n\n## Where to Start\n\n**1. Choose Framework:**\n- Use **ASP.NET Core Web API** (equivalent to FastAPI)\n- Target .NET 6+ for modern features\n\n**2. Key Dependencies You'll Need:**\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK\n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n- `Microsoft.AspNetCore.Mvc` - Web API framework\n\n**3. Core Components to Implement:**\n- **Program.cs** - Entry point (like main.py)\n- **Controllers** - Handle HTTP endpoints (`/client`, `/object/{bucket}/{key}`)\n- **ClientCacheService** - In-memory client storage (like client_cache dict)\n- **Models** - Request/response DTOs\n- **Error handling** - Custom exception responses\n\n**4. Learning Path:**\n1. Start with basic ASP.NET Core Web API tutorial\n2. Learn dependency injection (built into .NET)\n3. Understand controllers and routing\n4. Study AWS SDK for .NET usage patterns\n\n**5. Initial Setup Steps:**\n1. Install .NET SDK\n2. Create solution: `dotnet new sln -n NetV3Server`\n3. Create web API project: `dotnet new webapi -n NetV3Server`\n4. Add AWS NuGet packages\n5. Set up basic controller structure\n\nThe .NET version will be more structured than Python due to strong typing and dependency injection, but the core logic flow will mirror your Python implementation exactly." + } + }, + "request_metadata": { + "request_id": "a4195bc7-d719-46ca-ba46-618bac161476", + "message_id": "8fd1cb75-b685-4896-8ddc-5a8b849c40dd", + "request_start_timestamp_ms": 1757960854851, + "stream_end_timestamp_ms": 1757960870815, + "time_to_first_chunk": { + "secs": 4, + "nanos": 37539625 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 61625 + }, + { + "secs": 0, + "nanos": 14875 + }, + { + "secs": 0, + "nanos": 425821416 + }, + { + "secs": 0, + "nanos": 400610958 + }, + { + "secs": 0, + "nanos": 67599084 + }, + { + "secs": 0, + "nanos": 187239333 + }, + { + "secs": 0, + "nanos": 288225625 + }, + { + "secs": 0, + "nanos": 106530750 + }, + { + "secs": 0, + "nanos": 171668125 + }, + { + "secs": 0, + "nanos": 118489750 + }, + { + "secs": 0, + "nanos": 479248708 + }, + { + "secs": 0, + "nanos": 173749459 + }, + { + "secs": 0, + "nanos": 63474167 + }, + { + "secs": 0, + "nanos": 228928167 + }, + { + "secs": 0, + "nanos": 242453083 + }, + { + "secs": 0, + "nanos": 333747083 + }, + { + "secs": 0, + "nanos": 8208 + }, + { + "secs": 0, + "nanos": 174769042 + }, + { + "secs": 0, + "nanos": 296416750 + }, + { + "secs": 0, + "nanos": 225901042 + }, + { + "secs": 0, + "nanos": 214547375 + }, + { + "secs": 0, + "nanos": 720664583 + }, + { + "secs": 0, + "nanos": 6917 + }, + { + "secs": 0, + "nanos": 343617917 + }, + { + "secs": 0, + "nanos": 294897542 + }, + { + "secs": 0, + "nanos": 908833 + }, + { + "secs": 0, + "nanos": 92441209 + }, + { + "secs": 0, + "nanos": 231569625 + }, + { + "secs": 0, + "nanos": 323144750 + }, + { + "secs": 0, + "nanos": 548053875 + }, + { + "secs": 0, + "nanos": 396507917 + }, + { + "secs": 0, + "nanos": 218866875 + }, + { + "secs": 0, + "nanos": 7125 + }, + { + "secs": 0, + "nanos": 231234917 + }, + { + "secs": 0, + "nanos": 504475834 + }, + { + "secs": 0, + "nanos": 456930625 + }, + { + "secs": 0, + "nanos": 286679625 + }, + { + "secs": 0, + "nanos": 537425792 + }, + { + "secs": 0, + "nanos": 348420584 + }, + { + "secs": 0, + "nanos": 541625292 + }, + { + "secs": 0, + "nanos": 14708 + }, + { + "secs": 0, + "nanos": 4929875 + }, + { + "secs": 0, + "nanos": 259916958 + }, + { + "secs": 1, + "nanos": 383415250 + }, + { + "secs": 0, + "nanos": 13708 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 22958 + }, + { + "secs": 0, + "nanos": 125 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 457, + "response_size": 2080, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "How do I create such directory structure?" + } + }, + "timestamp": "2025-09-15T11:51:52.140173-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "db3b1a3c-2ee7-4562-9756-e1f0480a532b", + "content": "I'll help you create the .NET project structure. Let me check your current directory first and then create the structure.", + "tool_uses": [ + { + "id": "tooluse_A770w0NKREKNmAarZ5tgSg", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "pwd" + }, + "orig_args": { + "command": "pwd" + } + } + ] + } + }, + "request_metadata": { + "request_id": "f318f46b-1e28-465b-a97b-9a5088262428", + "message_id": "db3b1a3c-2ee7-4562-9756-e1f0480a532b", + "request_start_timestamp_ms": 1757962312144, + "stream_end_timestamp_ms": 1757962317438, + "time_to_first_chunk": { + "secs": 3, + "nanos": 943614042 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 18250 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 457979625 + }, + { + "secs": 0, + "nanos": 270583875 + }, + { + "secs": 0, + "nanos": 108414166 + }, + { + "secs": 0, + "nanos": 388537542 + }, + { + "secs": 0, + "nanos": 118164000 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 6494875 + }, + { + "secs": 0, + "nanos": 11208 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 199, + "response_size": 139, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_A770w0NKREKNmAarZ5tgSg", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_A770w0NKREKNmAarZ5tgSg", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "849cfa6f-127e-4739-8fea-80c195e29c7c", + "content": "", + "tool_uses": [ + { + "id": "tooluse_EAh8v8zhQrGEC60SXV4PLg", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "876e3ba6-676a-4425-ac68-61ea26de69c0", + "message_id": "849cfa6f-127e-4739-8fea-80c195e29c7c", + "request_start_timestamp_ms": 1757962317981, + "stream_end_timestamp_ms": 1757962321238, + "time_to_first_chunk": { + "secs": 3, + "nanos": 191994292 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 12417 + }, + { + "secs": 0, + "nanos": 10792 + }, + { + "secs": 0, + "nanos": 13377458 + }, + { + "secs": 0, + "nanos": 5312959 + }, + { + "secs": 0, + "nanos": 2269458 + }, + { + "secs": 0, + "nanos": 6975500 + }, + { + "secs": 0, + "nanos": 2127125 + }, + { + "secs": 0, + "nanos": 2151958 + }, + { + "secs": 0, + "nanos": 6583 + }, + { + "secs": 0, + "nanos": 2309292 + }, + { + "secs": 0, + "nanos": 4186208 + }, + { + "secs": 0, + "nanos": 934584 + }, + { + "secs": 0, + "nanos": 6041 + }, + { + "secs": 0, + "nanos": 25295125 + }, + { + "secs": 0, + "nanos": 13792 + }, + { + "secs": 0, + "nanos": 4541 + }, + { + "secs": 0, + "nanos": 4125 + }, + { + "secs": 0, + "nanos": 31958 + }, + { + "secs": 0, + "nanos": 13458 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 127, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_EAh8v8zhQrGEC60SXV4PLg", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_EAh8v8zhQrGEC60SXV4PLg", + "content": [ + { + "Text": "drwxr-xr-x 12 504 20 384 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/cdk\n-rw-r--r-- 1 504 20 309 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/CODE_OF_CONDUCT.md\n-rw-r--r-- 1 504 20 10142 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/LICENSE\n-rw-r--r-- 1 504 20 172 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/requirements.txt\ndrwxr-xr-x 9 504 20 288 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test\n-rw-r--r-- 1 504 20 875 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/Makefile\n-rw-r--r-- 1 504 20 1205 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/pyproject.toml\n-rw-r--r-- 1 504 20 1509 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/SUPPORT_POLICY.rst\n-rw-r--r-- 1 504 20 67 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/NOTICE\n-rw-r--r-- 1 504 20 1605 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/README.md\ndrwxr-xr-x 10 504 20 320 Sep 15 18:15 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server\n-rw-r--r-- 1 504 20 529 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.gitignore\n-rw-r--r-- 1 504 20 3160 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/CONTRIBUTING.md\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.github\ndrwxr-xr-x 14 504 20 448 Sep 15 18:16 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.git\ndrwxr-xr-x 6 504 20 192 Sep 15 18:13 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.idea\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/src" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "86bbd309-5bbc-4731-b904-c557276ddd97", + "content": "", + "tool_uses": [ + { + "id": "tooluse_MKWwYFMvTsm1D4xETOrxBw", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "6a0f8768-92c8-4ce2-9aad-712257ec58c1", + "message_id": "86bbd309-5bbc-4731-b904-c557276ddd97", + "request_start_timestamp_ms": 1757962321748, + "stream_end_timestamp_ms": 1757962325195, + "time_to_first_chunk": { + "secs": 2, + "nanos": 461779250 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 12250 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 743658500 + }, + { + "secs": 0, + "nanos": 46745625 + }, + { + "secs": 0, + "nanos": 14185833 + }, + { + "secs": 0, + "nanos": 38030125 + }, + { + "secs": 0, + "nanos": 47036208 + }, + { + "secs": 0, + "nanos": 20137041 + }, + { + "secs": 0, + "nanos": 42643208 + }, + { + "secs": 0, + "nanos": 25078666 + }, + { + "secs": 0, + "nanos": 18416 + }, + { + "secs": 0, + "nanos": 99292 + }, + { + "secs": 0, + "nanos": 464959 + }, + { + "secs": 0, + "nanos": 1442875 + }, + { + "secs": 0, + "nanos": 4709 + }, + { + "secs": 0, + "nanos": 420709 + }, + { + "secs": 0, + "nanos": 1605791 + }, + { + "secs": 0, + "nanos": 4416 + }, + { + "secs": 0, + "nanos": 3888042 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 139, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_MKWwYFMvTsm1D4xETOrxBw", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_MKWwYFMvTsm1D4xETOrxBw", + "content": [ + { + "Text": "drwxr-xr-x 14 504 20 448 Sep 15 17:42 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/java-server\n-rw-r--r-- 1 504 20 4085 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/Makefile\n-rw-r--r-- 1 504 20 1624 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/README.md\ndrwxr-xr-x 4 504 20 128 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server\ndrwxr-xr-x 5 504 20 160 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/model\ndrwxr-xr-x 15 504 20 480 Sep 15 17:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/java-tests\n-rw-r--r-- 1 504 20 1625 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/gradle.init\ndrwxr-xr-x 9 504 20 288 Sep 15 17:48 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "0a14913f-2ab4-4f68-b20c-6b695134792e", + "content": "I see you already have a `net-v3-server` directory. Let me check what's in it and then create the proper .NET structure:", + "tool_uses": [ + { + "id": "tooluse_KZne_4WjSeeHSL7wjzl80A", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "d849b78f-743d-49c9-9a8b-e8060441d8e6", + "message_id": "0a14913f-2ab4-4f68-b20c-6b695134792e", + "request_start_timestamp_ms": 1757962325820, + "stream_end_timestamp_ms": 1757962329456, + "time_to_first_chunk": { + "secs": 3, + "nanos": 129136875 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 18875 + }, + { + "secs": 0, + "nanos": 9625 + }, + { + "secs": 0, + "nanos": 25445709 + }, + { + "secs": 0, + "nanos": 24529375 + }, + { + "secs": 0, + "nanos": 31320833 + }, + { + "secs": 0, + "nanos": 125152417 + }, + { + "secs": 0, + "nanos": 139956417 + }, + { + "secs": 0, + "nanos": 7139958 + }, + { + "secs": 0, + "nanos": 6217750 + }, + { + "secs": 0, + "nanos": 9087167 + }, + { + "secs": 0, + "nanos": 5574042 + }, + { + "secs": 0, + "nanos": 4945542 + }, + { + "secs": 0, + "nanos": 8432500 + }, + { + "secs": 0, + "nanos": 6706208 + }, + { + "secs": 0, + "nanos": 167542 + }, + { + "secs": 0, + "nanos": 6084584 + }, + { + "secs": 0, + "nanos": 4448458 + }, + { + "secs": 0, + "nanos": 24230375 + }, + { + "secs": 0, + "nanos": 3260834 + }, + { + "secs": 0, + "nanos": 15226250 + }, + { + "secs": 0, + "nanos": 1786167 + }, + { + "secs": 0, + "nanos": 20510084 + }, + { + "secs": 0, + "nanos": 6605416 + }, + { + "secs": 0, + "nanos": 5046167 + }, + { + "secs": 0, + "nanos": 2842833 + }, + { + "secs": 0, + "nanos": 21944500 + }, + { + "secs": 0, + "nanos": 11791 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 273, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_KZne_4WjSeeHSL7wjzl80A", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_KZne_4WjSeeHSL7wjzl80A", + "content": [ + { + "Text": "drwxr-xr-x 3 504 20 96 Sep 15 18:45 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp\n-rw-r--r-- 1 504 20 5281 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/README.md" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "8022e5ff-cabd-4f99-85e4-79375b26208e", + "content": "Now I'll create the proper .NET project structure in your existing `net-v3-server` directory:", + "tool_uses": [ + { + "id": "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" + } + } + ] + } + }, + "request_metadata": { + "request_id": "0191a0ff-7bea-4a4e-ab88-b6ae8603d6b8", + "message_id": "8022e5ff-cabd-4f99-85e4-79375b26208e", + "request_start_timestamp_ms": 1757962329981, + "stream_end_timestamp_ms": 1757962333285, + "time_to_first_chunk": { + "secs": 2, + "nanos": 470799541 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 14500 + }, + { + "secs": 0, + "nanos": 7500 + }, + { + "secs": 0, + "nanos": 11875 + }, + { + "secs": 0, + "nanos": 113253000 + }, + { + "secs": 0, + "nanos": 336681291 + }, + { + "secs": 0, + "nanos": 354360250 + }, + { + "secs": 0, + "nanos": 264750 + }, + { + "secs": 0, + "nanos": 1762792 + }, + { + "secs": 0, + "nanos": 2922166 + }, + { + "secs": 0, + "nanos": 7500 + }, + { + "secs": 0, + "nanos": 2239833 + }, + { + "secs": 0, + "nanos": 804958 + }, + { + "secs": 0, + "nanos": 1851250 + }, + { + "secs": 0, + "nanos": 1119125 + }, + { + "secs": 0, + "nanos": 886042 + }, + { + "secs": 0, + "nanos": 500541 + }, + { + "secs": 0, + "nanos": 318125 + }, + { + "secs": 0, + "nanos": 402833 + }, + { + "secs": 0, + "nanos": 3033875 + }, + { + "secs": 0, + "nanos": 7542 + }, + { + "secs": 0, + "nanos": 720958 + }, + { + "secs": 0, + "nanos": 1408416 + }, + { + "secs": 0, + "nanos": 291333 + }, + { + "secs": 0, + "nanos": 1546417 + }, + { + "secs": 0, + "nanos": 7198250 + }, + { + "secs": 0, + "nanos": 1327167 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 216, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "121dc459-0d38-48b7-9495-1f3514c24722", + "content": "", + "tool_uses": [ + { + "id": "tooluse_JPl0vN_HSaeMQDGkzksZTw", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet new sln -n NetV3Server" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet new sln -n NetV3Server" + } + } + ] + } + }, + "request_metadata": { + "request_id": "02f07f16-1f02-4a6d-a2d1-53bc8860db5a", + "message_id": "121dc459-0d38-48b7-9495-1f3514c24722", + "request_start_timestamp_ms": 1757962425667, + "stream_end_timestamp_ms": 1757962429478, + "time_to_first_chunk": { + "secs": 3, + "nanos": 741227875 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 41250 + }, + { + "secs": 0, + "nanos": 10916 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 3542 + }, + { + "secs": 0, + "nanos": 4209 + }, + { + "secs": 0, + "nanos": 18000 + }, + { + "secs": 0, + "nanos": 17000 + }, + { + "secs": 0, + "nanos": 5084 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 26250 + }, + { + "secs": 0, + "nanos": 8792 + }, + { + "secs": 0, + "nanos": 4334 + }, + { + "secs": 0, + "nanos": 572084 + }, + { + "secs": 0, + "nanos": 68028708 + }, + { + "secs": 0, + "nanos": 9375 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 16666 + }, + { + "secs": 0, + "nanos": 2584 + }, + { + "secs": 0, + "nanos": 9958 + }, + { + "secs": 0, + "nanos": 32667 + }, + { + "secs": 0, + "nanos": 22667 + }, + { + "secs": 0, + "nanos": 14125 + }, + { + "secs": 0, + "nanos": 5875 + }, + { + "secs": 0, + "nanos": 16958 + }, + { + "secs": 0, + "nanos": 13083 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 156, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_JPl0vN_HSaeMQDGkzksZTw", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_JPl0vN_HSaeMQDGkzksZTw", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "The template \"Solution File\" was created successfully.\n", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "bc045b27-ecbc-4b79-9683-99be21f72639", + "content": "", + "tool_uses": [ + { + "id": "tooluse_HkQDlinhQbCf3twlDtiPSA", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server" + } + } + ] + } + }, + "request_metadata": { + "request_id": "24f6f914-4b3c-4457-952a-d6c5dd280613", + "message_id": "bc045b27-ecbc-4b79-9683-99be21f72639", + "request_start_timestamp_ms": 1757962530874, + "stream_end_timestamp_ms": 1757962533948, + "time_to_first_chunk": { + "secs": 2, + "nanos": 485698458 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 19416 + }, + { + "secs": 0, + "nanos": 56898583 + }, + { + "secs": 0, + "nanos": 466530459 + }, + { + "secs": 0, + "nanos": 20167 + }, + { + "secs": 0, + "nanos": 963875 + }, + { + "secs": 0, + "nanos": 11375 + }, + { + "secs": 0, + "nanos": 254750 + }, + { + "secs": 0, + "nanos": 3255250 + }, + { + "secs": 0, + "nanos": 15084 + }, + { + "secs": 0, + "nanos": 699333 + }, + { + "secs": 0, + "nanos": 2341083 + }, + { + "secs": 0, + "nanos": 13167 + }, + { + "secs": 0, + "nanos": 56429375 + }, + { + "secs": 0, + "nanos": 23292 + }, + { + "secs": 0, + "nanos": 266875 + }, + { + "secs": 0, + "nanos": 7875 + }, + { + "secs": 0, + "nanos": 6125 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 6000 + }, + { + "secs": 0, + "nanos": 5458 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 6083 + }, + { + "secs": 0, + "nanos": 4208 + }, + { + "secs": 0, + "nanos": 5833 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 28291 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 151, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_HkQDlinhQbCf3twlDtiPSA", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_HkQDlinhQbCf3twlDtiPSA", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "8b913888-4a02-4e32-9b59-02531f6a3ff6", + "content": "", + "tool_uses": [ + { + "id": "tooluse_psTtPl0pSXW4OptoieHkZQ", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet new webapi -n NetV3Server --no-https" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet new webapi -n NetV3Server --no-https" + } + } + ] + } + }, + "request_metadata": { + "request_id": "a4f06c4a-f70a-4483-8891-05a4fb8dfbbe", + "message_id": "8b913888-4a02-4e32-9b59-02531f6a3ff6", + "request_start_timestamp_ms": 1757962643695, + "stream_end_timestamp_ms": 1757962647473, + "time_to_first_chunk": { + "secs": 2, + "nanos": 292079542 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 40333 + }, + { + "secs": 0, + "nanos": 8583 + }, + { + "secs": 0, + "nanos": 901305167 + }, + { + "secs": 0, + "nanos": 44968875 + }, + { + "secs": 0, + "nanos": 92830958 + }, + { + "secs": 0, + "nanos": 86021625 + }, + { + "secs": 0, + "nanos": 72207125 + }, + { + "secs": 0, + "nanos": 102639459 + }, + { + "secs": 0, + "nanos": 122166625 + }, + { + "secs": 0, + "nanos": 1445459 + }, + { + "secs": 0, + "nanos": 2547625 + }, + { + "secs": 0, + "nanos": 18416 + }, + { + "secs": 0, + "nanos": 4235208 + }, + { + "secs": 0, + "nanos": 1623125 + }, + { + "secs": 0, + "nanos": 5031709 + }, + { + "secs": 0, + "nanos": 13625 + }, + { + "secs": 0, + "nanos": 880625 + }, + { + "secs": 0, + "nanos": 1485709 + }, + { + "secs": 0, + "nanos": 45831625 + }, + { + "secs": 0, + "nanos": 14541 + }, + { + "secs": 0, + "nanos": 5917 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 4833 + }, + { + "secs": 0, + "nanos": 10666 + }, + { + "secs": 0, + "nanos": 4167 + }, + { + "secs": 0, + "nanos": 8042 + }, + { + "secs": 0, + "nanos": 6250 + }, + { + "secs": 0, + "nanos": 34875 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 0, + "response_size": 186, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_psTtPl0pSXW4OptoieHkZQ", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_psTtPl0pSXW4OptoieHkZQ", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "The template \"ASP.NET Core Web API\" was created successfully.\n\nProcessing post-creation actions...\nRestoring /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server/NetV3Server.csproj:\n Determining projects to restore...\n Restored /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server/NetV3Server.csproj (in 2.26 sec).\nRestore succeeded.\n\n", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "63dd9120-5182-4430-a78b-dee7c07e7405", + "content": "", + "tool_uses": [ + { + "id": "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mv src/NetV3Server/NetV3Server/* src/NetV3Server/ && rmdir src/NetV3Server/NetV3Server" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mv src/NetV3Server/NetV3Server/* src/NetV3Server/ && rmdir src/NetV3Server/NetV3Server" + } + } + ] + } + }, + "request_metadata": { + "request_id": "30e1ad40-279c-44ea-9f8d-f3eba94dc26f", + "message_id": "63dd9120-5182-4430-a78b-dee7c07e7405", + "request_start_timestamp_ms": 1757966019104, + "stream_end_timestamp_ms": 1757966025345, + "time_to_first_chunk": { + "secs": 4, + "nanos": 828176417 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 121208 + }, + { + "secs": 0, + "nanos": 18875 + }, + { + "secs": 1, + "nanos": 24963875 + }, + { + "secs": 0, + "nanos": 60526917 + }, + { + "secs": 0, + "nanos": 41836000 + }, + { + "secs": 0, + "nanos": 103713666 + }, + { + "secs": 0, + "nanos": 83328958 + }, + { + "secs": 0, + "nanos": 30372000 + }, + { + "secs": 0, + "nanos": 8000 + }, + { + "secs": 0, + "nanos": 585500 + }, + { + "secs": 0, + "nanos": 478500 + }, + { + "secs": 0, + "nanos": 472334 + }, + { + "secs": 0, + "nanos": 751625 + }, + { + "secs": 0, + "nanos": 1519375 + }, + { + "secs": 0, + "nanos": 3958 + }, + { + "secs": 0, + "nanos": 781750 + }, + { + "secs": 0, + "nanos": 31811208 + }, + { + "secs": 0, + "nanos": 7125 + }, + { + "secs": 0, + "nanos": 2708 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 10334 + }, + { + "secs": 0, + "nanos": 4584 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 30589917 + }, + { + "secs": 0, + "nanos": 19791 + }, + { + "secs": 0, + "nanos": 10541 + }, + { + "secs": 0, + "nanos": 6333 + }, + { + "secs": 0, + "nanos": 3208 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 28500 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 0, + "response_size": 213, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "d1a5e9e6-1143-47db-82a7-d10c731a9c76", + "content": "", + "tool_uses": [ + { + "id": "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server/Controllers src/NetV3Server/Models src/NetV3Server/Services" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server/Controllers src/NetV3Server/Models src/NetV3Server/Services" + } + } + ] + } + }, + "request_metadata": { + "request_id": "b029c787-3276-4f33-87d3-84e69d1a6d71", + "message_id": "d1a5e9e6-1143-47db-82a7-d10c731a9c76", + "request_start_timestamp_ms": 1757972355190, + "stream_end_timestamp_ms": 1757972361232, + "time_to_first_chunk": { + "secs": 4, + "nanos": 622618875 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 19417 + }, + { + "secs": 0, + "nanos": 6792 + }, + { + "secs": 0, + "nanos": 872446750 + }, + { + "secs": 0, + "nanos": 84310459 + }, + { + "secs": 0, + "nanos": 9794000 + }, + { + "secs": 0, + "nanos": 62261458 + }, + { + "secs": 0, + "nanos": 13407208 + }, + { + "secs": 0, + "nanos": 23658375 + }, + { + "secs": 0, + "nanos": 43075208 + }, + { + "secs": 0, + "nanos": 25519875 + }, + { + "secs": 0, + "nanos": 52962417 + }, + { + "secs": 0, + "nanos": 57737250 + }, + { + "secs": 0, + "nanos": 79478000 + }, + { + "secs": 0, + "nanos": 30226333 + }, + { + "secs": 0, + "nanos": 1584166 + }, + { + "secs": 0, + "nanos": 7583 + }, + { + "secs": 0, + "nanos": 3083 + }, + { + "secs": 0, + "nanos": 1867917 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 252375 + }, + { + "secs": 0, + "nanos": 837375 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 29489584 + }, + { + "secs": 0, + "nanos": 16917 + }, + { + "secs": 0, + "nanos": 4333 + }, + { + "secs": 0, + "nanos": 10708 + }, + { + "secs": 0, + "nanos": 3917 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 4209 + }, + { + "secs": 0, + "nanos": 4000 + }, + { + "secs": 0, + "nanos": 4083 + }, + { + "secs": 0, + "nanos": 7125 + }, + { + "secs": 0, + "nanos": 3541 + }, + { + "secs": 0, + "nanos": 8083 + }, + { + "secs": 0, + "nanos": 29809958 + }, + { + "secs": 0, + "nanos": 21083 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 0, + "response_size": 211, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "81042895-d7b6-4b3b-8ca1-8456eb65266a", + "content": "", + "tool_uses": [ + { + "id": "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p tests/NetV3Server.Tests" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p tests/NetV3Server.Tests" + } + } + ] + } + }, + "request_metadata": { + "request_id": "954c77cc-f8a7-4650-988c-6655a8f11878", + "message_id": "81042895-d7b6-4b3b-8ca1-8456eb65266a", + "request_start_timestamp_ms": 1757972406411, + "stream_end_timestamp_ms": 1757972409050, + "time_to_first_chunk": { + "secs": 1, + "nanos": 944051042 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 61333 + }, + { + "secs": 0, + "nanos": 10875 + }, + { + "secs": 0, + "nanos": 680713500 + }, + { + "secs": 0, + "nanos": 5917 + }, + { + "secs": 0, + "nanos": 31041 + }, + { + "secs": 0, + "nanos": 15917 + }, + { + "secs": 0, + "nanos": 1458792 + }, + { + "secs": 0, + "nanos": 1427083 + }, + { + "secs": 0, + "nanos": 10500 + }, + { + "secs": 0, + "nanos": 1980709 + }, + { + "secs": 0, + "nanos": 9875 + }, + { + "secs": 0, + "nanos": 3389750 + }, + { + "secs": 0, + "nanos": 7083 + }, + { + "secs": 0, + "nanos": 3000 + }, + { + "secs": 0, + "nanos": 3791 + }, + { + "secs": 0, + "nanos": 1389000 + }, + { + "secs": 0, + "nanos": 9375 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 4584 + }, + { + "secs": 0, + "nanos": 4209 + }, + { + "secs": 0, + "nanos": 1503209 + }, + { + "secs": 0, + "nanos": 10583 + }, + { + "secs": 0, + "nanos": 1328209 + }, + { + "secs": 0, + "nanos": 2089416 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 159, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "a4a9af7f-f08a-4672-b74c-f15122f3bb31", + "content": "", + "tool_uses": [ + { + "id": "tooluse_9OHP7DT3TgaTw9YzknTtsg", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/tests/NetV3Server.Tests && dotnet new xunit -n NetV3Server.Tests" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/tests/NetV3Server.Tests && dotnet new xunit -n NetV3Server.Tests" + } + } + ] + } + }, + "request_metadata": { + "request_id": "fc8fcf52-7e36-4faa-8ac5-6b353fe5eda1", + "message_id": "a4a9af7f-f08a-4672-b74c-f15122f3bb31", + "request_start_timestamp_ms": 1757972420862, + "stream_end_timestamp_ms": 1757972423723, + "time_to_first_chunk": { + "secs": 2, + "nanos": 38255208 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 49917 + }, + { + "secs": 0, + "nanos": 10667 + }, + { + "secs": 0, + "nanos": 822215000 + }, + { + "secs": 0, + "nanos": 11833 + }, + { + "secs": 0, + "nanos": 372583 + }, + { + "secs": 0, + "nanos": 6000 + }, + { + "secs": 0, + "nanos": 2792 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 2666 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 15542 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 5541 + }, + { + "secs": 0, + "nanos": 12291 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 0, + "nanos": 5166 + }, + { + "secs": 0, + "nanos": 4208 + }, + { + "secs": 0, + "nanos": 2959 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 59875 + }, + { + "secs": 0, + "nanos": 5833 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2958 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 2666 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 5041 + }, + { + "secs": 0, + "nanos": 11708 + }, + { + "secs": 0, + "nanos": 84 + } + ], + "user_prompt_length": 0, + "response_size": 188, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_9OHP7DT3TgaTw9YzknTtsg", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "CancelledToolUses": { + "prompt": "Why would you need a test?", + "tool_use_results": [ + { + "tool_use_id": "tooluse_9OHP7DT3TgaTw9YzknTtsg", + "content": [ + { + "Text": "Tool use was cancelled by the user" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": "2025-09-15T14:41:17.037813-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "d8f2f907-82bc-43ac-857a-0f94e382436a", + "content": "You're right to question that. For your immediate goal of creating a test server similar to the Python one, you **don't need tests**.\n\nThe test project was just following .NET conventions, but since you're building a test server (which itself is a testing tool), adding unit tests would be overkill and unnecessary complexity.\n\nLet me clean up and show you the minimal structure you actually need:", + "tool_uses": [ + { + "id": "tooluse_ywmS-to2Ryuo37X2iEyPog", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && rm -rf tests" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && rm -rf tests" + } + } + ] + } + }, + "request_metadata": { + "request_id": "6cb23bb0-b678-4c03-a8e9-2bd59e8a1bf4", + "message_id": "d8f2f907-82bc-43ac-857a-0f94e382436a", + "request_start_timestamp_ms": 1757972477248, + "stream_end_timestamp_ms": 1757972483194, + "time_to_first_chunk": { + "secs": 2, + "nanos": 120958417 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 12959 + }, + { + "secs": 0, + "nanos": 3417 + }, + { + "secs": 0, + "nanos": 93805625 + }, + { + "secs": 0, + "nanos": 599700791 + }, + { + "secs": 0, + "nanos": 101509584 + }, + { + "secs": 0, + "nanos": 349305791 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 68222208 + }, + { + "secs": 0, + "nanos": 156565292 + }, + { + "secs": 0, + "nanos": 185222667 + }, + { + "secs": 0, + "nanos": 238502417 + }, + { + "secs": 0, + "nanos": 201262917 + }, + { + "secs": 0, + "nanos": 117300250 + }, + { + "secs": 0, + "nanos": 105225458 + }, + { + "secs": 0, + "nanos": 56738167 + }, + { + "secs": 0, + "nanos": 179426125 + }, + { + "secs": 0, + "nanos": 99785458 + }, + { + "secs": 0, + "nanos": 224553584 + }, + { + "secs": 0, + "nanos": 172400250 + }, + { + "secs": 0, + "nanos": 122294625 + }, + { + "secs": 0, + "nanos": 154151292 + }, + { + "secs": 0, + "nanos": 568565167 + }, + { + "secs": 0, + "nanos": 1006959 + }, + { + "secs": 0, + "nanos": 6167 + }, + { + "secs": 0, + "nanos": 2573250 + }, + { + "secs": 0, + "nanos": 1787208 + }, + { + "secs": 0, + "nanos": 5102917 + }, + { + "secs": 0, + "nanos": 847958 + }, + { + "secs": 0, + "nanos": 1825666 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 1794917 + }, + { + "secs": 0, + "nanos": 3417 + }, + { + "secs": 0, + "nanos": 931834 + }, + { + "secs": 0, + "nanos": 3042 + }, + { + "secs": 0, + "nanos": 2808875 + }, + { + "secs": 0, + "nanos": 6084 + }, + { + "secs": 0, + "nanos": 1212666 + }, + { + "secs": 0, + "nanos": 4584 + }, + { + "secs": 0, + "nanos": 1206875 + }, + { + "secs": 0, + "nanos": 1932625 + }, + { + "secs": 0, + "nanos": 8916 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 5943625 + }, + { + "secs": 0, + "nanos": 1865542 + }, + { + "secs": 0, + "nanos": 0 + } + ], + "user_prompt_length": 185, + "response_size": 536, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_ywmS-to2Ryuo37X2iEyPog", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_ywmS-to2Ryuo37X2iEyPog", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "9afcfa92-43f7-4b8e-9d0a-09fad6581ffb", + "content": "", + "tool_uses": [ + { + "id": "tooluse_EGhkURTAQsycx_K8Fc_lxQ", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet sln add src/NetV3Server/NetV3Server.csproj" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet sln add src/NetV3Server/NetV3Server.csproj" + } + } + ] + } + }, + "request_metadata": { + "request_id": "0a199baf-57d4-45ac-8f9f-bb384224dc9a", + "message_id": "9afcfa92-43f7-4b8e-9d0a-09fad6581ffb", + "request_start_timestamp_ms": 1757972495247, + "stream_end_timestamp_ms": 1757972498232, + "time_to_first_chunk": { + "secs": 1, + "nanos": 961189667 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 18792 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 728345708 + }, + { + "secs": 0, + "nanos": 10906667 + }, + { + "secs": 0, + "nanos": 16989667 + }, + { + "secs": 0, + "nanos": 9073458 + }, + { + "secs": 0, + "nanos": 5730500 + }, + { + "secs": 0, + "nanos": 3961166 + }, + { + "secs": 0, + "nanos": 5743250 + }, + { + "secs": 0, + "nanos": 27732333 + }, + { + "secs": 0, + "nanos": 5974125 + }, + { + "secs": 0, + "nanos": 5083 + }, + { + "secs": 0, + "nanos": 6904417 + }, + { + "secs": 0, + "nanos": 7417 + }, + { + "secs": 0, + "nanos": 3062458 + }, + { + "secs": 0, + "nanos": 4459 + }, + { + "secs": 0, + "nanos": 1738375 + }, + { + "secs": 0, + "nanos": 4563333 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 5560125 + }, + { + "secs": 0, + "nanos": 3875 + }, + { + "secs": 0, + "nanos": 2023458 + }, + { + "secs": 0, + "nanos": 8916 + }, + { + "secs": 0, + "nanos": 9417 + }, + { + "secs": 0, + "nanos": 3833 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 2416 + }, + { + "secs": 0, + "nanos": 4997334 + }, + { + "secs": 0, + "nanos": 178881250 + }, + { + "secs": 0, + "nanos": 858958 + }, + { + "secs": 0, + "nanos": 84 + } + ], + "user_prompt_length": 0, + "response_size": 176, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_EGhkURTAQsycx_K8Fc_lxQ", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_EGhkURTAQsycx_K8Fc_lxQ", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": "Project `src/NetV3Server/NetV3Server.csproj` added to the solution.", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "aca358a5-2732-4cc6-a222-10d9e3bc2bef", + "content": "", + "tool_uses": [ + { + "id": "tooluse_gRBhKrICTrmLbIMX9ycgDQ", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server", + "depth": 3 + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Directory", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server", + "depth": 3 + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "ab319ca4-f128-445c-be9f-e85830a5a785", + "message_id": "aca358a5-2732-4cc6-a222-10d9e3bc2bef", + "request_start_timestamp_ms": 1757972559856, + "stream_end_timestamp_ms": 1757972562721, + "time_to_first_chunk": { + "secs": 2, + "nanos": 586168250 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 18250 + }, + { + "secs": 0, + "nanos": 3959 + }, + { + "secs": 0, + "nanos": 179495292 + }, + { + "secs": 0, + "nanos": 6667 + }, + { + "secs": 0, + "nanos": 6704208 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 1265083 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 4011916 + }, + { + "secs": 0, + "nanos": 4001250 + }, + { + "secs": 0, + "nanos": 1400000 + }, + { + "secs": 0, + "nanos": 2945208 + }, + { + "secs": 0, + "nanos": 6334 + }, + { + "secs": 0, + "nanos": 1335333 + }, + { + "secs": 0, + "nanos": 2159708 + }, + { + "secs": 0, + "nanos": 735875 + }, + { + "secs": 0, + "nanos": 6416 + }, + { + "secs": 0, + "nanos": 2649167 + }, + { + "secs": 0, + "nanos": 2590166 + }, + { + "secs": 0, + "nanos": 574250 + }, + { + "secs": 0, + "nanos": 6375 + }, + { + "secs": 0, + "nanos": 1127750 + }, + { + "secs": 0, + "nanos": 5586708 + }, + { + "secs": 0, + "nanos": 3781459 + }, + { + "secs": 0, + "nanos": 5579917 + }, + { + "secs": 0, + "nanos": 5114333 + }, + { + "secs": 0, + "nanos": 10913167 + }, + { + "secs": 0, + "nanos": 1972875 + }, + { + "secs": 0, + "nanos": 33417291 + }, + { + "secs": 0, + "nanos": 1440208 + }, + { + "secs": 0, + "nanos": 41 + } + ], + "user_prompt_length": 0, + "response_size": 163, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_gRBhKrICTrmLbIMX9ycgDQ", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_gRBhKrICTrmLbIMX9ycgDQ", + "content": [ + { + "Text": "drwxr-xr-x 3 504 20 96 Sep 15 18:45 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp\n-rw-r--r-- 1 504 20 5290 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/README.md\n-rw-r--r-- 1 504 20 1281 Sep 15 21:42 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/NetV3Server.sln\ndrwxr-xr-x 3 504 20 96 Sep 15 18:57 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src\n-rw-r--r-- 1 504 20 523057 Sep 15 18:24 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json\ndrwxr-xr-x 13 504 20 416 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server\n-rw-r--r-- 1 504 20 409 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj\ndrwxr-xr-x 8 504 20 256 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/bin\n-rw-r--r-- 1 504 20 151 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models\ndrwxr-xr-x 3 504 20 96 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Properties\n-rw-r--r-- 1 504 20 135 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.http\n-rw-r--r-- 1 504 20 127 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services\n-rw-r--r-- 1 504 20 1142 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs\n-rw-r--r-- 1 504 20 2068 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.props\n-rw-r--r-- 1 504 20 546 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.targets\n-rw-r--r-- 1 504 20 1166 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.nuget.cache\n-rw-r--r-- 1 504 20 2742 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.dgspec.json\n-rw-r--r-- 1 504 20 24901 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.assets.json\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/Debug\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/bin/Debug\n-rw-r--r-- 1 504 20 805 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "1068d872-9892-43fe-9899-7e11ad61d5f5", + "content": "Perfect! Now you have a clean, minimal .NET project structure:\n\n```\nnet-v3-server/\n├── NetV3Server.sln # Solution file\n├── src/NetV3Server/\n│ ├── Controllers/ # Your API endpoints (empty, ready for you)\n│ ├── Models/ # Request/response models (empty, ready for you) \n│ ├── Services/ # Business logic like ClientCacheService (empty, ready for you)\n│ ├── Program.cs # Entry point (like main.py)\n│ ├── NetV3Server.csproj # Project dependencies\n│ └── appsettings.json # Configuration\n└── README.md\n```\n\nYou're right - no tests needed. This test server **is** the test. Just like the Python version, it's a tool for testing the S3 encryption client, not something that needs its own unit tests.\n\nNow you can start implementing the controllers and services to mirror the Python FastAPI functionality." + } + }, + "request_metadata": { + "request_id": "bee48976-afdb-4f81-b92d-07cf9a8a1058", + "message_id": "1068d872-9892-43fe-9899-7e11ad61d5f5", + "request_start_timestamp_ms": 1757972563209, + "stream_end_timestamp_ms": 1757972571769, + "time_to_first_chunk": { + "secs": 4, + "nanos": 704986208 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 15584 + }, + { + "secs": 0, + "nanos": 107281834 + }, + { + "secs": 0, + "nanos": 93954500 + }, + { + "secs": 0, + "nanos": 113215083 + }, + { + "secs": 0, + "nanos": 11278584 + }, + { + "secs": 0, + "nanos": 1788667 + }, + { + "secs": 0, + "nanos": 4387042 + }, + { + "secs": 0, + "nanos": 1229666 + }, + { + "secs": 0, + "nanos": 2290958 + }, + { + "secs": 0, + "nanos": 3882125 + }, + { + "secs": 0, + "nanos": 3947125 + }, + { + "secs": 0, + "nanos": 3914834 + }, + { + "secs": 0, + "nanos": 2141208 + }, + { + "secs": 0, + "nanos": 2169250 + }, + { + "secs": 0, + "nanos": 1919292 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 1358208 + }, + { + "secs": 0, + "nanos": 2065250 + }, + { + "secs": 0, + "nanos": 1791083 + }, + { + "secs": 0, + "nanos": 2102541 + }, + { + "secs": 0, + "nanos": 1972250 + }, + { + "secs": 0, + "nanos": 17958 + }, + { + "secs": 0, + "nanos": 9583 + }, + { + "secs": 0, + "nanos": 17083 + }, + { + "secs": 0, + "nanos": 17625 + }, + { + "secs": 0, + "nanos": 17416 + }, + { + "secs": 0, + "nanos": 24917 + }, + { + "secs": 0, + "nanos": 20791 + }, + { + "secs": 0, + "nanos": 17125 + }, + { + "secs": 0, + "nanos": 234418958 + }, + { + "secs": 0, + "nanos": 6417 + }, + { + "secs": 0, + "nanos": 16750 + }, + { + "secs": 0, + "nanos": 17208 + }, + { + "secs": 0, + "nanos": 6000 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 667 + } + ], + "user_prompt_length": 0, + "response_size": 964, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "What is this bin, obj, properties, appsetting.json, appsettings.Development.json, other .csproj and http files?" + } + }, + "timestamp": "2025-09-15T14:45:08.153822-07:00", + "images": null + }, + "assistant": { + "Response": { + "message_id": "4bb7d21d-1a97-404d-92de-42f5fe1fcd9d", + "content": "These are standard .NET project files - here's what each does:\n\n## Files You'll Work With:\n- **Program.cs** - Your main entry point (like main.py)\n- **NetV3Server.csproj** - Project file listing dependencies (like pyproject.toml)\n- **appsettings.json** - Configuration (ports, connection strings, etc.)\n\n## Files You Can Ignore:\n- **bin/** - Compiled output (like Python's __pycache__)\n- **obj/** - Build artifacts and temporary files\n- **Properties/launchSettings.json** - IDE debugging settings\n- **appsettings.Development.json** - Dev-only config overrides\n- **NetV3Server.http** - Sample HTTP requests for testing\n\n## What to Focus On:\n1. **Program.cs** - Configure your web server and routes\n2. **Controllers/** - Your API endpoints (`/client`, `/object/{bucket}/{key}`)\n3. **Models/** - Request/response classes\n4. **Services/** - Business logic (client cache)\n5. **NetV3Server.csproj** - Add AWS SDK packages here\n\nThe `bin` and `obj` folders are like Python's virtual environment artifacts - generated during build, safe to delete, and should be in `.gitignore`.\n\nThink of it this way:\n- **bin/obj** = Python's `.venv` and `__pycache__`\n- **appsettings.json** = Your config file\n- **Program.cs** = Your `main.py`\n- **.csproj** = Your `pyproject.toml`\n\nYou only need to touch the first 5 items I mentioned." + } + }, + "request_metadata": { + "request_id": "dcc33812-f48c-4607-95fe-ee75aee4d2f2", + "message_id": "4bb7d21d-1a97-404d-92de-42f5fe1fcd9d", + "request_start_timestamp_ms": 1757972708157, + "stream_end_timestamp_ms": 1757972719336, + "time_to_first_chunk": { + "secs": 2, + "nanos": 551164542 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 39291 + }, + { + "secs": 0, + "nanos": 6875 + }, + { + "secs": 0, + "nanos": 281801416 + }, + { + "secs": 0, + "nanos": 226044750 + }, + { + "secs": 0, + "nanos": 363376667 + }, + { + "secs": 0, + "nanos": 152234000 + }, + { + "secs": 0, + "nanos": 100946250 + }, + { + "secs": 0, + "nanos": 169830042 + }, + { + "secs": 0, + "nanos": 111369625 + }, + { + "secs": 0, + "nanos": 57304291 + }, + { + "secs": 0, + "nanos": 278657625 + }, + { + "secs": 0, + "nanos": 117190541 + }, + { + "secs": 0, + "nanos": 371903875 + }, + { + "secs": 0, + "nanos": 62885458 + }, + { + "secs": 0, + "nanos": 161187500 + }, + { + "secs": 0, + "nanos": 106972125 + }, + { + "secs": 0, + "nanos": 310951417 + }, + { + "secs": 0, + "nanos": 165540500 + }, + { + "secs": 0, + "nanos": 226349458 + }, + { + "secs": 0, + "nanos": 62893459 + }, + { + "secs": 0, + "nanos": 234520750 + }, + { + "secs": 0, + "nanos": 210761166 + }, + { + "secs": 0, + "nanos": 105610125 + }, + { + "secs": 0, + "nanos": 218673125 + }, + { + "secs": 0, + "nanos": 64097708 + }, + { + "secs": 0, + "nanos": 138821958 + }, + { + "secs": 0, + "nanos": 168110667 + }, + { + "secs": 0, + "nanos": 336051416 + }, + { + "secs": 0, + "nanos": 119190250 + }, + { + "secs": 0, + "nanos": 263103875 + }, + { + "secs": 0, + "nanos": 52826333 + }, + { + "secs": 0, + "nanos": 341796667 + }, + { + "secs": 0, + "nanos": 328502333 + }, + { + "secs": 0, + "nanos": 278887042 + }, + { + "secs": 0, + "nanos": 170325333 + }, + { + "secs": 0, + "nanos": 171507792 + }, + { + "secs": 0, + "nanos": 54421916 + }, + { + "secs": 0, + "nanos": 216826500 + }, + { + "secs": 0, + "nanos": 253941708 + }, + { + "secs": 0, + "nanos": 373136500 + }, + { + "secs": 0, + "nanos": 79289875 + }, + { + "secs": 0, + "nanos": 111929625 + }, + { + "secs": 0, + "nanos": 111010875 + }, + { + "secs": 0, + "nanos": 180891916 + }, + { + "secs": 0, + "nanos": 44498042 + }, + { + "secs": 0, + "nanos": 253745875 + }, + { + "secs": 0, + "nanos": 150561792 + }, + { + "secs": 0, + "nanos": 260028084 + }, + { + "secs": 0, + "nanos": 5878625 + }, + { + "secs": 0, + "nanos": 209 + }, + { + "secs": 0, + "nanos": 208 + } + ], + "user_prompt_length": 269, + "response_size": 1313, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "So, what is the next steps?" + } + }, + "timestamp": "2025-09-15T14:53:23.896032-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "87908723-d8de-4e89-9d89-f192b16b330f", + "content": "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nAdd the S3 encryption client and AWS SDK packages to your project:", + "tool_uses": [ + { + "id": "tooluse_qA7SLDIgT7uQDHHjHtuWeg", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package AWSSDK.S3 && dotnet add package AWSSDK.KeyManagementService" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package AWSSDK.S3 && dotnet add package AWSSDK.KeyManagementService" + } + } + ] + } + }, + "request_metadata": { + "request_id": "3df8ed85-997c-4e54-9d49-266bedfd419a", + "message_id": "87908723-d8de-4e89-9d89-f192b16b330f", + "request_start_timestamp_ms": 1757973203899, + "stream_end_timestamp_ms": 1757973210961, + "time_to_first_chunk": { + "secs": 4, + "nanos": 554073042 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 46375 + }, + { + "secs": 0, + "nanos": 11875 + }, + { + "secs": 0, + "nanos": 387612916 + }, + { + "secs": 0, + "nanos": 306701125 + }, + { + "secs": 0, + "nanos": 67498542 + }, + { + "secs": 0, + "nanos": 241621459 + }, + { + "secs": 1, + "nanos": 101653625 + }, + { + "secs": 0, + "nanos": 100018542 + }, + { + "secs": 0, + "nanos": 72277250 + }, + { + "secs": 0, + "nanos": 95738792 + }, + { + "secs": 0, + "nanos": 68908917 + }, + { + "secs": 0, + "nanos": 1585250 + }, + { + "secs": 0, + "nanos": 14833 + }, + { + "secs": 0, + "nanos": 1207000 + }, + { + "secs": 0, + "nanos": 2098292 + }, + { + "secs": 0, + "nanos": 3440000 + }, + { + "secs": 0, + "nanos": 13750 + }, + { + "secs": 0, + "nanos": 2200792 + }, + { + "secs": 0, + "nanos": 1829750 + }, + { + "secs": 0, + "nanos": 2034250 + }, + { + "secs": 0, + "nanos": 49088750 + }, + { + "secs": 0, + "nanos": 7833 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 873000 + }, + { + "secs": 0, + "nanos": 3833 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 2002917 + }, + { + "secs": 0, + "nanos": 6083 + }, + { + "secs": 0, + "nanos": 2333 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 7291 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 185, + "response_size": 350, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_qA7SLDIgT7uQDHHjHtuWeg", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "CancelledToolUses": { + "prompt": "Can you list them first?", + "tool_use_results": [ + { + "tool_use_id": "tooluse_qA7SLDIgT7uQDHHjHtuWeg", + "content": [ + { + "Text": "Tool use was cancelled by the user" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": "2025-09-15T14:53:57.015219-07:00", + "images": null + }, + "assistant": { + "Response": { + "message_id": "9a107d49-e417-4d62-8eda-b8bc021f296e", + "content": "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nYou'll need these NuGet packages:\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK \n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n\n## 2. Configure the Server\n- Set port to 8081 in `appsettings.json` (to match Python server)\n- Configure dependency injection in `Program.cs`\n\n## 3. Create Models\n- `ClientRequest.cs` - For POST /client requests\n- `ClientResponse.cs` - For client creation responses\n- `ErrorModels.cs` - For error responses\n\n## 4. Create Services\n- `ClientCacheService.cs` - In-memory client storage (like Python's client_cache dict)\n\n## 5. Create Controllers\n- `ClientController.cs` - Handle POST /client\n- `ObjectController.cs` - Handle PUT/GET /object/{bucket}/{key}\n\n## 6. Test\n- Run the server and verify it starts on port 8081\n- Test endpoints match Python server behavior\n\nWant me to help with step 1 (adding the packages) first?" + } + }, + "request_metadata": { + "request_id": "d559997f-8e52-4b64-82a5-63fe40203a81", + "message_id": "9a107d49-e417-4d62-8eda-b8bc021f296e", + "request_start_timestamp_ms": 1757973237376, + "stream_end_timestamp_ms": 1757973245216, + "time_to_first_chunk": { + "secs": 2, + "nanos": 254640791 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 14833 + }, + { + "secs": 0, + "nanos": 3834 + }, + { + "secs": 0, + "nanos": 106632625 + }, + { + "secs": 0, + "nanos": 242511792 + }, + { + "secs": 0, + "nanos": 215528250 + }, + { + "secs": 0, + "nanos": 161817834 + }, + { + "secs": 0, + "nanos": 115514917 + }, + { + "secs": 0, + "nanos": 200784000 + }, + { + "secs": 0, + "nanos": 154354208 + }, + { + "secs": 0, + "nanos": 169257625 + }, + { + "secs": 0, + "nanos": 113756916 + }, + { + "secs": 0, + "nanos": 438160166 + }, + { + "secs": 0, + "nanos": 116102000 + }, + { + "secs": 0, + "nanos": 196862500 + }, + { + "secs": 0, + "nanos": 223619333 + }, + { + "secs": 0, + "nanos": 193304625 + }, + { + "secs": 0, + "nanos": 318715333 + }, + { + "secs": 0, + "nanos": 255427959 + }, + { + "secs": 0, + "nanos": 200595792 + }, + { + "secs": 0, + "nanos": 54208625 + }, + { + "secs": 0, + "nanos": 72667375 + }, + { + "secs": 0, + "nanos": 210610041 + }, + { + "secs": 0, + "nanos": 330727541 + }, + { + "secs": 0, + "nanos": 759643041 + }, + { + "secs": 0, + "nanos": 38890667 + }, + { + "secs": 0, + "nanos": 21869625 + }, + { + "secs": 0, + "nanos": 14905333 + }, + { + "secs": 0, + "nanos": 7768333 + }, + { + "secs": 0, + "nanos": 20798167 + }, + { + "secs": 0, + "nanos": 185438083 + }, + { + "secs": 0, + "nanos": 336475458 + }, + { + "secs": 0, + "nanos": 94192084 + }, + { + "secs": 0, + "nanos": 10025166 + }, + { + "secs": 0, + "nanos": 83 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 182, + "response_size": 957, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Before doing this can I have a gitignore? Where should I put this?" + } + }, + "timestamp": "2025-09-15T14:55:46.471426-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "9ac83ad7-9f4b-4810-a2fc-bc98929ce92a", + "content": "You should put the `.gitignore` file in the root of your `net-v3-server` directory (same level as the `.sln` file).", + "tool_uses": [ + { + "id": "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "command": "create", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", + "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" + }, + "orig_args": { + "command": "create", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", + "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" + } + } + ] + } + }, + "request_metadata": { + "request_id": "0116edd0-30b4-4ffd-81ec-5dedd50a1e4a", + "message_id": "9ac83ad7-9f4b-4810-a2fc-bc98929ce92a", + "request_start_timestamp_ms": 1757973346476, + "stream_end_timestamp_ms": 1757973354602, + "time_to_first_chunk": { + "secs": 2, + "nanos": 615486542 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 14375 + }, + { + "secs": 0, + "nanos": 7750 + }, + { + "secs": 0, + "nanos": 396886042 + }, + { + "secs": 0, + "nanos": 245263417 + }, + { + "secs": 0, + "nanos": 306512709 + }, + { + "secs": 0, + "nanos": 175293292 + }, + { + "secs": 0, + "nanos": 644961208 + }, + { + "secs": 0, + "nanos": 521756334 + }, + { + "secs": 0, + "nanos": 3542 + }, + { + "secs": 0, + "nanos": 13166 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 467408959 + }, + { + "secs": 0, + "nanos": 34375 + }, + { + "secs": 0, + "nanos": 7625 + }, + { + "secs": 0, + "nanos": 3015917 + }, + { + "secs": 0, + "nanos": 12302791 + }, + { + "secs": 0, + "nanos": 45292 + }, + { + "secs": 0, + "nanos": 1234666 + }, + { + "secs": 0, + "nanos": 24458 + }, + { + "secs": 0, + "nanos": 949833 + }, + { + "secs": 0, + "nanos": 22834 + }, + { + "secs": 0, + "nanos": 49575167 + }, + { + "secs": 0, + "nanos": 34209 + }, + { + "secs": 0, + "nanos": 12333 + }, + { + "secs": 0, + "nanos": 10084 + }, + { + "secs": 0, + "nanos": 10167 + }, + { + "secs": 0, + "nanos": 9250 + }, + { + "secs": 0, + "nanos": 110500 + }, + { + "secs": 0, + "nanos": 106833 + }, + { + "secs": 0, + "nanos": 6375 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 6041 + }, + { + "secs": 2, + "nanos": 613837709 + }, + { + "secs": 0, + "nanos": 1257583 + }, + { + "secs": 0, + "nanos": 1144584 + }, + { + "secs": 0, + "nanos": 6083 + }, + { + "secs": 0, + "nanos": 249500 + }, + { + "secs": 0, + "nanos": 2163042 + }, + { + "secs": 0, + "nanos": 6791 + }, + { + "secs": 0, + "nanos": 5571917 + }, + { + "secs": 0, + "nanos": 8250 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 399666 + }, + { + "secs": 0, + "nanos": 5875 + }, + { + "secs": 0, + "nanos": 1362000 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 587834 + }, + { + "secs": 0, + "nanos": 1689041 + }, + { + "secs": 0, + "nanos": 4542 + }, + { + "secs": 0, + "nanos": 2255416 + }, + { + "secs": 0, + "nanos": 48497250 + }, + { + "secs": 0, + "nanos": 24833 + }, + { + "secs": 0, + "nanos": 6125 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 548291 + }, + { + "secs": 0, + "nanos": 83833 + }, + { + "secs": 0, + "nanos": 9000 + }, + { + "secs": 0, + "nanos": 15375 + }, + { + "secs": 0, + "nanos": 15000 + }, + { + "secs": 0, + "nanos": 6584 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 421959 + }, + { + "secs": 0, + "nanos": 6166 + }, + { + "secs": 0, + "nanos": 56125 + }, + { + "secs": 0, + "nanos": 6459 + }, + { + "secs": 0, + "nanos": 5584 + }, + { + "secs": 0, + "nanos": 6375 + }, + { + "secs": 0, + "nanos": 2640250 + }, + { + "secs": 0, + "nanos": 15333 + }, + { + "secs": 0, + "nanos": 6500 + }, + { + "secs": 0, + "nanos": 5666 + }, + { + "secs": 0, + "nanos": 5666 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 5166 + }, + { + "secs": 0, + "nanos": 6750 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 4833 + }, + { + "secs": 0, + "nanos": 5708 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 4958 + }, + { + "secs": 0, + "nanos": 4958 + }, + { + "secs": 0, + "nanos": 4834 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 5041 + }, + { + "secs": 0, + "nanos": 4792 + }, + { + "secs": 0, + "nanos": 235917 + }, + { + "secs": 0, + "nanos": 23083 + }, + { + "secs": 0, + "nanos": 7209 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 11334 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 36500 + }, + { + "secs": 0, + "nanos": 19708 + }, + { + "secs": 0, + "nanos": 10416 + }, + { + "secs": 0, + "nanos": 5875 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 15333 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 224, + "response_size": 768, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "CancelledToolUses": { + "prompt": "Question: What is difference between this and gitignore in the root?", + "tool_use_results": [ + { + "tool_use_id": "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", + "content": [ + { + "Text": "Tool use was cancelled by the user" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": "2025-09-15T14:56:38.144732-07:00", + "images": null + }, + "assistant": { + "Response": { + "message_id": "7b0357b9-12a2-41e0-887e-c50f7991c3fc", + "content": "Good question! Here's the difference:\n\n## Root `.gitignore` (python repo level)\n- Located at `/amazon-s3-encryption-client-python/.gitignore`\n- Ignores files for the **entire Python repository**\n- Already exists and handles Python-specific files (`.pyenv`, `__pycache__`, etc.)\n\n## Net server `.gitignore` (net-v3-server level) \n- Would be at `/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore`\n- Ignores files **only for the .NET project**\n- Handles .NET-specific files (`bin/`, `obj/`, `.vs/`, etc.)\n\n## Why You Need Both:\n- Root ignores Python stuff globally\n- Net server ignores .NET stuff locally\n- Git processes `.gitignore` files hierarchically - it reads both\n\n## Alternative:\nYou could add .NET ignore patterns to the root `.gitignore` instead:\n```\n# Add to root .gitignore\ntest-server/net-v3-server/bin/\ntest-server/net-v3-server/obj/\ntest-server/net-v3-server/.vs/\n```\n\n**Recommendation:** Create a local `.gitignore` in `net-v3-server/` - it's cleaner and keeps .NET concerns separate from Python concerns." + } + }, + "request_metadata": { + "request_id": "64bf9e0f-fcf6-4134-9abe-76c9ebd91173", + "message_id": "7b0357b9-12a2-41e0-887e-c50f7991c3fc", + "request_start_timestamp_ms": 1757973398447, + "stream_end_timestamp_ms": 1757973416538, + "time_to_first_chunk": { + "secs": 11, + "nanos": 995218500 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 55334 + }, + { + "secs": 0, + "nanos": 29875 + }, + { + "secs": 0, + "nanos": 131826792 + }, + { + "secs": 0, + "nanos": 288688833 + }, + { + "secs": 0, + "nanos": 276512500 + }, + { + "secs": 0, + "nanos": 54733375 + }, + { + "secs": 0, + "nanos": 113804417 + }, + { + "secs": 0, + "nanos": 283913708 + }, + { + "secs": 0, + "nanos": 92647250 + }, + { + "secs": 0, + "nanos": 307415917 + }, + { + "secs": 0, + "nanos": 830049750 + }, + { + "secs": 0, + "nanos": 6912750 + }, + { + "secs": 0, + "nanos": 19802875 + }, + { + "secs": 0, + "nanos": 5754459 + }, + { + "secs": 0, + "nanos": 30504542 + }, + { + "secs": 0, + "nanos": 47366084 + }, + { + "secs": 0, + "nanos": 169927250 + }, + { + "secs": 0, + "nanos": 37373000 + }, + { + "secs": 0, + "nanos": 183769292 + }, + { + "secs": 0, + "nanos": 110210792 + }, + { + "secs": 0, + "nanos": 16200417 + }, + { + "secs": 0, + "nanos": 119389000 + }, + { + "secs": 0, + "nanos": 169745041 + }, + { + "secs": 0, + "nanos": 170980750 + }, + { + "secs": 0, + "nanos": 329810542 + }, + { + "secs": 0, + "nanos": 96640667 + }, + { + "secs": 0, + "nanos": 506309917 + }, + { + "secs": 0, + "nanos": 109476416 + }, + { + "secs": 0, + "nanos": 177202208 + }, + { + "secs": 0, + "nanos": 158928500 + }, + { + "secs": 0, + "nanos": 173579292 + }, + { + "secs": 0, + "nanos": 52149042 + }, + { + "secs": 0, + "nanos": 54382167 + }, + { + "secs": 0, + "nanos": 166240792 + }, + { + "secs": 0, + "nanos": 798802875 + }, + { + "secs": 0, + "nanos": 8083 + }, + { + "secs": 0, + "nanos": 1430583 + }, + { + "secs": 0, + "nanos": 3917 + }, + { + "secs": 0, + "nanos": 2254875 + }, + { + "secs": 0, + "nanos": 41 + }, + { + "secs": 0, + "nanos": 0 + } + ], + "user_prompt_length": 226, + "response_size": 1042, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Sure. Let's create gitignore in net-v3-server" + } + }, + "timestamp": "2025-09-15T14:57:24.132611-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "7fba195a-2a3f-45bf-ba4e-6c923a4d0362", + "content": "", + "tool_uses": [ + { + "id": "tooluse_5IYUis90Sq-TiqFl_6Utbw", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", + "command": "create", + "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", + "command": "create", + "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" + } + } + ] + } + }, + "request_metadata": { + "request_id": "695e022a-92e5-4643-a180-76bb12f9352b", + "message_id": "7fba195a-2a3f-45bf-ba4e-6c923a4d0362", + "request_start_timestamp_ms": 1757973444139, + "stream_end_timestamp_ms": 1757973448757, + "time_to_first_chunk": { + "secs": 2, + "nanos": 286244541 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 24750 + }, + { + "secs": 0, + "nanos": 6791 + }, + { + "secs": 0, + "nanos": 469352583 + }, + { + "secs": 0, + "nanos": 14292 + }, + { + "secs": 0, + "nanos": 88425333 + }, + { + "secs": 0, + "nanos": 20918541 + }, + { + "secs": 0, + "nanos": 53751208 + }, + { + "secs": 0, + "nanos": 12485792 + }, + { + "secs": 0, + "nanos": 940208 + }, + { + "secs": 0, + "nanos": 8081833 + }, + { + "secs": 0, + "nanos": 1722333 + }, + { + "secs": 0, + "nanos": 2647417 + }, + { + "secs": 0, + "nanos": 1242917 + }, + { + "secs": 0, + "nanos": 10784792 + }, + { + "secs": 0, + "nanos": 10250 + }, + { + "secs": 0, + "nanos": 852167 + }, + { + "secs": 0, + "nanos": 4220500 + }, + { + "secs": 0, + "nanos": 1970042 + }, + { + "secs": 0, + "nanos": 10881667 + }, + { + "secs": 0, + "nanos": 25250 + }, + { + "secs": 0, + "nanos": 11542 + }, + { + "secs": 0, + "nanos": 9834 + }, + { + "secs": 0, + "nanos": 2134958 + }, + { + "secs": 1, + "nanos": 573889917 + }, + { + "secs": 0, + "nanos": 14459 + }, + { + "secs": 0, + "nanos": 695459 + }, + { + "secs": 0, + "nanos": 56291 + }, + { + "secs": 0, + "nanos": 1851208 + }, + { + "secs": 0, + "nanos": 3820625 + }, + { + "secs": 0, + "nanos": 20417 + }, + { + "secs": 0, + "nanos": 1573458 + }, + { + "secs": 0, + "nanos": 6542 + }, + { + "secs": 0, + "nanos": 1630209 + }, + { + "secs": 0, + "nanos": 7084 + }, + { + "secs": 0, + "nanos": 1313417 + }, + { + "secs": 0, + "nanos": 357667 + }, + { + "secs": 0, + "nanos": 6208 + }, + { + "secs": 0, + "nanos": 1068916 + }, + { + "secs": 0, + "nanos": 6666 + }, + { + "secs": 0, + "nanos": 806875 + }, + { + "secs": 0, + "nanos": 8208 + }, + { + "secs": 0, + "nanos": 1235833 + }, + { + "secs": 0, + "nanos": 1089083 + }, + { + "secs": 0, + "nanos": 6542 + }, + { + "secs": 0, + "nanos": 453209 + }, + { + "secs": 0, + "nanos": 1254208 + }, + { + "secs": 0, + "nanos": 6208 + }, + { + "secs": 0, + "nanos": 696834 + }, + { + "secs": 0, + "nanos": 6917 + }, + { + "secs": 0, + "nanos": 345417 + }, + { + "secs": 0, + "nanos": 5709 + }, + { + "secs": 0, + "nanos": 1914542 + }, + { + "secs": 0, + "nanos": 3387042 + }, + { + "secs": 0, + "nanos": 520083 + }, + { + "secs": 0, + "nanos": 1355292 + }, + { + "secs": 0, + "nanos": 12083 + }, + { + "secs": 0, + "nanos": 826459 + }, + { + "secs": 0, + "nanos": 7667 + }, + { + "secs": 0, + "nanos": 1044291 + }, + { + "secs": 0, + "nanos": 21042 + }, + { + "secs": 0, + "nanos": 535834 + }, + { + "secs": 0, + "nanos": 1068791 + }, + { + "secs": 0, + "nanos": 25750 + }, + { + "secs": 0, + "nanos": 929667 + }, + { + "secs": 0, + "nanos": 16584 + }, + { + "secs": 0, + "nanos": 604792 + }, + { + "secs": 0, + "nanos": 25000 + }, + { + "secs": 0, + "nanos": 35824166 + }, + { + "secs": 0, + "nanos": 580833 + }, + { + "secs": 0, + "nanos": 14458 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 3166 + }, + { + "secs": 0, + "nanos": 4542 + }, + { + "secs": 0, + "nanos": 3667 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 2958 + }, + { + "secs": 0, + "nanos": 6791 + }, + { + "secs": 0, + "nanos": 11375 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 6125 + }, + { + "secs": 0, + "nanos": 2917 + }, + { + "secs": 0, + "nanos": 5583 + }, + { + "secs": 0, + "nanos": 3041 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 3416 + }, + { + "secs": 0, + "nanos": 2792 + }, + { + "secs": 0, + "nanos": 3333 + }, + { + "secs": 0, + "nanos": 8333 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 2542 + }, + { + "secs": 0, + "nanos": 4167 + }, + { + "secs": 0, + "nanos": 2875 + }, + { + "secs": 0, + "nanos": 4084 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 3375 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 2291 + }, + { + "secs": 0, + "nanos": 3583 + }, + { + "secs": 0, + "nanos": 19375 + }, + { + "secs": 0, + "nanos": 26708 + }, + { + "secs": 0, + "nanos": 84 + } + ], + "user_prompt_length": 203, + "response_size": 653, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_5IYUis90Sq-TiqFl_6Utbw", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_5IYUis90Sq-TiqFl_6Utbw", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "106a2854-8336-4b93-a961-db6be2b52dc2", + "content": "Perfect! Now you have a `.gitignore` file in your `net-v3-server` directory that will ignore all the .NET build artifacts (`bin/`, `obj/`), IDE files, and other temporary files.\n\nReady to move to step 1 - adding the AWS dependencies?" + } + }, + "request_metadata": { + "request_id": "743dc0aa-fb89-464e-9e96-15f379a21d85", + "message_id": "106a2854-8336-4b93-a961-db6be2b52dc2", + "request_start_timestamp_ms": 1757973452600, + "stream_end_timestamp_ms": 1757973455843, + "time_to_first_chunk": { + "secs": 2, + "nanos": 25540959 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 61875 + }, + { + "secs": 0, + "nanos": 2210958 + }, + { + "secs": 0, + "nanos": 213752167 + }, + { + "secs": 0, + "nanos": 138766458 + }, + { + "secs": 0, + "nanos": 290354167 + }, + { + "secs": 0, + "nanos": 226527125 + }, + { + "secs": 0, + "nanos": 340481208 + }, + { + "secs": 0, + "nanos": 5147917 + }, + { + "secs": 0, + "nanos": 167 + }, + { + "secs": 0, + "nanos": 208 + } + ], + "user_prompt_length": 0, + "response_size": 233, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "The only more dependency needed is dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" + } + }, + "timestamp": "2025-09-15T14:58:30.987685-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "1cb698d4-7989-4d62-91fd-b1a85c1f1011", + "content": "Perfect! Let's add that dependency:", + "tool_uses": [ + { + "id": "tooluse_Y29nD5e0QPieaPa8qMg1Jw", + "name": "execute_bash", + "orig_name": "execute_bash", + "args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" + }, + "orig_args": { + "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" + } + } + ] + } + }, + "request_metadata": { + "request_id": "8dfd4ca9-b48c-4cf1-9c71-c24ecb93c467", + "message_id": "1cb698d4-7989-4d62-91fd-b1a85c1f1011", + "request_start_timestamp_ms": 1757973510993, + "stream_end_timestamp_ms": 1757973514269, + "time_to_first_chunk": { + "secs": 2, + "nanos": 48055334 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 26083 + }, + { + "secs": 0, + "nanos": 258458 + }, + { + "secs": 0, + "nanos": 209739750 + }, + { + "secs": 0, + "nanos": 387530291 + }, + { + "secs": 0, + "nanos": 612566542 + }, + { + "secs": 0, + "nanos": 17042 + }, + { + "secs": 0, + "nanos": 38000 + }, + { + "secs": 0, + "nanos": 12500 + }, + { + "secs": 0, + "nanos": 2658709 + }, + { + "secs": 0, + "nanos": 1212000 + }, + { + "secs": 0, + "nanos": 2710416 + }, + { + "secs": 0, + "nanos": 6496542 + }, + { + "secs": 0, + "nanos": 15000 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 5041 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 8750 + }, + { + "secs": 0, + "nanos": 18000 + }, + { + "secs": 0, + "nanos": 9209 + }, + { + "secs": 0, + "nanos": 13166 + }, + { + "secs": 0, + "nanos": 8542 + }, + { + "secs": 0, + "nanos": 9209 + }, + { + "secs": 0, + "nanos": 7875 + }, + { + "secs": 0, + "nanos": 6834 + }, + { + "secs": 0, + "nanos": 7000 + }, + { + "secs": 0, + "nanos": 9292 + }, + { + "secs": 0, + "nanos": 25250 + }, + { + "secs": 0, + "nanos": 4417 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 366542 + }, + { + "secs": 0, + "nanos": 1289334 + }, + { + "secs": 0, + "nanos": 12291 + }, + { + "secs": 0, + "nanos": 3083166 + }, + { + "secs": 0, + "nanos": 23917 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 259, + "response_size": 244, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_Y29nD5e0QPieaPa8qMg1Jw", + "execute_bash" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_Y29nD5e0QPieaPa8qMg1Jw", + "content": [ + { + "Json": { + "exit_status": "0", + "stdout": " Determining projects to restore...\n Writing /var/folders/p2/z7c04sc52xz2w704y0z3fkt00000gr/T/tmp9NfU5s.tmp\ninfo : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/x64/sdk/8.0.405/trustedroots/codesignctl.pem'.\ninfo : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/x64/sdk/8.0.405/trustedroots/timestampctl.pem'.\ninfo : Adding PackageReference for package 'Amazon.Extensions.S3.Encryption' into project '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : Restoring packages for /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj...\ninfo : GET https://api.nuget.org/v3/vulnerabilities/index.json\ninfo : OK https://api.nuget.org/v3/vulnerabilities/index.json 69ms\ninfo : GET https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/vulnerability.base.json\ninfo : GET https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/2025.09.15.17.20.52/vulnerability.update.json\ninfo : OK https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/vulnerability.base.json 66ms\ninfo : OK https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/2025.09.15.17.20.52/vulnerability.update.json 101ms\ninfo : Package 'Amazon.Extensions.S3.Encryption' is compatible with all the specified frameworks in project '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : PackageReference for package 'Amazon.Extensions.S3.Encryption' version '3.0.0' added to file '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : Generating MSBuild file /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.props.\ninfo : Writing assets file to disk. Path: /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.assets.json\nlog : Restored /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj (in 1.11 sec).", + "stderr": "" + } + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "5505d710-db45-49e4-aaeb-60ac3218ec90", + "content": "Great! The S3 encryption client package has been added. This package includes all the AWS SDK dependencies you need (S3, KMS, etc.).\n\nLet's verify it was added to your project file:", + "tool_uses": [ + { + "id": "tooluse_13rKR-j4Taua8MxHBUfpQg", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "e922d041-4b5e-4b91-a633-2172e7fab8de", + "message_id": "5505d710-db45-49e4-aaeb-60ac3218ec90", + "request_start_timestamp_ms": 1757973525188, + "stream_end_timestamp_ms": 1757973530625, + "time_to_first_chunk": { + "secs": 2, + "nanos": 450169208 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 334834 + }, + { + "secs": 0, + "nanos": 10875 + }, + { + "secs": 0, + "nanos": 16290500 + }, + { + "secs": 0, + "nanos": 279763541 + }, + { + "secs": 0, + "nanos": 265604708 + }, + { + "secs": 0, + "nanos": 214582791 + }, + { + "secs": 0, + "nanos": 253725042 + }, + { + "secs": 0, + "nanos": 191188208 + }, + { + "secs": 0, + "nanos": 41755208 + }, + { + "secs": 0, + "nanos": 266891417 + }, + { + "secs": 1, + "nanos": 21613417 + }, + { + "secs": 0, + "nanos": 81566667 + }, + { + "secs": 0, + "nanos": 81791458 + }, + { + "secs": 0, + "nanos": 80545833 + }, + { + "secs": 0, + "nanos": 94932542 + }, + { + "secs": 0, + "nanos": 59769583 + }, + { + "secs": 0, + "nanos": 10875 + }, + { + "secs": 0, + "nanos": 2166 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 1345833 + }, + { + "secs": 0, + "nanos": 2039333 + }, + { + "secs": 0, + "nanos": 1964458 + }, + { + "secs": 0, + "nanos": 1706083 + }, + { + "secs": 0, + "nanos": 2014541 + }, + { + "secs": 0, + "nanos": 2064166 + }, + { + "secs": 0, + "nanos": 1789250 + }, + { + "secs": 0, + "nanos": 1941542 + }, + { + "secs": 0, + "nanos": 9583 + }, + { + "secs": 0, + "nanos": 2234458 + }, + { + "secs": 0, + "nanos": 1877625 + }, + { + "secs": 0, + "nanos": 1887584 + }, + { + "secs": 0, + "nanos": 1823084 + }, + { + "secs": 0, + "nanos": 1826458 + }, + { + "secs": 0, + "nanos": 2168625 + }, + { + "secs": 0, + "nanos": 1710209 + }, + { + "secs": 0, + "nanos": 6049208 + }, + { + "secs": 0, + "nanos": 1904792 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 364, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_13rKR-j4Taua8MxHBUfpQg", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_13rKR-j4Taua8MxHBUfpQg", + "content": [ + { + "Text": "\n\n \n net8.0\n enable\n enable\n \n\n \n \n \n \n \n\n" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "dcbae8a1-b171-4bcf-8988-27ec292387a0", + "content": "Perfect! Now you're ready for step 2 - configuring the server. What's next:\n\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. **Configure server** - Set port to 8081 in appsettings.json\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service\n5. **Create controllers** - API endpoints\n\nReady to configure the server port?" + } + }, + "request_metadata": { + "request_id": "f15b962a-e840-4f4d-b404-e616c08174d0", + "message_id": "dcbae8a1-b171-4bcf-8988-27ec292387a0", + "request_start_timestamp_ms": 1757973531226, + "stream_end_timestamp_ms": 1757973535676, + "time_to_first_chunk": { + "secs": 2, + "nanos": 332914458 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 12708 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 267828333 + }, + { + "secs": 0, + "nanos": 167365541 + }, + { + "secs": 0, + "nanos": 183121125 + }, + { + "secs": 0, + "nanos": 99661041 + }, + { + "secs": 0, + "nanos": 128961792 + }, + { + "secs": 0, + "nanos": 285091458 + }, + { + "secs": 0, + "nanos": 52983583 + }, + { + "secs": 0, + "nanos": 41271959 + }, + { + "secs": 0, + "nanos": 220555208 + }, + { + "secs": 0, + "nanos": 54339875 + }, + { + "secs": 0, + "nanos": 306622167 + }, + { + "secs": 0, + "nanos": 308596708 + }, + { + "secs": 0, + "nanos": 31833 + }, + { + "secs": 0, + "nanos": 208 + }, + { + "secs": 0, + "nanos": 209 + } + ], + "user_prompt_length": 0, + "response_size": 383, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Set port to 8084" + } + }, + "timestamp": "2025-09-15T15:00:12.070259-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "36bf6342-d1b2-4b4a-af28-9b7d2a52e28b", + "content": "", + "tool_uses": [ + { + "id": "tooluse_Uc5jWCxoROSBkoI0nbLOIg", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "d7c78400-25e0-421f-aebc-a67c77a47d48", + "message_id": "36bf6342-d1b2-4b4a-af28-9b7d2a52e28b", + "request_start_timestamp_ms": 1757973612074, + "stream_end_timestamp_ms": 1757973615989, + "time_to_first_chunk": { + "secs": 2, + "nanos": 544521834 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 26750 + }, + { + "secs": 0, + "nanos": 38458 + }, + { + "secs": 0, + "nanos": 682665083 + }, + { + "secs": 0, + "nanos": 16833 + }, + { + "secs": 0, + "nanos": 9916 + }, + { + "secs": 0, + "nanos": 2090541 + }, + { + "secs": 0, + "nanos": 1854917 + }, + { + "secs": 0, + "nanos": 10084916 + }, + { + "secs": 0, + "nanos": 1404416 + }, + { + "secs": 0, + "nanos": 1065958 + }, + { + "secs": 0, + "nanos": 15833 + }, + { + "secs": 0, + "nanos": 687542 + }, + { + "secs": 0, + "nanos": 10193666 + }, + { + "secs": 0, + "nanos": 31084 + }, + { + "secs": 0, + "nanos": 7875 + }, + { + "secs": 0, + "nanos": 7708 + }, + { + "secs": 0, + "nanos": 6584 + }, + { + "secs": 0, + "nanos": 6917 + }, + { + "secs": 0, + "nanos": 7166 + }, + { + "secs": 0, + "nanos": 33574542 + }, + { + "secs": 0, + "nanos": 61160417 + }, + { + "secs": 0, + "nanos": 83368750 + }, + { + "secs": 0, + "nanos": 38556125 + }, + { + "secs": 0, + "nanos": 65242750 + }, + { + "secs": 0, + "nanos": 44979709 + }, + { + "secs": 0, + "nanos": 166798000 + }, + { + "secs": 0, + "nanos": 102669667 + }, + { + "secs": 0, + "nanos": 50529917 + }, + { + "secs": 0, + "nanos": 624875 + }, + { + "secs": 0, + "nanos": 14292 + }, + { + "secs": 0, + "nanos": 7532958 + }, + { + "secs": 0, + "nanos": 11125 + }, + { + "secs": 0, + "nanos": 3500 + }, + { + "secs": 0, + "nanos": 11333 + }, + { + "secs": 0, + "nanos": 3712833 + }, + { + "secs": 0, + "nanos": 1268042 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 174, + "response_size": 181, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_Uc5jWCxoROSBkoI0nbLOIg", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_Uc5jWCxoROSBkoI0nbLOIg", + "content": [ + { + "Text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "37c5b725-d7f2-426e-96da-682e1d947d07", + "content": "", + "tool_uses": [ + { + "id": "tooluse_Z_L9fuBNR22QrGjXn4IlxA", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", + "command": "str_replace", + "old_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}", + "new_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", + "command": "str_replace", + "old_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}", + "new_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "56194e36-a64b-4d98-bf64-90100bb808fb", + "message_id": "37c5b725-d7f2-426e-96da-682e1d947d07", + "request_start_timestamp_ms": 1757973616575, + "stream_end_timestamp_ms": 1757973622231, + "time_to_first_chunk": { + "secs": 3, + "nanos": 369449333 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 42167 + }, + { + "secs": 0, + "nanos": 1021167 + }, + { + "secs": 0, + "nanos": 863647541 + }, + { + "secs": 0, + "nanos": 881792 + }, + { + "secs": 0, + "nanos": 10250 + }, + { + "secs": 0, + "nanos": 516917 + }, + { + "secs": 0, + "nanos": 1659042 + }, + { + "secs": 0, + "nanos": 44959 + }, + { + "secs": 0, + "nanos": 1578500 + }, + { + "secs": 0, + "nanos": 816667 + }, + { + "secs": 0, + "nanos": 779584 + }, + { + "secs": 0, + "nanos": 11583 + }, + { + "secs": 0, + "nanos": 1064834 + }, + { + "secs": 0, + "nanos": 1126709 + }, + { + "secs": 0, + "nanos": 8625 + }, + { + "secs": 0, + "nanos": 2074667 + }, + { + "secs": 0, + "nanos": 12583 + }, + { + "secs": 0, + "nanos": 190583 + }, + { + "secs": 0, + "nanos": 7500 + }, + { + "secs": 0, + "nanos": 1795417 + }, + { + "secs": 0, + "nanos": 9792 + }, + { + "secs": 0, + "nanos": 536584 + }, + { + "secs": 0, + "nanos": 1020334 + }, + { + "secs": 0, + "nanos": 9500 + }, + { + "secs": 0, + "nanos": 688833 + }, + { + "secs": 0, + "nanos": 323775208 + }, + { + "secs": 0, + "nanos": 27833 + }, + { + "secs": 0, + "nanos": 85708 + }, + { + "secs": 0, + "nanos": 8000 + }, + { + "secs": 0, + "nanos": 464750 + }, + { + "secs": 0, + "nanos": 1053833 + }, + { + "secs": 0, + "nanos": 6500 + }, + { + "secs": 0, + "nanos": 332643125 + }, + { + "secs": 0, + "nanos": 86125 + }, + { + "secs": 0, + "nanos": 7667 + }, + { + "secs": 0, + "nanos": 40542 + }, + { + "secs": 0, + "nanos": 4174583 + }, + { + "secs": 0, + "nanos": 19375 + }, + { + "secs": 0, + "nanos": 51083 + }, + { + "secs": 0, + "nanos": 7041 + }, + { + "secs": 0, + "nanos": 30791 + }, + { + "secs": 0, + "nanos": 9042 + }, + { + "secs": 0, + "nanos": 44416 + }, + { + "secs": 0, + "nanos": 1759125 + }, + { + "secs": 0, + "nanos": 18125 + }, + { + "secs": 0, + "nanos": 713042 + }, + { + "secs": 0, + "nanos": 7792 + }, + { + "secs": 0, + "nanos": 1353167 + }, + { + "secs": 0, + "nanos": 838583 + }, + { + "secs": 0, + "nanos": 8042 + }, + { + "secs": 0, + "nanos": 1055667 + }, + { + "secs": 0, + "nanos": 1053917 + }, + { + "secs": 0, + "nanos": 7209 + }, + { + "secs": 0, + "nanos": 1871083 + }, + { + "secs": 0, + "nanos": 1122875 + }, + { + "secs": 0, + "nanos": 5583 + }, + { + "secs": 0, + "nanos": 3333 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 680216292 + }, + { + "secs": 0, + "nanos": 32953750 + }, + { + "secs": 0, + "nanos": 14375 + }, + { + "secs": 0, + "nanos": 3083 + }, + { + "secs": 0, + "nanos": 3042 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 0, + "nanos": 3291 + }, + { + "secs": 0, + "nanos": 1656209 + }, + { + "secs": 0, + "nanos": 12584 + }, + { + "secs": 0, + "nanos": 552583 + }, + { + "secs": 0, + "nanos": 16875 + }, + { + "secs": 0, + "nanos": 1180916 + }, + { + "secs": 0, + "nanos": 857750 + }, + { + "secs": 0, + "nanos": 9125 + }, + { + "secs": 0, + "nanos": 854042 + }, + { + "secs": 0, + "nanos": 938167 + }, + { + "secs": 0, + "nanos": 1583500 + }, + { + "secs": 0, + "nanos": 10459 + }, + { + "secs": 0, + "nanos": 1302000 + }, + { + "secs": 0, + "nanos": 9000 + }, + { + "secs": 0, + "nanos": 1479542 + }, + { + "secs": 0, + "nanos": 9958 + }, + { + "secs": 0, + "nanos": 1578041 + }, + { + "secs": 0, + "nanos": 1361042 + }, + { + "secs": 0, + "nanos": 6875 + }, + { + "secs": 0, + "nanos": 3209 + }, + { + "secs": 0, + "nanos": 885417 + }, + { + "secs": 0, + "nanos": 8000 + }, + { + "secs": 0, + "nanos": 457416 + }, + { + "secs": 0, + "nanos": 2223250 + }, + { + "secs": 0, + "nanos": 1959500 + }, + { + "secs": 0, + "nanos": 1970334 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 0, + "response_size": 576, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_Z_L9fuBNR22QrGjXn4IlxA", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_Z_L9fuBNR22QrGjXn4IlxA", + "content": [ + { + "Text": "An error occurred processing the tool: \nno occurrences of \"{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}\" were found" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "65f83228-a308-4c4e-81a4-71c0e5178141", + "content": "", + "tool_uses": [ + { + "id": "tooluse_SBot2eV7QUGNNt2zLnPgHQ", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", + "command": "create", + "file_text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", + "command": "create", + "file_text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "dc31763f-2f38-4ac0-98c6-a1a9fad638cb", + "message_id": "65f83228-a308-4c4e-81a4-71c0e5178141", + "request_start_timestamp_ms": 1757973627124, + "stream_end_timestamp_ms": 1757973631858, + "time_to_first_chunk": { + "secs": 2, + "nanos": 344258709 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 22583 + }, + { + "secs": 0, + "nanos": 18958833 + }, + { + "secs": 0, + "nanos": 956386291 + }, + { + "secs": 0, + "nanos": 8875 + }, + { + "secs": 0, + "nanos": 1550250 + }, + { + "secs": 0, + "nanos": 7542 + }, + { + "secs": 0, + "nanos": 2041250 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 787833 + }, + { + "secs": 0, + "nanos": 4416 + }, + { + "secs": 0, + "nanos": 961417 + }, + { + "secs": 0, + "nanos": 4334 + }, + { + "secs": 0, + "nanos": 863209 + }, + { + "secs": 0, + "nanos": 1249208 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 816792 + }, + { + "secs": 0, + "nanos": 4669583 + }, + { + "secs": 0, + "nanos": 16500 + }, + { + "secs": 0, + "nanos": 6708 + }, + { + "secs": 0, + "nanos": 6000 + }, + { + "secs": 0, + "nanos": 5834 + }, + { + "secs": 0, + "nanos": 13250 + }, + { + "secs": 0, + "nanos": 227834 + }, + { + "secs": 0, + "nanos": 11250 + }, + { + "secs": 0, + "nanos": 575333 + }, + { + "secs": 0, + "nanos": 11125 + }, + { + "secs": 0, + "nanos": 2256166 + }, + { + "secs": 0, + "nanos": 263016833 + }, + { + "secs": 0, + "nanos": 19458 + }, + { + "secs": 0, + "nanos": 677959 + }, + { + "secs": 0, + "nanos": 517388375 + }, + { + "secs": 0, + "nanos": 44086333 + }, + { + "secs": 0, + "nanos": 39403916 + }, + { + "secs": 0, + "nanos": 20235583 + }, + { + "secs": 0, + "nanos": 94834334 + }, + { + "secs": 0, + "nanos": 75191375 + }, + { + "secs": 0, + "nanos": 46924417 + }, + { + "secs": 0, + "nanos": 15806417 + }, + { + "secs": 0, + "nanos": 88065250 + }, + { + "secs": 0, + "nanos": 68795583 + }, + { + "secs": 0, + "nanos": 39125792 + }, + { + "secs": 0, + "nanos": 42616250 + }, + { + "secs": 0, + "nanos": 1181833 + }, + { + "secs": 0, + "nanos": 1906583 + }, + { + "secs": 0, + "nanos": 14074875 + }, + { + "secs": 0, + "nanos": 13291 + }, + { + "secs": 0, + "nanos": 9666750 + }, + { + "secs": 0, + "nanos": 36792 + }, + { + "secs": 0, + "nanos": 98458 + }, + { + "secs": 0, + "nanos": 10375 + }, + { + "secs": 0, + "nanos": 3679958 + }, + { + "secs": 0, + "nanos": 20417 + }, + { + "secs": 0, + "nanos": 51208 + }, + { + "secs": 0, + "nanos": 1125417 + }, + { + "secs": 0, + "nanos": 22292 + }, + { + "secs": 0, + "nanos": 3321292 + }, + { + "secs": 0, + "nanos": 20750 + }, + { + "secs": 0, + "nanos": 173750 + }, + { + "secs": 0, + "nanos": 22208 + }, + { + "secs": 0, + "nanos": 85542 + }, + { + "secs": 0, + "nanos": 3250 + }, + { + "secs": 0, + "nanos": 673500 + }, + { + "secs": 0, + "nanos": 12000 + }, + { + "secs": 0, + "nanos": 2507166 + }, + { + "secs": 0, + "nanos": 1706792 + }, + { + "secs": 0, + "nanos": 1697458 + }, + { + "secs": 0, + "nanos": 84 + } + ], + "user_prompt_length": 0, + "response_size": 393, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_SBot2eV7QUGNNt2zLnPgHQ", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_SBot2eV7QUGNNt2zLnPgHQ", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "e4e85924-7c9e-4de0-9d55-f7e3070697d1", + "content": "Perfect! Server is now configured to run on port 8084. \n\nNext steps:\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. ✅ **Server configured** - Port set to 8084\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service \n5. **Create controllers** - API endpoints\n\nReady to create the models next?" + } + }, + "request_metadata": { + "request_id": "1534a9a1-c1a1-4852-9abd-342bf0c7787b", + "message_id": "e4e85924-7c9e-4de0-9d55-f7e3070697d1", + "request_start_timestamp_ms": 1757973645230, + "stream_end_timestamp_ms": 1757973648725, + "time_to_first_chunk": { + "secs": 2, + "nanos": 105322500 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 14000 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 210039542 + }, + { + "secs": 0, + "nanos": 166278917 + }, + { + "secs": 0, + "nanos": 117245542 + }, + { + "secs": 0, + "nanos": 105178625 + }, + { + "secs": 0, + "nanos": 145004500 + }, + { + "secs": 0, + "nanos": 101339334 + }, + { + "secs": 0, + "nanos": 61737541 + }, + { + "secs": 0, + "nanos": 53424959 + }, + { + "secs": 0, + "nanos": 109557666 + }, + { + "secs": 0, + "nanos": 320054000 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 9791 + }, + { + "secs": 0, + "nanos": 42 + }, + { + "secs": 0, + "nanos": 41 + } + ], + "user_prompt_length": 0, + "response_size": 359, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Yes. What should I be doing for this?" + } + }, + "timestamp": "2025-09-15T15:01:15.741909-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "07d6fd25-3523-428e-a0be-8238ec9513ea", + "content": "Based on the Python server, you need these models to handle the JSON requests/responses:", + "tool_uses": [ + { + "id": "tooluse_3waQ7nstRBivlgdqPcvaUg", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public KeyMaterial KeyMaterial { get; set; } = new();\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public KeyMaterial KeyMaterial { get; set; } = new();\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "f7a38c34-3068-4404-87c1-b48fee119cd5", + "message_id": "07d6fd25-3523-428e-a0be-8238ec9513ea", + "request_start_timestamp_ms": 1757973675745, + "stream_end_timestamp_ms": 1757973684081, + "time_to_first_chunk": { + "secs": 3, + "nanos": 570548667 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 22292 + }, + { + "secs": 0, + "nanos": 7689708 + }, + { + "secs": 0, + "nanos": 682247333 + }, + { + "secs": 0, + "nanos": 274894958 + }, + { + "secs": 0, + "nanos": 204481250 + }, + { + "secs": 0, + "nanos": 676661500 + }, + { + "secs": 0, + "nanos": 21973208 + }, + { + "secs": 0, + "nanos": 2936459 + }, + { + "secs": 0, + "nanos": 5853334 + }, + { + "secs": 0, + "nanos": 18633417 + }, + { + "secs": 0, + "nanos": 5016292 + }, + { + "secs": 0, + "nanos": 24833 + }, + { + "secs": 0, + "nanos": 1917084 + }, + { + "secs": 0, + "nanos": 16375 + }, + { + "secs": 0, + "nanos": 6000 + }, + { + "secs": 0, + "nanos": 2768000 + }, + { + "secs": 0, + "nanos": 15208 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 4666 + }, + { + "secs": 0, + "nanos": 1841834 + }, + { + "secs": 0, + "nanos": 12916 + }, + { + "secs": 0, + "nanos": 1268833 + }, + { + "secs": 0, + "nanos": 11166 + }, + { + "secs": 0, + "nanos": 5583 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 9084 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 160124125 + }, + { + "secs": 0, + "nanos": 307333 + }, + { + "secs": 0, + "nanos": 11042 + }, + { + "secs": 0, + "nanos": 1006750 + }, + { + "secs": 2, + "nanos": 482528000 + }, + { + "secs": 0, + "nanos": 35871083 + }, + { + "secs": 0, + "nanos": 35193458 + }, + { + "secs": 0, + "nanos": 46335000 + }, + { + "secs": 0, + "nanos": 16142083 + }, + { + "secs": 0, + "nanos": 16750 + }, + { + "secs": 0, + "nanos": 6125 + }, + { + "secs": 0, + "nanos": 5583 + }, + { + "secs": 0, + "nanos": 5333 + }, + { + "secs": 0, + "nanos": 4834 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 5592250 + }, + { + "secs": 0, + "nanos": 8958 + }, + { + "secs": 0, + "nanos": 2708 + }, + { + "secs": 0, + "nanos": 4480291 + }, + { + "secs": 0, + "nanos": 6333 + }, + { + "secs": 0, + "nanos": 73958 + }, + { + "secs": 0, + "nanos": 3917 + }, + { + "secs": 0, + "nanos": 468750 + }, + { + "secs": 0, + "nanos": 4458 + }, + { + "secs": 0, + "nanos": 2417 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 563083 + }, + { + "secs": 0, + "nanos": 5833 + }, + { + "secs": 0, + "nanos": 1834000 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 2667 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 11656417 + }, + { + "secs": 0, + "nanos": 15611084 + }, + { + "secs": 0, + "nanos": 5651459 + }, + { + "secs": 0, + "nanos": 17167 + }, + { + "secs": 0, + "nanos": 5334 + }, + { + "secs": 0, + "nanos": 7333 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 5958 + }, + { + "secs": 0, + "nanos": 5667 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 1353583 + }, + { + "secs": 0, + "nanos": 9875 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 721459 + }, + { + "secs": 0, + "nanos": 5083 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 6478834 + }, + { + "secs": 0, + "nanos": 3645084 + }, + { + "secs": 0, + "nanos": 17000 + }, + { + "secs": 0, + "nanos": 73791 + }, + { + "secs": 0, + "nanos": 13125 + }, + { + "secs": 0, + "nanos": 1365083 + }, + { + "secs": 0, + "nanos": 9859625 + }, + { + "secs": 0, + "nanos": 25959 + }, + { + "secs": 0, + "nanos": 13042 + }, + { + "secs": 0, + "nanos": 9665542 + }, + { + "secs": 0, + "nanos": 208 + } + ], + "user_prompt_length": 195, + "response_size": 651, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_3waQ7nstRBivlgdqPcvaUg", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "CancelledToolUses": { + "prompt": "Can you explain what this is?", + "tool_use_results": [ + { + "tool_use_id": "tooluse_3waQ7nstRBivlgdqPcvaUg", + "content": [ + { + "Text": "Tool use was cancelled by the user" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": "2025-09-15T15:49:08.683777-07:00", + "images": null + }, + "assistant": { + "Response": { + "message_id": "abd343e6-7583-47d6-a767-14d68f205305", + "content": "These models represent the JSON data structures that your .NET server will receive and send, matching what the Python server handles.\n\n## What These Models Do:\n\n**ClientRequest** - Represents the JSON sent to `POST /client`:\n```json\n{\n \"config\": {\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012\"\n },\n \"enableLegacyWrappingAlgorithms\": false\n }\n}\n```\n\n## Why You Need Them:\n\nIn the Python server, this JSON gets parsed manually:\n```python\nparsed_data = json.loads(body.decode(\"utf-8\"))\nconfig_data = parsed_data.get(\"config\", {})\nkey_material = config_data.get(\"keyMaterial\", {})\nkms_key_id = key_material[\"kmsKeyId\"]\n```\n\nIn .NET, these classes automatically convert JSON to strongly-typed objects:\n```csharp\n// .NET automatically converts JSON to ClientRequest object\npublic async Task CreateClient([FromBody] ClientRequest request)\n{\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n // ...\n}\n```\n\n## The Pattern:\n- **ClientRequest** = What comes IN to your API\n- **ClientResponse** = What goes OUT from your API \n- **ErrorModels** = Error responses\n\nThis gives you type safety and automatic JSON serialization/deserialization instead of manual parsing." + } + }, + "request_metadata": { + "request_id": "783d1135-80be-4bd4-824a-96ea4bc95dc6", + "message_id": "abd343e6-7583-47d6-a767-14d68f205305", + "request_start_timestamp_ms": 1757976548926, + "stream_end_timestamp_ms": 1757976560036, + "time_to_first_chunk": { + "secs": 4, + "nanos": 631729959 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 18167 + }, + { + "secs": 0, + "nanos": 13768375 + }, + { + "secs": 0, + "nanos": 205140500 + }, + { + "secs": 0, + "nanos": 19291500 + }, + { + "secs": 0, + "nanos": 60673959 + }, + { + "secs": 0, + "nanos": 174323250 + }, + { + "secs": 0, + "nanos": 225767375 + }, + { + "secs": 0, + "nanos": 164963583 + }, + { + "secs": 0, + "nanos": 112904917 + }, + { + "secs": 0, + "nanos": 230161125 + }, + { + "secs": 0, + "nanos": 221409750 + }, + { + "secs": 0, + "nanos": 156795167 + }, + { + "secs": 0, + "nanos": 5625 + }, + { + "secs": 0, + "nanos": 1808167 + }, + { + "secs": 0, + "nanos": 11913917 + }, + { + "secs": 0, + "nanos": 56945750 + }, + { + "secs": 0, + "nanos": 54508584 + }, + { + "secs": 0, + "nanos": 56529541 + }, + { + "secs": 0, + "nanos": 113496209 + }, + { + "secs": 0, + "nanos": 90189334 + }, + { + "secs": 0, + "nanos": 80009250 + }, + { + "secs": 0, + "nanos": 117152083 + }, + { + "secs": 0, + "nanos": 169466833 + }, + { + "secs": 0, + "nanos": 501664166 + }, + { + "secs": 0, + "nanos": 117451833 + }, + { + "secs": 0, + "nanos": 1985000 + }, + { + "secs": 0, + "nanos": 3522542 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 2177917 + }, + { + "secs": 0, + "nanos": 1923375 + }, + { + "secs": 0, + "nanos": 1820417 + }, + { + "secs": 0, + "nanos": 47091834 + }, + { + "secs": 0, + "nanos": 56952083 + }, + { + "secs": 0, + "nanos": 62854291 + }, + { + "secs": 0, + "nanos": 63335791 + }, + { + "secs": 0, + "nanos": 51310041 + }, + { + "secs": 0, + "nanos": 54891292 + }, + { + "secs": 0, + "nanos": 52631208 + }, + { + "secs": 0, + "nanos": 21417 + }, + { + "secs": 0, + "nanos": 5298083 + }, + { + "secs": 0, + "nanos": 19917 + }, + { + "secs": 0, + "nanos": 17500 + }, + { + "secs": 0, + "nanos": 464539417 + }, + { + "secs": 0, + "nanos": 1854417 + }, + { + "secs": 0, + "nanos": 3760417 + }, + { + "secs": 0, + "nanos": 4751625 + }, + { + "secs": 0, + "nanos": 1354625 + }, + { + "secs": 0, + "nanos": 1746625 + }, + { + "secs": 0, + "nanos": 1237416 + }, + { + "secs": 0, + "nanos": 9416 + }, + { + "secs": 0, + "nanos": 19292 + }, + { + "secs": 0, + "nanos": 32791 + }, + { + "secs": 0, + "nanos": 66283875 + }, + { + "secs": 0, + "nanos": 42672375 + }, + { + "secs": 0, + "nanos": 17750 + }, + { + "secs": 0, + "nanos": 18583 + }, + { + "secs": 0, + "nanos": 17416 + }, + { + "secs": 0, + "nanos": 23750 + }, + { + "secs": 0, + "nanos": 22333 + }, + { + "secs": 0, + "nanos": 65417 + }, + { + "secs": 0, + "nanos": 35332417 + }, + { + "secs": 0, + "nanos": 6750 + }, + { + "secs": 0, + "nanos": 19750 + }, + { + "secs": 0, + "nanos": 31453708 + }, + { + "secs": 0, + "nanos": 250563750 + }, + { + "secs": 0, + "nanos": 5791 + }, + { + "secs": 0, + "nanos": 301705167 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 4992666 + }, + { + "secs": 0, + "nanos": 83 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 187, + "response_size": 1258, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "These are example of request/responses. CreateClient:\n\n[DEBUG] HTTP Method: POST\n[DEBUG] Endpoint Path: /client\n[DEBUG] Request Headers: {'content-length': '234', 'host': 'localhost:8081', 'content-type': 'application/json', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: b'{\"config\":{\"enableLegacyUnauthenticatedModes\":false,\"enableDelayedAuthenticationMode\":false,\"enableLegacyWrappingAlgorithms\":false,\"keyMaterial\":{\"kmsKeyId\":\"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"}}}'\n\nGetObject: \n\n[DEBUG] HTTP Method: GET\n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python\n[DEBUG] Request Headers: {'content-length': '0', 'host': 'localhost:8081', 'clientid': 'ff529adc-bc33-4db9-9309-9086d09f62d4', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: (no body for GET request)\nINFO: 127.0.0.1:52340 - \"GET /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python HTTP/1.1\" 200 OK\n\nMore generally, the endpoint path is /object/$BUCKET/$KEY and there is no HTTP body. The client-id is in the request headers. \nPutObject:\n\n[DEBUG] HTTP Method: PUT\n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python\n[DEBUG] Request Headers: {'content-length': '17', 'host': 'localhost:8081', 'clientid': '4c10da8d-4b2e-4a91-a0bb-5ba2c658ebe4', 'content-metadata': '[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]', 'content-type': 'application/octet-stream', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: b'simple-test-input'\nINFO: 127.0.0.1:52330 - \"PUT /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python HTTP/1.1\" 200 OK\n\n" + } + }, + "timestamp": "2025-09-15T15:51:50.760504-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "da219f02-3fb0-4944-9ebc-c947b55e1fc8", + "content": "Perfect! Now I can see the exact request/response patterns. Let me create the correct models:", + "tool_uses": [ + { + "id": "tooluse_WfvGXUKOQSuoxkos4pb-1A", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public bool EnableLegacyUnauthenticatedModes { get; set; }\n public bool EnableDelayedAuthenticationMode { get; set; }\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n public KeyMaterial KeyMaterial { get; set; } = new();\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public bool EnableLegacyUnauthenticatedModes { get; set; }\n public bool EnableDelayedAuthenticationMode { get; set; }\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n public KeyMaterial KeyMaterial { get; set; } = new();\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "723b6def-bc08-4533-b9a5-1402064be574", + "message_id": "da219f02-3fb0-4944-9ebc-c947b55e1fc8", + "request_start_timestamp_ms": 1757976710766, + "stream_end_timestamp_ms": 1757976720595, + "time_to_first_chunk": { + "secs": 6, + "nanos": 77480291 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 11500 + }, + { + "secs": 0, + "nanos": 10556792 + }, + { + "secs": 0, + "nanos": 40145792 + }, + { + "secs": 0, + "nanos": 178052542 + }, + { + "secs": 0, + "nanos": 19365000 + }, + { + "secs": 0, + "nanos": 53056000 + }, + { + "secs": 0, + "nanos": 161449916 + }, + { + "secs": 0, + "nanos": 567052542 + }, + { + "secs": 0, + "nanos": 11833 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 2458 + }, + { + "secs": 0, + "nanos": 71458 + }, + { + "secs": 0, + "nanos": 6417 + }, + { + "secs": 0, + "nanos": 1247166 + }, + { + "secs": 0, + "nanos": 1912625 + }, + { + "secs": 0, + "nanos": 5541 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 62901125 + }, + { + "secs": 0, + "nanos": 7083 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 35042 + }, + { + "secs": 0, + "nanos": 6375 + }, + { + "secs": 0, + "nanos": 3250 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 117248500 + }, + { + "secs": 0, + "nanos": 3234750 + }, + { + "secs": 0, + "nanos": 15751625 + }, + { + "secs": 0, + "nanos": 6041 + }, + { + "secs": 1, + "nanos": 992288000 + }, + { + "secs": 0, + "nanos": 55097291 + }, + { + "secs": 0, + "nanos": 60212208 + }, + { + "secs": 0, + "nanos": 175849958 + }, + { + "secs": 0, + "nanos": 108202667 + }, + { + "secs": 0, + "nanos": 31066916 + }, + { + "secs": 0, + "nanos": 1648083 + }, + { + "secs": 0, + "nanos": 7255458 + }, + { + "secs": 0, + "nanos": 36541 + }, + { + "secs": 0, + "nanos": 5432375 + }, + { + "secs": 0, + "nanos": 52584 + }, + { + "secs": 0, + "nanos": 14084 + }, + { + "secs": 0, + "nanos": 1413916 + }, + { + "secs": 0, + "nanos": 1103333 + }, + { + "secs": 0, + "nanos": 35792 + }, + { + "secs": 0, + "nanos": 3362375 + }, + { + "secs": 0, + "nanos": 14000 + }, + { + "secs": 0, + "nanos": 3784875 + }, + { + "secs": 0, + "nanos": 13250 + }, + { + "secs": 0, + "nanos": 176958 + }, + { + "secs": 0, + "nanos": 2216542 + }, + { + "secs": 0, + "nanos": 1746833 + }, + { + "secs": 0, + "nanos": 10375 + }, + { + "secs": 0, + "nanos": 1952167 + }, + { + "secs": 0, + "nanos": 1936750 + }, + { + "secs": 0, + "nanos": 9292 + }, + { + "secs": 0, + "nanos": 2918208 + }, + { + "secs": 0, + "nanos": 11459 + }, + { + "secs": 0, + "nanos": 5875 + }, + { + "secs": 0, + "nanos": 30850375 + }, + { + "secs": 0, + "nanos": 18042 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 4333 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 4291 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 34667 + }, + { + "secs": 0, + "nanos": 830500 + }, + { + "secs": 0, + "nanos": 10250 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 4958 + }, + { + "secs": 0, + "nanos": 6250 + }, + { + "secs": 0, + "nanos": 358666 + }, + { + "secs": 0, + "nanos": 8064125 + }, + { + "secs": 0, + "nanos": 20584 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 4708 + }, + { + "secs": 0, + "nanos": 3017833 + }, + { + "secs": 0, + "nanos": 906667 + }, + { + "secs": 0, + "nanos": 1332833 + }, + { + "secs": 0, + "nanos": 5416 + }, + { + "secs": 0, + "nanos": 1989167 + }, + { + "secs": 0, + "nanos": 2336834 + }, + { + "secs": 0, + "nanos": 1673667 + }, + { + "secs": 0, + "nanos": 17792 + }, + { + "secs": 0, + "nanos": 1229084 + }, + { + "secs": 0, + "nanos": 20375 + }, + { + "secs": 0, + "nanos": 815375 + }, + { + "secs": 0, + "nanos": 1968750 + }, + { + "secs": 0, + "nanos": 17625 + }, + { + "secs": 0, + "nanos": 289209 + }, + { + "secs": 0, + "nanos": 16541 + }, + { + "secs": 0, + "nanos": 604708 + }, + { + "secs": 0, + "nanos": 14000 + }, + { + "secs": 0, + "nanos": 5105625 + }, + { + "secs": 0, + "nanos": 22500 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 1968, + "response_size": 783, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_WfvGXUKOQSuoxkos4pb-1A", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_WfvGXUKOQSuoxkos4pb-1A", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "5eabd27b-b6cd-405e-a835-67a2e34ebc90", + "content": "", + "tool_uses": [ + { + "id": "tooluse_O4A_46LJSpKApGrCiiWhLQ", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientResponse\n{\n public string ClientId { get; set; } = string.Empty;\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class ClientResponse\n{\n public string ClientId { get; set; } = string.Empty;\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "efe53e58-861f-4378-abcc-e8a0191efc8e", + "message_id": "5eabd27b-b6cd-405e-a835-67a2e34ebc90", + "request_start_timestamp_ms": 1757976751603, + "stream_end_timestamp_ms": 1757976755466, + "time_to_first_chunk": { + "secs": 2, + "nanos": 550903000 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 21959 + }, + { + "secs": 0, + "nanos": 8042 + }, + { + "secs": 0, + "nanos": 786241459 + }, + { + "secs": 0, + "nanos": 35768041 + }, + { + "secs": 0, + "nanos": 44780792 + }, + { + "secs": 0, + "nanos": 68475250 + }, + { + "secs": 0, + "nanos": 40930458 + }, + { + "secs": 0, + "nanos": 18250208 + }, + { + "secs": 0, + "nanos": 57608583 + }, + { + "secs": 0, + "nanos": 34264750 + }, + { + "secs": 0, + "nanos": 42643291 + }, + { + "secs": 0, + "nanos": 59271958 + }, + { + "secs": 0, + "nanos": 24799125 + }, + { + "secs": 0, + "nanos": 62183000 + }, + { + "secs": 0, + "nanos": 5177708 + }, + { + "secs": 0, + "nanos": 27959 + }, + { + "secs": 0, + "nanos": 282042 + }, + { + "secs": 0, + "nanos": 882958 + }, + { + "secs": 0, + "nanos": 7750 + }, + { + "secs": 0, + "nanos": 2416167 + }, + { + "secs": 0, + "nanos": 11583 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 590000 + }, + { + "secs": 0, + "nanos": 329083 + }, + { + "secs": 0, + "nanos": 8708 + }, + { + "secs": 0, + "nanos": 1308917 + }, + { + "secs": 0, + "nanos": 7667 + }, + { + "secs": 0, + "nanos": 1381083 + }, + { + "secs": 0, + "nanos": 7542 + }, + { + "secs": 0, + "nanos": 1163291 + }, + { + "secs": 0, + "nanos": 310000 + }, + { + "secs": 0, + "nanos": 9125 + }, + { + "secs": 0, + "nanos": 1714833 + }, + { + "secs": 0, + "nanos": 7542 + }, + { + "secs": 0, + "nanos": 1704417 + }, + { + "secs": 0, + "nanos": 21833 + }, + { + "secs": 0, + "nanos": 7125 + }, + { + "secs": 0, + "nanos": 994000 + }, + { + "secs": 0, + "nanos": 34208 + }, + { + "secs": 0, + "nanos": 1123167 + }, + { + "secs": 0, + "nanos": 24375 + }, + { + "secs": 0, + "nanos": 31667 + }, + { + "secs": 0, + "nanos": 971750 + }, + { + "secs": 0, + "nanos": 13167 + }, + { + "secs": 0, + "nanos": 1289958 + }, + { + "secs": 0, + "nanos": 26583 + }, + { + "secs": 0, + "nanos": 3173834 + }, + { + "secs": 0, + "nanos": 34833 + }, + { + "secs": 0, + "nanos": 11792 + }, + { + "secs": 0, + "nanos": 15334 + }, + { + "secs": 0, + "nanos": 98917 + }, + { + "secs": 0, + "nanos": 1535417 + }, + { + "secs": 0, + "nanos": 11916 + }, + { + "secs": 0, + "nanos": 82166 + }, + { + "secs": 0, + "nanos": 2987250 + }, + { + "secs": 0, + "nanos": 6174625 + }, + { + "secs": 0, + "nanos": 42167 + }, + { + "secs": 0, + "nanos": 125 + } + ], + "user_prompt_length": 0, + "response_size": 320, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_O4A_46LJSpKApGrCiiWhLQ", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_O4A_46LJSpKApGrCiiWhLQ", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "652f81fd-c3a7-4097-88b5-dfa088c91d84", + "content": "", + "tool_uses": [ + { + "id": "tooluse_Pir3pwnFSjiffDJ92UUStw", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class GenericServerError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#GenericServerError\";\n public string Message { get; set; } = string.Empty;\n}\n\npublic class S3EncryptionClientError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#S3EncryptionClientError\";\n public string Message { get; set; } = string.Empty;\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs", + "command": "create", + "file_text": "namespace NetV3Server.Models;\n\npublic class GenericServerError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#GenericServerError\";\n public string Message { get; set; } = string.Empty;\n}\n\npublic class S3EncryptionClientError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#S3EncryptionClientError\";\n public string Message { get; set; } = string.Empty;\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "65082f4e-c0cd-4abe-b912-26eb1978aaf6", + "message_id": "652f81fd-c3a7-4097-88b5-dfa088c91d84", + "request_start_timestamp_ms": 1757976771866, + "stream_end_timestamp_ms": 1757976777269, + "time_to_first_chunk": { + "secs": 2, + "nanos": 486623542 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 11125 + }, + { + "secs": 0, + "nanos": 4042 + }, + { + "secs": 0, + "nanos": 614651667 + }, + { + "secs": 0, + "nanos": 100458 + }, + { + "secs": 0, + "nanos": 3333 + }, + { + "secs": 0, + "nanos": 4208 + }, + { + "secs": 0, + "nanos": 1914167 + }, + { + "secs": 0, + "nanos": 4000 + }, + { + "secs": 0, + "nanos": 853583 + }, + { + "secs": 0, + "nanos": 5917 + }, + { + "secs": 0, + "nanos": 2467083 + }, + { + "secs": 0, + "nanos": 3791 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1666 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 3090459 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 1790000 + }, + { + "secs": 0, + "nanos": 6565375 + }, + { + "secs": 0, + "nanos": 6600125 + }, + { + "secs": 0, + "nanos": 187495667 + }, + { + "secs": 0, + "nanos": 28205250 + }, + { + "secs": 0, + "nanos": 42105417 + }, + { + "secs": 0, + "nanos": 8386083 + }, + { + "secs": 1, + "nanos": 541204959 + }, + { + "secs": 0, + "nanos": 70784083 + }, + { + "secs": 0, + "nanos": 30713959 + }, + { + "secs": 0, + "nanos": 37000125 + }, + { + "secs": 0, + "nanos": 27816959 + }, + { + "secs": 0, + "nanos": 5289500 + }, + { + "secs": 0, + "nanos": 13708 + }, + { + "secs": 0, + "nanos": 6691417 + }, + { + "secs": 0, + "nanos": 33778000 + }, + { + "secs": 0, + "nanos": 14384042 + }, + { + "secs": 0, + "nanos": 9746250 + }, + { + "secs": 0, + "nanos": 22792 + }, + { + "secs": 0, + "nanos": 13515792 + }, + { + "secs": 0, + "nanos": 19961500 + }, + { + "secs": 0, + "nanos": 48461875 + }, + { + "secs": 0, + "nanos": 52899750 + }, + { + "secs": 0, + "nanos": 19583 + }, + { + "secs": 0, + "nanos": 5166 + }, + { + "secs": 0, + "nanos": 3750 + }, + { + "secs": 0, + "nanos": 27246334 + }, + { + "secs": 0, + "nanos": 15871334 + }, + { + "secs": 0, + "nanos": 23458 + }, + { + "secs": 0, + "nanos": 84084 + }, + { + "secs": 0, + "nanos": 11296250 + }, + { + "secs": 0, + "nanos": 5768084 + }, + { + "secs": 0, + "nanos": 11918750 + }, + { + "secs": 0, + "nanos": 16390000 + }, + { + "secs": 0, + "nanos": 16708 + }, + { + "secs": 0, + "nanos": 73125 + }, + { + "secs": 0, + "nanos": 11416 + }, + { + "secs": 0, + "nanos": 642125 + }, + { + "secs": 0, + "nanos": 1674791 + }, + { + "secs": 0, + "nanos": 18833 + }, + { + "secs": 0, + "nanos": 331167 + }, + { + "secs": 0, + "nanos": 13875 + }, + { + "secs": 0, + "nanos": 513583 + }, + { + "secs": 0, + "nanos": 10125 + }, + { + "secs": 0, + "nanos": 1028167 + }, + { + "secs": 0, + "nanos": 14375 + }, + { + "secs": 0, + "nanos": 224583 + }, + { + "secs": 0, + "nanos": 2216709 + }, + { + "secs": 0, + "nanos": 14042 + }, + { + "secs": 0, + "nanos": 18083 + }, + { + "secs": 0, + "nanos": 4166 + }, + { + "secs": 0, + "nanos": 6459 + }, + { + "secs": 0, + "nanos": 5416 + }, + { + "secs": 0, + "nanos": 4959 + }, + { + "secs": 0, + "nanos": 5917 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 700292 + }, + { + "secs": 0, + "nanos": 4666 + }, + { + "secs": 0, + "nanos": 10875 + }, + { + "secs": 0, + "nanos": 3250 + }, + { + "secs": 0, + "nanos": 915083 + }, + { + "secs": 0, + "nanos": 11000 + }, + { + "secs": 0, + "nanos": 2342334 + }, + { + "secs": 0, + "nanos": 167 + } + ], + "user_prompt_length": 0, + "response_size": 620, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_Pir3pwnFSjiffDJ92UUStw", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_Pir3pwnFSjiffDJ92UUStw", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "c2e0c94e-d11a-4468-b9f8-05b04a913ef3", + "content": "Now you have the exact models matching your request/response patterns:\n\n## What Each Model Handles:\n\n**ClientRequest** - Handles the POST /client JSON:\n```json\n{\n \"config\": {\n \"enableLegacyUnauthenticatedModes\": false,\n \"enableDelayedAuthenticationMode\": false, \n \"enableLegacyWrappingAlgorithms\": false,\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"\n }\n }\n}\n```\n\n**ClientResponse** - Returns the client ID:\n```json\n{\n \"clientId\": \"ff529adc-bc33-4db9-9309-9086d09f62d4\"\n}\n```\n\n**Error Models** - Match Python server's error format with `__type` field\n\n**Note:** For GET/PUT `/object/{bucket}/{key}`:\n- No special models needed - use route parameters and headers\n- `clientid` comes from request header\n- `content-metadata` comes from request header \n- Body is raw bytes\n\nReady for step 4 - creating the client cache service?" + } + }, + "request_metadata": { + "request_id": "db84f7a3-75b5-4083-a6cd-6df32db4c91e", + "message_id": "c2e0c94e-d11a-4468-b9f8-05b04a913ef3", + "request_start_timestamp_ms": 1757976841925, + "stream_end_timestamp_ms": 1757976849660, + "time_to_first_chunk": { + "secs": 2, + "nanos": 289558083 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 25917 + }, + { + "secs": 0, + "nanos": 8166 + }, + { + "secs": 0, + "nanos": 41783625 + }, + { + "secs": 0, + "nanos": 47654667 + }, + { + "secs": 0, + "nanos": 61645875 + }, + { + "secs": 0, + "nanos": 141076916 + }, + { + "secs": 0, + "nanos": 60248166 + }, + { + "secs": 0, + "nanos": 58699417 + }, + { + "secs": 0, + "nanos": 114440750 + }, + { + "secs": 0, + "nanos": 108020584 + }, + { + "secs": 0, + "nanos": 119092833 + }, + { + "secs": 0, + "nanos": 72398583 + }, + { + "secs": 0, + "nanos": 87272791 + }, + { + "secs": 0, + "nanos": 65579666 + }, + { + "secs": 0, + "nanos": 274669542 + }, + { + "secs": 0, + "nanos": 82889750 + }, + { + "secs": 0, + "nanos": 122202500 + }, + { + "secs": 0, + "nanos": 1302000 + }, + { + "secs": 0, + "nanos": 8750 + }, + { + "secs": 0, + "nanos": 436312292 + }, + { + "secs": 0, + "nanos": 5135917 + }, + { + "secs": 0, + "nanos": 1855875 + }, + { + "secs": 0, + "nanos": 2142541 + }, + { + "secs": 0, + "nanos": 1932250 + }, + { + "secs": 0, + "nanos": 1503458 + }, + { + "secs": 0, + "nanos": 7500 + }, + { + "secs": 0, + "nanos": 116882083 + }, + { + "secs": 0, + "nanos": 12541 + }, + { + "secs": 0, + "nanos": 176375 + }, + { + "secs": 0, + "nanos": 5625 + }, + { + "secs": 0, + "nanos": 2447000 + }, + { + "secs": 0, + "nanos": 17791 + }, + { + "secs": 0, + "nanos": 170081542 + }, + { + "secs": 0, + "nanos": 1764458 + }, + { + "secs": 0, + "nanos": 110354833 + }, + { + "secs": 0, + "nanos": 53043000 + }, + { + "secs": 0, + "nanos": 77336167 + }, + { + "secs": 0, + "nanos": 183374417 + }, + { + "secs": 0, + "nanos": 107177625 + }, + { + "secs": 0, + "nanos": 424756833 + }, + { + "secs": 0, + "nanos": 7083 + }, + { + "secs": 0, + "nanos": 984875 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 18958 + }, + { + "secs": 0, + "nanos": 47917 + }, + { + "secs": 0, + "nanos": 21958 + }, + { + "secs": 0, + "nanos": 24584 + }, + { + "secs": 0, + "nanos": 17000 + }, + { + "secs": 0, + "nanos": 19875 + }, + { + "secs": 0, + "nanos": 18541 + }, + { + "secs": 0, + "nanos": 31916 + }, + { + "secs": 0, + "nanos": 17458 + }, + { + "secs": 0, + "nanos": 48208 + }, + { + "secs": 0, + "nanos": 33375 + }, + { + "secs": 0, + "nanos": 38292 + }, + { + "secs": 0, + "nanos": 23875 + }, + { + "secs": 0, + "nanos": 22250 + }, + { + "secs": 0, + "nanos": 18833 + }, + { + "secs": 0, + "nanos": 23208 + }, + { + "secs": 0, + "nanos": 27500 + }, + { + "secs": 0, + "nanos": 16792 + }, + { + "secs": 0, + "nanos": 16459 + }, + { + "secs": 0, + "nanos": 17667 + }, + { + "secs": 0, + "nanos": 16542 + }, + { + "secs": 0, + "nanos": 20334 + }, + { + "secs": 0, + "nanos": 10042 + }, + { + "secs": 0, + "nanos": 17250 + }, + { + "secs": 0, + "nanos": 9833 + }, + { + "secs": 0, + "nanos": 17000 + }, + { + "secs": 0, + "nanos": 19291 + }, + { + "secs": 0, + "nanos": 16500 + }, + { + "secs": 0, + "nanos": 21958 + }, + { + "secs": 0, + "nanos": 19708 + }, + { + "secs": 0, + "nanos": 6666 + }, + { + "secs": 0, + "nanos": 84 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 907, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "yes" + } + }, + "timestamp": "2025-09-15T15:56:38.910978-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "fa393476-d36e-4d18-bdbd-26a2e202ce5f", + "content": "", + "tool_uses": [ + { + "id": "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs", + "command": "create", + "file_text": "using Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing System.Collections.Concurrent;\n\nnamespace NetV3Server.Services;\n\npublic interface IClientCacheService\n{\n string AddClient(AmazonS3EncryptionClientV2 client);\n AmazonS3EncryptionClientV2? GetClient(string clientId);\n}\n\npublic class ClientCacheService : IClientCacheService\n{\n private readonly ConcurrentDictionary _clients = new();\n\n public string AddClient(AmazonS3EncryptionClientV2 client)\n {\n var clientId = Guid.NewGuid().ToString();\n _clients[clientId] = client;\n return clientId;\n }\n\n public AmazonS3EncryptionClientV2? GetClient(string clientId)\n {\n _clients.TryGetValue(clientId, out var client);\n return client;\n }\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs", + "command": "create", + "file_text": "using Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing System.Collections.Concurrent;\n\nnamespace NetV3Server.Services;\n\npublic interface IClientCacheService\n{\n string AddClient(AmazonS3EncryptionClientV2 client);\n AmazonS3EncryptionClientV2? GetClient(string clientId);\n}\n\npublic class ClientCacheService : IClientCacheService\n{\n private readonly ConcurrentDictionary _clients = new();\n\n public string AddClient(AmazonS3EncryptionClientV2 client)\n {\n var clientId = Guid.NewGuid().ToString();\n _clients[clientId] = client;\n return clientId;\n }\n\n public AmazonS3EncryptionClientV2? GetClient(string clientId)\n {\n _clients.TryGetValue(clientId, out var client);\n return client;\n }\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "84af30b0-fa05-4ba6-8695-f5454dc47e69", + "message_id": "fa393476-d36e-4d18-bdbd-26a2e202ce5f", + "request_start_timestamp_ms": 1757976998918, + "stream_end_timestamp_ms": 1757977005274, + "time_to_first_chunk": { + "secs": 2, + "nanos": 688399125 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 38167 + }, + { + "secs": 0, + "nanos": 7917 + }, + { + "secs": 0, + "nanos": 981249625 + }, + { + "secs": 0, + "nanos": 103032916 + }, + { + "secs": 0, + "nanos": 30958 + }, + { + "secs": 0, + "nanos": 1728000 + }, + { + "secs": 0, + "nanos": 14499584 + }, + { + "secs": 0, + "nanos": 940500 + }, + { + "secs": 0, + "nanos": 1912958 + }, + { + "secs": 0, + "nanos": 4721709 + }, + { + "secs": 0, + "nanos": 14417 + }, + { + "secs": 0, + "nanos": 3518542 + }, + { + "secs": 0, + "nanos": 424875 + }, + { + "secs": 0, + "nanos": 35865292 + }, + { + "secs": 0, + "nanos": 9125 + }, + { + "secs": 0, + "nanos": 2417 + }, + { + "secs": 0, + "nanos": 1709 + }, + { + "secs": 0, + "nanos": 54167 + }, + { + "secs": 0, + "nanos": 7958 + }, + { + "secs": 0, + "nanos": 9967334 + }, + { + "secs": 0, + "nanos": 18042 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 2959 + }, + { + "secs": 0, + "nanos": 5166 + }, + { + "secs": 0, + "nanos": 6458 + }, + { + "secs": 0, + "nanos": 62458 + }, + { + "secs": 0, + "nanos": 16250 + }, + { + "secs": 0, + "nanos": 6417 + }, + { + "secs": 0, + "nanos": 10708 + }, + { + "secs": 0, + "nanos": 4167 + }, + { + "secs": 2, + "nanos": 420773292 + }, + { + "secs": 0, + "nanos": 18708 + }, + { + "secs": 0, + "nanos": 6667 + }, + { + "secs": 0, + "nanos": 5209 + }, + { + "secs": 0, + "nanos": 2993000 + }, + { + "secs": 0, + "nanos": 1577709 + }, + { + "secs": 0, + "nanos": 9292 + }, + { + "secs": 0, + "nanos": 6756416 + }, + { + "secs": 0, + "nanos": 17625 + }, + { + "secs": 0, + "nanos": 2858208 + }, + { + "secs": 0, + "nanos": 14000 + }, + { + "secs": 0, + "nanos": 4318667 + }, + { + "secs": 0, + "nanos": 49958 + }, + { + "secs": 0, + "nanos": 1937334 + }, + { + "secs": 0, + "nanos": 1952584 + }, + { + "secs": 0, + "nanos": 2111541 + }, + { + "secs": 0, + "nanos": 2071042 + }, + { + "secs": 0, + "nanos": 1166292 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 15958 + }, + { + "secs": 0, + "nanos": 2600500 + }, + { + "secs": 0, + "nanos": 17750 + }, + { + "secs": 0, + "nanos": 35542 + }, + { + "secs": 0, + "nanos": 14583 + }, + { + "secs": 0, + "nanos": 32699417 + }, + { + "secs": 0, + "nanos": 21417 + }, + { + "secs": 0, + "nanos": 9792 + }, + { + "secs": 0, + "nanos": 5875 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 5666 + }, + { + "secs": 0, + "nanos": 4917 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 4834 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 5556333 + }, + { + "secs": 0, + "nanos": 9958 + }, + { + "secs": 0, + "nanos": 2667 + }, + { + "secs": 0, + "nanos": 2416 + }, + { + "secs": 0, + "nanos": 2292 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 2166 + }, + { + "secs": 0, + "nanos": 2985542 + }, + { + "secs": 0, + "nanos": 9292 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 6458 + }, + { + "secs": 0, + "nanos": 2291 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 11625 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 2667 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 2791 + }, + { + "secs": 0, + "nanos": 2333 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 2166 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 48917 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 3625 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1416 + }, + { + "secs": 0, + "nanos": 1375 + }, + { + "secs": 0, + "nanos": 1375 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1250 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 2166 + }, + { + "secs": 0, + "nanos": 8875 + }, + { + "secs": 0, + "nanos": 3792 + }, + { + "secs": 0, + "nanos": 3656833 + }, + { + "secs": 0, + "nanos": 11959 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 5333 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 7750 + }, + { + "secs": 0, + "nanos": 3958 + }, + { + "secs": 0, + "nanos": 2417 + }, + { + "secs": 0, + "nanos": 4417 + }, + { + "secs": 0, + "nanos": 999917 + }, + { + "secs": 0, + "nanos": 5750 + }, + { + "secs": 0, + "nanos": 1217167 + }, + { + "secs": 0, + "nanos": 8375 + }, + { + "secs": 0, + "nanos": 2583 + }, + { + "secs": 0, + "nanos": 882750 + }, + { + "secs": 0, + "nanos": 8208 + }, + { + "secs": 0, + "nanos": 5003917 + }, + { + "secs": 0, + "nanos": 8750 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 8375 + }, + { + "secs": 0, + "nanos": 3333 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 1981834 + }, + { + "secs": 0, + "nanos": 7208 + }, + { + "secs": 0, + "nanos": 1279417 + }, + { + "secs": 0, + "nanos": 1960042 + }, + { + "secs": 0, + "nanos": 41 + } + ], + "user_prompt_length": 161, + "response_size": 1015, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "d5c5ab21-7a30-486f-a1e4-2b827c093b09", + "content": "Perfect! This service replicates the Python server's `client_cache` dictionary:\n\n## What This Service Does:\n\n**Replaces Python's:**\n```python\nclient_cache = {} # Dictionary to store clients with UUIDs as keys\n\n# Add client\nclient_id = str(uuid.uuid4())\nclient_cache[client_id] = client\n\n# Get client \nclient = client_cache.get(client_id)\n```\n\n**With .NET's:**\n```csharp\n// Add client\nstring clientId = _clientCacheService.AddClient(client);\n\n// Get client\nvar client = _clientCacheService.GetClient(clientId);\n```\n\n## Key Features:\n- **Thread-safe** - Uses `ConcurrentDictionary` (multiple requests can access safely)\n- **Interface** - `IClientCacheService` for dependency injection\n- **UUID generation** - `Guid.NewGuid()` like Python's `uuid.uuid4()`\n- **Null handling** - Returns `null` if client not found\n\nNext step: Register this service in `Program.cs` and create the controllers. Ready?" + } + }, + "request_metadata": { + "request_id": "180b5a53-544d-4cdc-85f8-e18f3bf9f891", + "message_id": "d5c5ab21-7a30-486f-a1e4-2b827c093b09", + "request_start_timestamp_ms": 1757977190987, + "stream_end_timestamp_ms": 1757977200574, + "time_to_first_chunk": { + "secs": 4, + "nanos": 374357542 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 19167 + }, + { + "secs": 0, + "nanos": 6041 + }, + { + "secs": 0, + "nanos": 39964959 + }, + { + "secs": 0, + "nanos": 65937708 + }, + { + "secs": 0, + "nanos": 49371416 + }, + { + "secs": 0, + "nanos": 55226292 + }, + { + "secs": 0, + "nanos": 120336417 + }, + { + "secs": 0, + "nanos": 42042 + }, + { + "secs": 0, + "nanos": 47009666 + }, + { + "secs": 0, + "nanos": 72529292 + }, + { + "secs": 0, + "nanos": 50171875 + }, + { + "secs": 0, + "nanos": 185849167 + }, + { + "secs": 0, + "nanos": 41024875 + }, + { + "secs": 0, + "nanos": 114017375 + }, + { + "secs": 0, + "nanos": 51586709 + }, + { + "secs": 0, + "nanos": 114075708 + }, + { + "secs": 0, + "nanos": 54457750 + }, + { + "secs": 0, + "nanos": 60492833 + }, + { + "secs": 0, + "nanos": 50480291 + }, + { + "secs": 0, + "nanos": 56490000 + }, + { + "secs": 0, + "nanos": 54789959 + }, + { + "secs": 0, + "nanos": 111317000 + }, + { + "secs": 0, + "nanos": 113278125 + }, + { + "secs": 0, + "nanos": 112004666 + }, + { + "secs": 0, + "nanos": 56030500 + }, + { + "secs": 0, + "nanos": 170201459 + }, + { + "secs": 0, + "nanos": 109292000 + }, + { + "secs": 0, + "nanos": 113360042 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 165501875 + }, + { + "secs": 0, + "nanos": 7375 + }, + { + "secs": 0, + "nanos": 14500 + }, + { + "secs": 0, + "nanos": 56683458 + }, + { + "secs": 0, + "nanos": 53013209 + }, + { + "secs": 0, + "nanos": 111424417 + }, + { + "secs": 0, + "nanos": 58205750 + }, + { + "secs": 0, + "nanos": 57958292 + }, + { + "secs": 0, + "nanos": 53428334 + }, + { + "secs": 0, + "nanos": 58031542 + }, + { + "secs": 0, + "nanos": 163391750 + }, + { + "secs": 0, + "nanos": 5708 + }, + { + "secs": 0, + "nanos": 398058292 + }, + { + "secs": 0, + "nanos": 7750 + }, + { + "secs": 0, + "nanos": 1502458 + }, + { + "secs": 0, + "nanos": 3023000 + }, + { + "secs": 0, + "nanos": 6416 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 4541 + }, + { + "secs": 0, + "nanos": 221365750 + }, + { + "secs": 0, + "nanos": 13041 + }, + { + "secs": 0, + "nanos": 507375 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 326553333 + }, + { + "secs": 0, + "nanos": 18375 + }, + { + "secs": 0, + "nanos": 29958 + }, + { + "secs": 0, + "nanos": 23958 + }, + { + "secs": 0, + "nanos": 19875 + }, + { + "secs": 0, + "nanos": 19292 + }, + { + "secs": 0, + "nanos": 18292 + }, + { + "secs": 0, + "nanos": 16375 + }, + { + "secs": 0, + "nanos": 16458 + }, + { + "secs": 0, + "nanos": 8875 + }, + { + "secs": 0, + "nanos": 15542 + }, + { + "secs": 0, + "nanos": 17584 + }, + { + "secs": 0, + "nanos": 16291 + }, + { + "secs": 0, + "nanos": 12542 + }, + { + "secs": 0, + "nanos": 18083 + }, + { + "secs": 0, + "nanos": 19583 + }, + { + "secs": 0, + "nanos": 19958 + }, + { + "secs": 0, + "nanos": 17500 + }, + { + "secs": 0, + "nanos": 18625 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 541 + }, + { + "secs": 0, + "nanos": 208 + } + ], + "user_prompt_length": 0, + "response_size": 896, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "yes" + } + }, + "timestamp": "2025-09-15T16:01:43.772104-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "6a1f2a59-c3fe-4131-a330-2670a2829915", + "content": "Let's first register the service in Program.cs, then create the controllers:", + "tool_uses": [ + { + "id": "tooluse_tR7cAfkrQyCBYYQ3RMElzA", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs" + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs" + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "f998ef7a-efbf-458d-8e96-2313212894f6", + "message_id": "6a1f2a59-c3fe-4131-a330-2670a2829915", + "request_start_timestamp_ms": 1757977303779, + "stream_end_timestamp_ms": 1757977308224, + "time_to_first_chunk": { + "secs": 2, + "nanos": 808598042 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 39375 + }, + { + "secs": 0, + "nanos": 57624167 + }, + { + "secs": 0, + "nanos": 44774667 + }, + { + "secs": 0, + "nanos": 2487708 + }, + { + "secs": 0, + "nanos": 89583 + }, + { + "secs": 0, + "nanos": 6667667 + }, + { + "secs": 0, + "nanos": 19391292 + }, + { + "secs": 0, + "nanos": 214956375 + }, + { + "secs": 0, + "nanos": 923553458 + }, + { + "secs": 0, + "nanos": 79027708 + }, + { + "secs": 0, + "nanos": 71783166 + }, + { + "secs": 0, + "nanos": 56079500 + }, + { + "secs": 0, + "nanos": 39121875 + }, + { + "secs": 0, + "nanos": 56872584 + }, + { + "secs": 0, + "nanos": 29174708 + }, + { + "secs": 0, + "nanos": 14833 + }, + { + "secs": 0, + "nanos": 1429667 + }, + { + "secs": 0, + "nanos": 7208 + }, + { + "secs": 0, + "nanos": 1187875 + }, + { + "secs": 0, + "nanos": 9542 + }, + { + "secs": 0, + "nanos": 767417 + }, + { + "secs": 0, + "nanos": 989791 + }, + { + "secs": 0, + "nanos": 10917 + }, + { + "secs": 0, + "nanos": 30821917 + }, + { + "secs": 0, + "nanos": 10000 + }, + { + "secs": 0, + "nanos": 3417 + }, + { + "secs": 0, + "nanos": 3000 + }, + { + "secs": 0, + "nanos": 2875 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2958 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 708 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 161, + "response_size": 251, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_tR7cAfkrQyCBYYQ3RMElzA", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_tR7cAfkrQyCBYYQ3RMElzA", + "content": [ + { + "Text": "var builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container.\n// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle\nbuilder.Services.AddEndpointsApiExplorer();\nbuilder.Services.AddSwaggerGen();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline.\nif (app.Environment.IsDevelopment())\n{\n app.UseSwagger();\n app.UseSwaggerUI();\n}\n\nvar summaries = new[]\n{\n \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\"\n};\n\napp.MapGet(\"/weatherforecast\", () =>\n{\n var forecast = Enumerable.Range(1, 5).Select(index =>\n new WeatherForecast\n (\n DateOnly.FromDateTime(DateTime.Now.AddDays(index)),\n Random.Shared.Next(-20, 55),\n summaries[Random.Shared.Next(summaries.Length)]\n ))\n .ToArray();\n return forecast;\n})\n.WithName(\"GetWeatherForecast\")\n.WithOpenApi();\n\napp.Run();\n\nrecord WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)\n{\n public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);\n}" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "ee694061-729a-4c00-8b06-4f9afd7962e3", + "content": "", + "tool_uses": [ + { + "id": "tooluse_tdhsdQtkToC-xYM9lDB2xg", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs", + "command": "create", + "file_text": "using NetV3Server.Services;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container\nbuilder.Services.AddControllers();\nbuilder.Services.AddSingleton();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline\napp.MapControllers();\n\napp.Run();" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs", + "command": "create", + "file_text": "using NetV3Server.Services;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container\nbuilder.Services.AddControllers();\nbuilder.Services.AddSingleton();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline\napp.MapControllers();\n\napp.Run();" + } + } + ] + } + }, + "request_metadata": { + "request_id": "171b2889-611e-4a35-a046-39323ed12c11", + "message_id": "ee694061-729a-4c00-8b06-4f9afd7962e3", + "request_start_timestamp_ms": 1757977308799, + "stream_end_timestamp_ms": 1757977312895, + "time_to_first_chunk": { + "secs": 2, + "nanos": 232189916 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 28833 + }, + { + "secs": 0, + "nanos": 44333 + }, + { + "secs": 0, + "nanos": 628267625 + }, + { + "secs": 0, + "nanos": 49511042 + }, + { + "secs": 0, + "nanos": 11475875 + }, + { + "secs": 0, + "nanos": 76311041 + }, + { + "secs": 0, + "nanos": 15720458 + }, + { + "secs": 0, + "nanos": 52472333 + }, + { + "secs": 0, + "nanos": 4636958 + }, + { + "secs": 0, + "nanos": 13584 + }, + { + "secs": 0, + "nanos": 5667 + }, + { + "secs": 0, + "nanos": 9958 + }, + { + "secs": 0, + "nanos": 17584 + }, + { + "secs": 0, + "nanos": 10000 + }, + { + "secs": 0, + "nanos": 12042 + }, + { + "secs": 0, + "nanos": 1532084 + }, + { + "secs": 0, + "nanos": 10417 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 8500 + }, + { + "secs": 0, + "nanos": 1123541 + }, + { + "secs": 0, + "nanos": 994375 + }, + { + "secs": 0, + "nanos": 12584 + }, + { + "secs": 0, + "nanos": 845583 + }, + { + "secs": 0, + "nanos": 1052667 + }, + { + "secs": 0, + "nanos": 9875 + }, + { + "secs": 0, + "nanos": 772916 + }, + { + "secs": 0, + "nanos": 2814291 + }, + { + "secs": 0, + "nanos": 12875 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 0, + "nanos": 952555417 + }, + { + "secs": 0, + "nanos": 15541 + }, + { + "secs": 0, + "nanos": 5958 + }, + { + "secs": 0, + "nanos": 5250 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 4458 + }, + { + "secs": 0, + "nanos": 4792 + }, + { + "secs": 0, + "nanos": 5500 + }, + { + "secs": 0, + "nanos": 4791 + }, + { + "secs": 0, + "nanos": 4792 + }, + { + "secs": 0, + "nanos": 19917 + }, + { + "secs": 0, + "nanos": 918250 + }, + { + "secs": 0, + "nanos": 28209 + }, + { + "secs": 0, + "nanos": 1785042 + }, + { + "secs": 0, + "nanos": 240958 + }, + { + "secs": 0, + "nanos": 16125 + }, + { + "secs": 0, + "nanos": 491041 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 637750 + }, + { + "secs": 0, + "nanos": 3208 + }, + { + "secs": 0, + "nanos": 1365167 + }, + { + "secs": 0, + "nanos": 3291 + }, + { + "secs": 0, + "nanos": 940250 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 1046959 + }, + { + "secs": 0, + "nanos": 3417 + }, + { + "secs": 0, + "nanos": 879250 + }, + { + "secs": 0, + "nanos": 3459 + }, + { + "secs": 0, + "nanos": 994375 + }, + { + "secs": 0, + "nanos": 3792 + }, + { + "secs": 0, + "nanos": 1034041 + }, + { + "secs": 0, + "nanos": 3250 + }, + { + "secs": 0, + "nanos": 876125 + }, + { + "secs": 0, + "nanos": 5583 + }, + { + "secs": 0, + "nanos": 1445500 + }, + { + "secs": 0, + "nanos": 5958 + }, + { + "secs": 0, + "nanos": 786042 + }, + { + "secs": 0, + "nanos": 2429959 + }, + { + "secs": 0, + "nanos": 17708 + }, + { + "secs": 0, + "nanos": 8334 + }, + { + "secs": 0, + "nanos": 9833 + }, + { + "secs": 0, + "nanos": 16417 + }, + { + "secs": 0, + "nanos": 11083 + }, + { + "secs": 0, + "nanos": 2045500 + }, + { + "secs": 0, + "nanos": 45761625 + }, + { + "secs": 0, + "nanos": 18125 + }, + { + "secs": 0, + "nanos": 6875 + }, + { + "secs": 0, + "nanos": 6209 + }, + { + "secs": 0, + "nanos": 6083 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 6042 + }, + { + "secs": 0, + "nanos": 80042 + }, + { + "secs": 0, + "nanos": 84 + } + ], + "user_prompt_length": 0, + "response_size": 518, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_tdhsdQtkToC-xYM9lDB2xg", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_tdhsdQtkToC-xYM9lDB2xg", + "content": [ + { + "Text": "" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "61715673-5fce-4b5d-9335-af60469e379b", + "content": "Now let's create the ClientController:", + "tool_uses": [ + { + "id": "tooluse_lhriUwZlQcmc0igmyxSvIQ", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", + "command": "create", + "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.KeyManagementService;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n // Create KMS client (hardcoded to us-west-2 like Python)\n var kmsClient = new AmazonKeyManagementServiceClient(Amazon.RegionEndpoint.USWest2);\n \n // Create S3 client\n var s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USWest2);\n \n // Create encryption materials\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionMaterials = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, new Dictionary());\n \n // Create encryption configuration\n var config = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy)\n {\n StorageMode = CryptoStorageMode.ObjectMetadata\n };\n \n // Create S3 encryption client\n var encryptionClient = new AmazonS3EncryptionClientV2(s3Client, config, encryptionMaterials);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", + "command": "create", + "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.KeyManagementService;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n // Create KMS client (hardcoded to us-west-2 like Python)\n var kmsClient = new AmazonKeyManagementServiceClient(Amazon.RegionEndpoint.USWest2);\n \n // Create S3 client\n var s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USWest2);\n \n // Create encryption materials\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionMaterials = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, new Dictionary());\n \n // Create encryption configuration\n var config = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy)\n {\n StorageMode = CryptoStorageMode.ObjectMetadata\n };\n \n // Create S3 encryption client\n var encryptionClient = new AmazonS3EncryptionClientV2(s3Client, config, encryptionMaterials);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "c91bf1df-c116-49d6-b60a-8c06fd0eea12", + "message_id": "61715673-5fce-4b5d-9335-af60469e379b", + "request_start_timestamp_ms": 1757977440049, + "stream_end_timestamp_ms": 1757977450462, + "time_to_first_chunk": { + "secs": 2, + "nanos": 481822708 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 15667 + }, + { + "secs": 0, + "nanos": 4458 + }, + { + "secs": 0, + "nanos": 38995792 + }, + { + "secs": 0, + "nanos": 170156125 + }, + { + "secs": 0, + "nanos": 575584709 + }, + { + "secs": 0, + "nanos": 6750 + }, + { + "secs": 0, + "nanos": 147833 + }, + { + "secs": 0, + "nanos": 568250 + }, + { + "secs": 0, + "nanos": 4291 + }, + { + "secs": 0, + "nanos": 894125 + }, + { + "secs": 0, + "nanos": 3709 + }, + { + "secs": 0, + "nanos": 20833 + }, + { + "secs": 0, + "nanos": 1234875 + }, + { + "secs": 0, + "nanos": 5708 + }, + { + "secs": 0, + "nanos": 63425333 + }, + { + "secs": 0, + "nanos": 6958 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1333 + }, + { + "secs": 0, + "nanos": 1334 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1333 + }, + { + "secs": 0, + "nanos": 288292 + }, + { + "secs": 0, + "nanos": 3833 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 82063250 + }, + { + "secs": 0, + "nanos": 140417 + }, + { + "secs": 0, + "nanos": 12875 + }, + { + "secs": 6, + "nanos": 832877709 + }, + { + "secs": 0, + "nanos": 16625 + }, + { + "secs": 0, + "nanos": 10083 + }, + { + "secs": 0, + "nanos": 6291 + }, + { + "secs": 0, + "nanos": 3472333 + }, + { + "secs": 0, + "nanos": 8542 + }, + { + "secs": 0, + "nanos": 1326000 + }, + { + "secs": 0, + "nanos": 5208 + }, + { + "secs": 0, + "nanos": 1993709 + }, + { + "secs": 0, + "nanos": 2747083 + }, + { + "secs": 0, + "nanos": 4001208 + }, + { + "secs": 0, + "nanos": 47625 + }, + { + "secs": 0, + "nanos": 11000 + }, + { + "secs": 0, + "nanos": 4100875 + }, + { + "secs": 0, + "nanos": 62708 + }, + { + "secs": 0, + "nanos": 18333 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 3402667 + }, + { + "secs": 0, + "nanos": 59125 + }, + { + "secs": 0, + "nanos": 11625 + }, + { + "secs": 0, + "nanos": 3844667 + }, + { + "secs": 0, + "nanos": 17208 + }, + { + "secs": 0, + "nanos": 438792 + }, + { + "secs": 0, + "nanos": 40633500 + }, + { + "secs": 0, + "nanos": 17875 + }, + { + "secs": 0, + "nanos": 7583 + }, + { + "secs": 0, + "nanos": 13208 + }, + { + "secs": 0, + "nanos": 24750 + }, + { + "secs": 0, + "nanos": 13125 + }, + { + "secs": 0, + "nanos": 15709 + }, + { + "secs": 0, + "nanos": 9791 + }, + { + "secs": 0, + "nanos": 11084 + }, + { + "secs": 0, + "nanos": 9792 + }, + { + "secs": 0, + "nanos": 11250 + }, + { + "secs": 0, + "nanos": 10125 + }, + { + "secs": 0, + "nanos": 11750 + }, + { + "secs": 0, + "nanos": 8958 + }, + { + "secs": 0, + "nanos": 11000 + }, + { + "secs": 0, + "nanos": 9625 + }, + { + "secs": 0, + "nanos": 10708 + }, + { + "secs": 0, + "nanos": 9875 + }, + { + "secs": 0, + "nanos": 10791 + }, + { + "secs": 0, + "nanos": 32583 + }, + { + "secs": 0, + "nanos": 6500 + }, + { + "secs": 0, + "nanos": 9708 + }, + { + "secs": 0, + "nanos": 10625 + }, + { + "secs": 0, + "nanos": 9542 + }, + { + "secs": 0, + "nanos": 11000 + }, + { + "secs": 0, + "nanos": 9417 + }, + { + "secs": 0, + "nanos": 11166 + }, + { + "secs": 0, + "nanos": 8500 + }, + { + "secs": 0, + "nanos": 11959 + }, + { + "secs": 0, + "nanos": 9375 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 9208 + }, + { + "secs": 0, + "nanos": 10583 + }, + { + "secs": 0, + "nanos": 5625 + }, + { + "secs": 0, + "nanos": 4958 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 5209 + }, + { + "secs": 0, + "nanos": 8500 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 34375 + }, + { + "secs": 0, + "nanos": 1498375 + }, + { + "secs": 0, + "nanos": 13250 + }, + { + "secs": 0, + "nanos": 6417 + }, + { + "secs": 0, + "nanos": 4564667 + }, + { + "secs": 0, + "nanos": 13083 + }, + { + "secs": 0, + "nanos": 5458 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 4708 + }, + { + "secs": 0, + "nanos": 7542 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 5334 + }, + { + "secs": 0, + "nanos": 5708 + }, + { + "secs": 0, + "nanos": 4916 + }, + { + "secs": 0, + "nanos": 2583 + }, + { + "secs": 0, + "nanos": 4584 + }, + { + "secs": 0, + "nanos": 2081041 + }, + { + "secs": 0, + "nanos": 11334 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 9709 + }, + { + "secs": 0, + "nanos": 18584 + }, + { + "secs": 0, + "nanos": 9834 + }, + { + "secs": 0, + "nanos": 12583 + }, + { + "secs": 0, + "nanos": 8500 + }, + { + "secs": 0, + "nanos": 2979917 + }, + { + "secs": 0, + "nanos": 1849708 + }, + { + "secs": 0, + "nanos": 2670708 + }, + { + "secs": 0, + "nanos": 1185750 + }, + { + "secs": 0, + "nanos": 3445750 + }, + { + "secs": 0, + "nanos": 458041 + }, + { + "secs": 0, + "nanos": 11125 + }, + { + "secs": 0, + "nanos": 1787250 + }, + { + "secs": 0, + "nanos": 9209 + }, + { + "secs": 0, + "nanos": 4006833 + }, + { + "secs": 0, + "nanos": 9625 + }, + { + "secs": 0, + "nanos": 5083 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 4458 + }, + { + "secs": 0, + "nanos": 8958 + }, + { + "secs": 0, + "nanos": 704083 + }, + { + "secs": 0, + "nanos": 10625 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 4209 + }, + { + "secs": 0, + "nanos": 222250 + }, + { + "secs": 0, + "nanos": 8959 + }, + { + "secs": 0, + "nanos": 4709 + }, + { + "secs": 0, + "nanos": 2542167 + }, + { + "secs": 0, + "nanos": 8458 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 35957292 + }, + { + "secs": 0, + "nanos": 85500 + }, + { + "secs": 0, + "nanos": 11584 + }, + { + "secs": 0, + "nanos": 5458 + }, + { + "secs": 0, + "nanos": 2417 + }, + { + "secs": 0, + "nanos": 3208 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 3041 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 2792 + }, + { + "secs": 0, + "nanos": 14834 + }, + { + "secs": 0, + "nanos": 4916 + }, + { + "secs": 0, + "nanos": 2542 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 2333 + }, + { + "secs": 0, + "nanos": 2292 + }, + { + "secs": 0, + "nanos": 1959 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 4125 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 4125 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 4541 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 3083 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 3833 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 3459 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 2667 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1666 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1791 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 5958 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 5792 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 3833 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 10833 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 2166 + }, + { + "secs": 0, + "nanos": 76250 + }, + { + "secs": 0, + "nanos": 4625 + }, + { + "secs": 0, + "nanos": 2459 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 1959 + }, + { + "secs": 0, + "nanos": 1958 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 3090250 + }, + { + "secs": 0, + "nanos": 5708 + }, + { + "secs": 0, + "nanos": 11000 + }, + { + "secs": 0, + "nanos": 3794958 + }, + { + "secs": 0, + "nanos": 7333 + }, + { + "secs": 0, + "nanos": 2834 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 2333 + }, + { + "secs": 0, + "nanos": 4667 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 3625 + }, + { + "secs": 0, + "nanos": 2792 + }, + { + "secs": 0, + "nanos": 3669458 + }, + { + "secs": 0, + "nanos": 6750 + }, + { + "secs": 0, + "nanos": 575709 + }, + { + "secs": 0, + "nanos": 4750 + }, + { + "secs": 0, + "nanos": 2500 + }, + { + "secs": 0, + "nanos": 4635042 + }, + { + "secs": 0, + "nanos": 6792 + }, + { + "secs": 0, + "nanos": 2542 + }, + { + "secs": 0, + "nanos": 2583 + }, + { + "secs": 0, + "nanos": 2417 + }, + { + "secs": 0, + "nanos": 6458 + }, + { + "secs": 0, + "nanos": 2292 + }, + { + "secs": 0, + "nanos": 1274750 + }, + { + "secs": 0, + "nanos": 22167 + }, + { + "secs": 0, + "nanos": 18750 + }, + { + "secs": 0, + "nanos": 3915667 + }, + { + "secs": 0, + "nanos": 7167 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 3083 + }, + { + "secs": 0, + "nanos": 7292 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 1250 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 1125 + }, + { + "secs": 0, + "nanos": 669542 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 2416 + }, + { + "secs": 0, + "nanos": 2084 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1958 + }, + { + "secs": 0, + "nanos": 4125 + }, + { + "secs": 0, + "nanos": 2042 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1791 + }, + { + "secs": 0, + "nanos": 1709 + }, + { + "secs": 0, + "nanos": 44000 + }, + { + "secs": 0, + "nanos": 2749250 + }, + { + "secs": 0, + "nanos": 2145000 + }, + { + "secs": 0, + "nanos": 6375 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 3542 + }, + { + "secs": 0, + "nanos": 9833 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 7875 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 1917 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 64417 + }, + { + "secs": 0, + "nanos": 10042 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 2000 + }, + { + "secs": 0, + "nanos": 1239167 + }, + { + "secs": 0, + "nanos": 7042 + }, + { + "secs": 0, + "nanos": 2834 + }, + { + "secs": 0, + "nanos": 2458 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 2041 + }, + { + "secs": 0, + "nanos": 14500 + }, + { + "secs": 0, + "nanos": 3459 + }, + { + "secs": 0, + "nanos": 2458 + }, + { + "secs": 0, + "nanos": 2125 + }, + { + "secs": 0, + "nanos": 916 + }, + { + "secs": 0, + "nanos": 10709 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 3416 + }, + { + "secs": 0, + "nanos": 2750 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 2346917 + }, + { + "secs": 0, + "nanos": 41 + } + ], + "user_prompt_length": 0, + "response_size": 2365, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_lhriUwZlQcmc0igmyxSvIQ", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "CancelledToolUses": { + "prompt": "Can you point me to this code in python?", + "tool_use_results": [ + { + "tool_use_id": "tooluse_lhriUwZlQcmc0igmyxSvIQ", + "content": [ + { + "Text": "Tool use was cancelled by the user" + } + ], + "status": "Error" + } + ] + } + }, + "timestamp": "2025-09-15T16:04:49.225623-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "5261871a-7196-40b1-b527-3d4b301964cb", + "content": "Sure! Here's the corresponding Python code from the test server:", + "tool_uses": [ + { + "id": "tooluse_hUPgjdC6SiK35mcMP88qYw", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", + "start_line": 120, + "end_line": 160 + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", + "start_line": 120, + "end_line": 160 + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "7b68d27e-abf5-450d-9653-2ecfa75cc17b", + "message_id": "5261871a-7196-40b1-b527-3d4b301964cb", + "request_start_timestamp_ms": 1757977489467, + "stream_end_timestamp_ms": 1757977493121, + "time_to_first_chunk": { + "secs": 2, + "nanos": 265048375 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 20959 + }, + { + "secs": 0, + "nanos": 6333 + }, + { + "secs": 0, + "nanos": 40191458 + }, + { + "secs": 0, + "nanos": 77387625 + }, + { + "secs": 0, + "nanos": 75763042 + }, + { + "secs": 0, + "nanos": 37136292 + }, + { + "secs": 0, + "nanos": 35948667 + }, + { + "secs": 0, + "nanos": 170207833 + }, + { + "secs": 0, + "nanos": 935385375 + }, + { + "secs": 0, + "nanos": 16209 + }, + { + "secs": 0, + "nanos": 25875 + }, + { + "secs": 0, + "nanos": 9333 + }, + { + "secs": 0, + "nanos": 2380375 + }, + { + "secs": 0, + "nanos": 3369167 + }, + { + "secs": 0, + "nanos": 6542 + }, + { + "secs": 0, + "nanos": 3584 + }, + { + "secs": 0, + "nanos": 4667 + }, + { + "secs": 0, + "nanos": 3417 + }, + { + "secs": 0, + "nanos": 2958 + }, + { + "secs": 0, + "nanos": 2958 + }, + { + "secs": 0, + "nanos": 3000 + }, + { + "secs": 0, + "nanos": 1026791 + }, + { + "secs": 0, + "nanos": 9500 + }, + { + "secs": 0, + "nanos": 2625 + }, + { + "secs": 0, + "nanos": 2803958 + }, + { + "secs": 0, + "nanos": 7209 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 3000 + }, + { + "secs": 0, + "nanos": 3250 + }, + { + "secs": 0, + "nanos": 729250 + }, + { + "secs": 0, + "nanos": 7291 + }, + { + "secs": 0, + "nanos": 552458 + }, + { + "secs": 0, + "nanos": 6194584 + }, + { + "secs": 0, + "nanos": 134416 + }, + { + "secs": 0, + "nanos": 14125 + }, + { + "secs": 0, + "nanos": 8833 + }, + { + "secs": 0, + "nanos": 6917 + }, + { + "secs": 0, + "nanos": 6959 + }, + { + "secs": 0, + "nanos": 11750 + }, + { + "secs": 0, + "nanos": 206375 + }, + { + "secs": 0, + "nanos": 42 + } + ], + "user_prompt_length": 198, + "response_size": 256, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_hUPgjdC6SiK35mcMP88qYw", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_hUPgjdC6SiK35mcMP88qYw", + "content": [ + { + "Text": " \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n try:\n # Use the client to make a GetObject request to S3\n response = client.get_object(**{\"Bucket\": bucket, \"Key\": key, \"EncryptionContext\": enc_ctx})\n\n # Extract the body and metadata from the response\n body = response.get(\"Body\").read() if response.get(\"Body\") else b\"\"\n metadata = response.get(\"Metadata\", [])\n\n # Convert metadata dictionary to a list of key-value pairs if it's a dict\n if isinstance(metadata, dict):\n metadata_list = [f\"{key}={value}\" for key, value in metadata.items()]\n else:\n metadata_list = metadata if isinstance(metadata, list) else []\n\n # Set the Content-Metadata header in the response\n # Convert metadata_list to a comma-separated string\n metadata_str = \",\".join(metadata_list) if metadata_list else \"\"\n headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "90a94be6-43f5-4cc3-a294-f444dc78ac78", + "content": "", + "tool_uses": [ + { + "id": "tooluse_xJEsgi-2QjC6iDbpmWDy_g", + "name": "fs_read", + "orig_name": "fs_read", + "args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", + "start_line": 150, + "end_line": 200 + } + ] + }, + "orig_args": { + "operations": [ + { + "mode": "Line", + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", + "start_line": 150, + "end_line": 200 + } + ] + } + } + ] + } + }, + "request_metadata": { + "request_id": "4167e1cb-f863-4706-9a79-fcd89cf1f6df", + "message_id": "90a94be6-43f5-4cc3-a294-f444dc78ac78", + "request_start_timestamp_ms": 1757977493586, + "stream_end_timestamp_ms": 1757977496583, + "time_to_first_chunk": { + "secs": 2, + "nanos": 396021291 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 34625 + }, + { + "secs": 0, + "nanos": 9485459 + }, + { + "secs": 0, + "nanos": 568155791 + }, + { + "secs": 0, + "nanos": 4080333 + }, + { + "secs": 0, + "nanos": 13875 + }, + { + "secs": 0, + "nanos": 4959 + }, + { + "secs": 0, + "nanos": 4542 + }, + { + "secs": 0, + "nanos": 801334 + }, + { + "secs": 0, + "nanos": 9417 + }, + { + "secs": 0, + "nanos": 3472666 + }, + { + "secs": 0, + "nanos": 9166 + }, + { + "secs": 0, + "nanos": 4792 + }, + { + "secs": 0, + "nanos": 2194792 + }, + { + "secs": 0, + "nanos": 10292 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 3792 + }, + { + "secs": 0, + "nanos": 2580625 + }, + { + "secs": 0, + "nanos": 9792 + }, + { + "secs": 0, + "nanos": 5916 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 3650209 + }, + { + "secs": 0, + "nanos": 11250 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 4333 + }, + { + "secs": 0, + "nanos": 13125 + }, + { + "secs": 0, + "nanos": 8667 + }, + { + "secs": 0, + "nanos": 209792 + }, + { + "secs": 0, + "nanos": 8584 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 2337000 + }, + { + "secs": 0, + "nanos": 1499875 + }, + { + "secs": 0, + "nanos": 1982500 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 0, + "response_size": 192, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_xJEsgi-2QjC6iDbpmWDy_g", + "fs_read" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "ToolUseResults": { + "tool_use_results": [ + { + "tool_use_id": "tooluse_xJEsgi-2QjC6iDbpmWDy_g", + "content": [ + { + "Text": " headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n \"\"\"\n Handle POST requests to /client by creating an S3EncryptionClient.\n \"\"\"\n body = await request.body()\n\n # Parse the bytes object as JSON\n try:\n # Decode bytes to string and parse as JSON\n parsed_data = json.loads(body.decode(\"utf-8\"))\n\n # Extract config from the parsed data\n config_data = parsed_data.get(\"config\", {})\n # Extract key material if provided\n key_material = config_data.get(\"keyMaterial\", {})\n\n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n\n # TODO pull region from ARN\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n\n # Create S3EncryptionClient\n client = S3EncryptionClient(wrapped_client, client_config)\n\n # Generate a client ID using UUID\n client_id = str(uuid.uuid4())\n\n # Add the client to the client_cache dictionary\n client_cache[client_id] = client\n\n return {\"clientId\": client_id}\n except json.JSONDecodeError as e:" + } + ], + "status": "Success" + } + ] + } + }, + "timestamp": null, + "images": null + }, + "assistant": { + "Response": { + "message_id": "c5e73dca-4a04-4b68-ac06-9db4f30a593f", + "content": "Here's the Python code that corresponds to what I was trying to implement in .NET:\n\n## Python Client Creation (lines 160-190):\n\n```python\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n # Parse JSON request\n parsed_data = json.loads(body.decode(\"utf-8\"))\n config_data = parsed_data.get(\"config\", {})\n key_material = config_data.get(\"keyMaterial\", {})\n \n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n \n # Create KMS client (hardcoded us-west-2)\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n \n # Create keyring and S3 encryption client\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n client = S3EncryptionClient(wrapped_client, client_config)\n \n # Generate UUID and cache client\n client_id = str(uuid.uuid4())\n client_cache[client_id] = client\n \n return {\"clientId\": client_id}\n```\n\nThe .NET version needs to do the same thing but with the Amazon.Extensions.S3.Encryption v3 API instead of the Python API." + } + }, + "request_metadata": { + "request_id": "3989e535-7d89-4f3b-9bc9-2a38561e1f1f", + "message_id": "c5e73dca-4a04-4b68-ac06-9db4f30a593f", + "request_start_timestamp_ms": 1757977497297, + "stream_end_timestamp_ms": 1757977510793, + "time_to_first_chunk": { + "secs": 2, + "nanos": 123736792 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 13625 + }, + { + "secs": 0, + "nanos": 11750 + }, + { + "secs": 0, + "nanos": 35474458 + }, + { + "secs": 0, + "nanos": 52508791 + }, + { + "secs": 0, + "nanos": 58570833 + }, + { + "secs": 0, + "nanos": 80316084 + }, + { + "secs": 0, + "nanos": 27029333 + }, + { + "secs": 0, + "nanos": 54990542 + }, + { + "secs": 0, + "nanos": 53780917 + }, + { + "secs": 0, + "nanos": 56931792 + }, + { + "secs": 0, + "nanos": 52288458 + }, + { + "secs": 0, + "nanos": 60277959 + }, + { + "secs": 0, + "nanos": 54682083 + }, + { + "secs": 0, + "nanos": 54092583 + }, + { + "secs": 0, + "nanos": 53651750 + }, + { + "secs": 0, + "nanos": 54378167 + }, + { + "secs": 0, + "nanos": 54924417 + }, + { + "secs": 0, + "nanos": 54354084 + }, + { + "secs": 0, + "nanos": 55123125 + }, + { + "secs": 0, + "nanos": 56179917 + }, + { + "secs": 0, + "nanos": 57912375 + }, + { + "secs": 0, + "nanos": 57621750 + }, + { + "secs": 0, + "nanos": 48962375 + }, + { + "secs": 0, + "nanos": 60463708 + }, + { + "secs": 0, + "nanos": 107805208 + }, + { + "secs": 0, + "nanos": 61915166 + }, + { + "secs": 0, + "nanos": 52671917 + }, + { + "secs": 0, + "nanos": 54904208 + }, + { + "secs": 0, + "nanos": 64280375 + }, + { + "secs": 0, + "nanos": 102642167 + }, + { + "secs": 0, + "nanos": 9958 + }, + { + "secs": 0, + "nanos": 215482459 + }, + { + "secs": 0, + "nanos": 15417 + }, + { + "secs": 0, + "nanos": 73542 + }, + { + "secs": 0, + "nanos": 2016750 + }, + { + "secs": 0, + "nanos": 394301958 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 1498125 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 435000 + }, + { + "secs": 0, + "nanos": 711366042 + }, + { + "secs": 0, + "nanos": 8167 + }, + { + "secs": 0, + "nanos": 1339208 + }, + { + "secs": 0, + "nanos": 17984416 + }, + { + "secs": 0, + "nanos": 17459 + }, + { + "secs": 0, + "nanos": 6084 + }, + { + "secs": 0, + "nanos": 7580959 + }, + { + "secs": 0, + "nanos": 408750 + }, + { + "secs": 0, + "nanos": 5292 + }, + { + "secs": 0, + "nanos": 24958 + }, + { + "secs": 0, + "nanos": 18167 + }, + { + "secs": 0, + "nanos": 19708 + }, + { + "secs": 0, + "nanos": 18042 + }, + { + "secs": 0, + "nanos": 18750 + }, + { + "secs": 0, + "nanos": 17792 + }, + { + "secs": 0, + "nanos": 20667 + }, + { + "secs": 0, + "nanos": 20709 + }, + { + "secs": 0, + "nanos": 23791 + }, + { + "secs": 0, + "nanos": 17041 + }, + { + "secs": 0, + "nanos": 22917 + }, + { + "secs": 0, + "nanos": 19542 + }, + { + "secs": 0, + "nanos": 20500 + }, + { + "secs": 0, + "nanos": 54667 + }, + { + "secs": 0, + "nanos": 16792 + }, + { + "secs": 0, + "nanos": 29333 + }, + { + "secs": 0, + "nanos": 17000 + }, + { + "secs": 0, + "nanos": 24709 + }, + { + "secs": 0, + "nanos": 29416 + }, + { + "secs": 0, + "nanos": 10583 + }, + { + "secs": 0, + "nanos": 18375 + }, + { + "secs": 0, + "nanos": 31292 + }, + { + "secs": 0, + "nanos": 18792 + }, + { + "secs": 0, + "nanos": 18792 + }, + { + "secs": 0, + "nanos": 19875 + }, + { + "secs": 0, + "nanos": 10167 + }, + { + "secs": 0, + "nanos": 17250 + }, + { + "secs": 0, + "nanos": 18167 + }, + { + "secs": 0, + "nanos": 18125 + }, + { + "secs": 0, + "nanos": 96542 + }, + { + "secs": 0, + "nanos": 30667 + }, + { + "secs": 0, + "nanos": 30459 + }, + { + "secs": 0, + "nanos": 20708 + }, + { + "secs": 0, + "nanos": 38667 + }, + { + "secs": 0, + "nanos": 23167 + }, + { + "secs": 0, + "nanos": 17875 + }, + { + "secs": 0, + "nanos": 17291 + }, + { + "secs": 0, + "nanos": 16917 + }, + { + "secs": 0, + "nanos": 32041 + }, + { + "secs": 0, + "nanos": 16083 + }, + { + "secs": 0, + "nanos": 18875 + }, + { + "secs": 0, + "nanos": 42375 + }, + { + "secs": 0, + "nanos": 18417 + }, + { + "secs": 0, + "nanos": 13459 + }, + { + "secs": 0, + "nanos": 833 + }, + { + "secs": 0, + "nanos": 417 + } + ], + "user_prompt_length": 0, + "response_size": 1287, + "chat_conversation_type": "NotToolUse", + "tool_use_ids_and_names": [], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + }, + { + "user": { + "additional_context": "", + "env_context": { + "env_state": { + "operating_system": "macos", + "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", + "environment_variables": [] + } + }, + "content": { + "Prompt": { + "prompt": "Thanks. This is how a basic put and get works with AmazonS3EncryptionClientV2: using Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\nusing Amazon.S3;\nusing Amazon.S3.Model;\n\nvar encryptionContext = new Dictionary();\nvar encryptionMaterial =\n new EncryptionMaterialsV2(Environment.GetEnvironmentVariable(\"key1\"), KmsType.KmsContext, encryptionContext);\n\nvar encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n\nIAmazonS3 encryptionClient = new AmazonS3Client();\nvar putContext = \"abc\";\nvar bucketName = \"testwiththisbucket\";\nvar key = \"test.txt\";\n\n// Put object\nvar putRequest = new PutObjectRequest\n{\n BucketName = bucketName,\n Key = key,\n ContentBody = putContext\n};\n\nawait encryptionClient.PutObjectAsync(putRequest);\n\nvar getRequest = new GetObjectRequest\n{\n BucketName = bucketName,\n Key = key\n};\n\nvar response = await encryptionClient.GetObjectAsync(getRequest);\nusing var reader = new StreamReader(response.ResponseStream);\nvar retrievedContent = await reader.ReadToEndAsync();\n\nConsole.WriteLine($\"Retrieved: {retrievedContent}\");" + } + }, + "timestamp": "2025-09-15T16:08:26.102193-07:00", + "images": null + }, + "assistant": { + "ToolUse": { + "message_id": "f12c3aea-fbc5-4186-96c8-3b2f181c6cf6", + "content": "Perfect! Now I can create the correct .NET controllers using the proper v3 API. Let me create the ClientController first:", + "tool_uses": [ + { + "id": "tooluse_optyE9P-TWeP7pg561V3Rg", + "name": "fs_write", + "orig_name": "fs_write", + "args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", + "command": "create", + "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionContext = new Dictionary();\n \n // Create encryption materials\n var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext);\n \n // Create S3 encryption client\n var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy);\n var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" + }, + "orig_args": { + "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", + "command": "create", + "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionContext = new Dictionary();\n \n // Create encryption materials\n var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext);\n \n // Create S3 encryption client\n var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy);\n var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" + } + } + ] + } + }, + "request_metadata": { + "request_id": "58e3d75f-d1e2-4202-af17-bf29432acfd2", + "message_id": "f12c3aea-fbc5-4186-96c8-3b2f181c6cf6", + "request_start_timestamp_ms": 1757977706106, + "stream_end_timestamp_ms": 1757977716903, + "time_to_first_chunk": { + "secs": 4, + "nanos": 978817000 + }, + "time_between_chunks": [ + { + "secs": 0, + "nanos": 37917 + }, + { + "secs": 0, + "nanos": 24083 + }, + { + "secs": 0, + "nanos": 42745292 + }, + { + "secs": 0, + "nanos": 48322542 + }, + { + "secs": 0, + "nanos": 66440042 + }, + { + "secs": 0, + "nanos": 48699584 + }, + { + "secs": 0, + "nanos": 57406000 + }, + { + "secs": 0, + "nanos": 53901250 + }, + { + "secs": 0, + "nanos": 54092125 + }, + { + "secs": 0, + "nanos": 56850125 + }, + { + "secs": 0, + "nanos": 91681000 + }, + { + "secs": 0, + "nanos": 53761708 + }, + { + "secs": 0, + "nanos": 59511250 + }, + { + "secs": 0, + "nanos": 52875625 + }, + { + "secs": 0, + "nanos": 51298667 + }, + { + "secs": 0, + "nanos": 283457750 + }, + { + "secs": 0, + "nanos": 463286042 + }, + { + "secs": 0, + "nanos": 18875 + }, + { + "secs": 0, + "nanos": 776416 + }, + { + "secs": 0, + "nanos": 32084 + }, + { + "secs": 0, + "nanos": 1251375 + }, + { + "secs": 0, + "nanos": 338375 + }, + { + "secs": 0, + "nanos": 57833 + }, + { + "secs": 0, + "nanos": 1238042 + }, + { + "secs": 0, + "nanos": 807875 + }, + { + "secs": 0, + "nanos": 48167 + }, + { + "secs": 0, + "nanos": 17834 + }, + { + "secs": 0, + "nanos": 55894083 + }, + { + "secs": 0, + "nanos": 12208 + }, + { + "secs": 0, + "nanos": 4667 + }, + { + "secs": 0, + "nanos": 3958 + }, + { + "secs": 0, + "nanos": 3958 + }, + { + "secs": 0, + "nanos": 8084 + }, + { + "secs": 0, + "nanos": 4000 + }, + { + "secs": 0, + "nanos": 3750 + }, + { + "secs": 0, + "nanos": 3625 + }, + { + "secs": 0, + "nanos": 6625 + }, + { + "secs": 0, + "nanos": 3667 + }, + { + "secs": 0, + "nanos": 5542 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 2917 + }, + { + "secs": 0, + "nanos": 107114125 + }, + { + "secs": 0, + "nanos": 17542 + }, + { + "secs": 0, + "nanos": 7333 + }, + { + "secs": 0, + "nanos": 6292 + }, + { + "secs": 3, + "nanos": 588082541 + }, + { + "secs": 0, + "nanos": 5625667 + }, + { + "secs": 0, + "nanos": 27279000 + }, + { + "secs": 0, + "nanos": 31656291 + }, + { + "secs": 0, + "nanos": 34590667 + }, + { + "secs": 0, + "nanos": 17964542 + }, + { + "secs": 0, + "nanos": 26545792 + }, + { + "secs": 0, + "nanos": 35439625 + }, + { + "secs": 0, + "nanos": 37886625 + }, + { + "secs": 0, + "nanos": 35373417 + }, + { + "secs": 0, + "nanos": 14934666 + }, + { + "secs": 0, + "nanos": 27312875 + }, + { + "secs": 0, + "nanos": 31947292 + }, + { + "secs": 0, + "nanos": 30889458 + }, + { + "secs": 0, + "nanos": 47385583 + }, + { + "secs": 0, + "nanos": 31048834 + }, + { + "secs": 0, + "nanos": 30953833 + }, + { + "secs": 0, + "nanos": 20183709 + }, + { + "secs": 0, + "nanos": 14417 + }, + { + "secs": 0, + "nanos": 12899667 + }, + { + "secs": 0, + "nanos": 6667834 + }, + { + "secs": 0, + "nanos": 1319083 + }, + { + "secs": 0, + "nanos": 1288541 + }, + { + "secs": 0, + "nanos": 2570959 + }, + { + "secs": 0, + "nanos": 17875 + }, + { + "secs": 0, + "nanos": 1762083 + }, + { + "secs": 0, + "nanos": 20917 + }, + { + "secs": 0, + "nanos": 2419125 + }, + { + "secs": 0, + "nanos": 15083 + }, + { + "secs": 0, + "nanos": 5667 + }, + { + "secs": 0, + "nanos": 4708 + }, + { + "secs": 0, + "nanos": 5167 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 2296458 + }, + { + "secs": 0, + "nanos": 16208 + }, + { + "secs": 0, + "nanos": 5709 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 42333 + }, + { + "secs": 0, + "nanos": 32375 + }, + { + "secs": 0, + "nanos": 10693833 + }, + { + "secs": 0, + "nanos": 312167 + }, + { + "secs": 0, + "nanos": 8041 + }, + { + "secs": 0, + "nanos": 5625 + }, + { + "secs": 0, + "nanos": 5417 + }, + { + "secs": 0, + "nanos": 1233959 + }, + { + "secs": 0, + "nanos": 17186333 + }, + { + "secs": 0, + "nanos": 33208 + }, + { + "secs": 0, + "nanos": 6542 + }, + { + "secs": 0, + "nanos": 48375 + }, + { + "secs": 0, + "nanos": 8250 + }, + { + "secs": 0, + "nanos": 5667 + }, + { + "secs": 0, + "nanos": 5333 + }, + { + "secs": 0, + "nanos": 4833 + }, + { + "secs": 0, + "nanos": 4791 + }, + { + "secs": 0, + "nanos": 4500 + }, + { + "secs": 0, + "nanos": 12958 + }, + { + "secs": 0, + "nanos": 7291 + }, + { + "secs": 0, + "nanos": 3875 + }, + { + "secs": 0, + "nanos": 4000 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 3458 + }, + { + "secs": 0, + "nanos": 3125 + }, + { + "secs": 0, + "nanos": 5375 + }, + { + "secs": 0, + "nanos": 5000 + }, + { + "secs": 0, + "nanos": 9083 + }, + { + "secs": 0, + "nanos": 320500 + }, + { + "secs": 0, + "nanos": 8875 + }, + { + "secs": 0, + "nanos": 4875 + }, + { + "secs": 0, + "nanos": 8042 + }, + { + "secs": 0, + "nanos": 4959 + }, + { + "secs": 0, + "nanos": 7166 + }, + { + "secs": 0, + "nanos": 10916 + }, + { + "secs": 0, + "nanos": 12158125 + }, + { + "secs": 0, + "nanos": 7709 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 1346917 + }, + { + "secs": 0, + "nanos": 3542 + }, + { + "secs": 0, + "nanos": 15834 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 1541 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 4405458 + }, + { + "secs": 0, + "nanos": 6500 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1666 + }, + { + "secs": 0, + "nanos": 1666 + }, + { + "secs": 0, + "nanos": 1672834 + }, + { + "secs": 0, + "nanos": 4125 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1417 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 6458 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 1296083 + }, + { + "secs": 0, + "nanos": 4375 + }, + { + "secs": 0, + "nanos": 1666 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1125 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 8541 + }, + { + "secs": 0, + "nanos": 4916 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1167 + }, + { + "secs": 0, + "nanos": 833 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1208 + }, + { + "secs": 0, + "nanos": 3834 + }, + { + "secs": 0, + "nanos": 1791 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 4000 + }, + { + "secs": 0, + "nanos": 959 + }, + { + "secs": 0, + "nanos": 7458 + }, + { + "secs": 0, + "nanos": 2741958 + }, + { + "secs": 0, + "nanos": 5125 + }, + { + "secs": 0, + "nanos": 1292 + }, + { + "secs": 0, + "nanos": 2250 + }, + { + "secs": 0, + "nanos": 1958 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 959 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1833 + }, + { + "secs": 0, + "nanos": 1709 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1000 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 1375 + }, + { + "secs": 0, + "nanos": 8625 + }, + { + "secs": 0, + "nanos": 3584 + }, + { + "secs": 0, + "nanos": 1250 + }, + { + "secs": 0, + "nanos": 1328750 + }, + { + "secs": 0, + "nanos": 3667 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 916 + }, + { + "secs": 0, + "nanos": 7833 + }, + { + "secs": 0, + "nanos": 1541 + }, + { + "secs": 0, + "nanos": 2709 + }, + { + "secs": 0, + "nanos": 1791 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 4291 + }, + { + "secs": 0, + "nanos": 3333 + }, + { + "secs": 0, + "nanos": 3000 + }, + { + "secs": 0, + "nanos": 1916 + }, + { + "secs": 0, + "nanos": 1125 + }, + { + "secs": 0, + "nanos": 1416 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 1584 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1375 + }, + { + "secs": 0, + "nanos": 18000 + }, + { + "secs": 0, + "nanos": 3542 + }, + { + "secs": 0, + "nanos": 791 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1459 + }, + { + "secs": 0, + "nanos": 1583 + }, + { + "secs": 0, + "nanos": 958 + }, + { + "secs": 0, + "nanos": 3209 + }, + { + "secs": 0, + "nanos": 1416 + }, + { + "secs": 0, + "nanos": 1417 + }, + { + "secs": 0, + "nanos": 1500 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 958 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 2631958 + }, + { + "secs": 0, + "nanos": 6959 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 4250 + }, + { + "secs": 0, + "nanos": 2416 + }, + { + "secs": 0, + "nanos": 1458 + }, + { + "secs": 0, + "nanos": 2084 + }, + { + "secs": 0, + "nanos": 2375 + }, + { + "secs": 0, + "nanos": 3750 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 3084 + }, + { + "secs": 0, + "nanos": 2209 + }, + { + "secs": 0, + "nanos": 2916 + }, + { + "secs": 0, + "nanos": 1709 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 2083 + }, + { + "secs": 0, + "nanos": 1125 + }, + { + "secs": 0, + "nanos": 10250 + }, + { + "secs": 0, + "nanos": 1166 + }, + { + "secs": 0, + "nanos": 1667 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1750 + }, + { + "secs": 0, + "nanos": 1708 + }, + { + "secs": 0, + "nanos": 7000 + }, + { + "secs": 0, + "nanos": 3458 + }, + { + "secs": 0, + "nanos": 1416 + }, + { + "secs": 0, + "nanos": 1167 + }, + { + "secs": 0, + "nanos": 3750 + }, + { + "secs": 0, + "nanos": 1209 + }, + { + "secs": 0, + "nanos": 3458 + }, + { + "secs": 0, + "nanos": 1084 + }, + { + "secs": 0, + "nanos": 791 + }, + { + "secs": 0, + "nanos": 12625 + }, + { + "secs": 0, + "nanos": 4583 + }, + { + "secs": 0, + "nanos": 875 + }, + { + "secs": 0, + "nanos": 792 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1334 + }, + { + "secs": 0, + "nanos": 1042 + }, + { + "secs": 0, + "nanos": 56542 + }, + { + "secs": 0, + "nanos": 834 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 2167 + }, + { + "secs": 0, + "nanos": 750 + }, + { + "secs": 0, + "nanos": 708 + }, + { + "secs": 0, + "nanos": 791 + }, + { + "secs": 0, + "nanos": 10750 + }, + { + "secs": 0, + "nanos": 958 + }, + { + "secs": 0, + "nanos": 1792 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 1542 + }, + { + "secs": 0, + "nanos": 1000 + }, + { + "secs": 0, + "nanos": 916 + }, + { + "secs": 0, + "nanos": 875 + }, + { + "secs": 0, + "nanos": 1875 + }, + { + "secs": 0, + "nanos": 1000 + }, + { + "secs": 0, + "nanos": 5042 + }, + { + "secs": 0, + "nanos": 875 + }, + { + "secs": 0, + "nanos": 1625 + }, + { + "secs": 0, + "nanos": 957417 + }, + { + "secs": 0, + "nanos": 1291 + }, + { + "secs": 0, + "nanos": 4458 + }, + { + "secs": 0, + "nanos": 1208 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 1834 + }, + { + "secs": 0, + "nanos": 2208 + }, + { + "secs": 0, + "nanos": 12166 + }, + { + "secs": 0, + "nanos": 83 + } + ], + "user_prompt_length": 1298, + "response_size": 2019, + "chat_conversation_type": "ToolUse", + "tool_use_ids_and_names": [ + [ + "tooluse_optyE9P-TWeP7pg561V3Rg", + "fs_write" + ] + ], + "model_id": "claude-sonnet-4", + "message_meta_tags": [] + } + } + ], + "valid_history_range": [ + 0, + 50 + ], + "transcript": [ + "> /load", + "> read /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server. I want to to know what this test server does. src/main.py is the starting point of the program. I want to to know what this test server does. src/main.py is the starting point of the program.", + "I'll read the test server directory and examine the main.py file to understand what this test server does.\n[Tool uses: fs_read]", + "\n[Tool uses: fs_read]", + "\n[Tool uses: fs_read]", + "\n[Tool uses: fs_read]", + "This test server is a **FastAPI-based HTTP server that provides a REST API wrapper around the S3 Encryption Client**. Here's what it does:\n\n## Purpose\nThe server acts as a test harness for the Amazon S3 Encryption Client Python library, allowing HTTP-based testing of client-side encryption functionality.\n\n## Key Functionality\n\n**Client Management:**\n- `POST /client` - Creates S3EncryptionClient instances with KMS keyring configuration\n- Stores clients in memory cache with UUID identifiers\n- Supports legacy wrapping algorithms configuration\n\n**S3 Operations:**\n- `PUT /object/{bucket}/{key}` - Encrypts and uploads objects to S3\n- `GET /object/{bucket}/{key}` - Downloads and decrypts objects from S3\n- Handles encryption context via `Content-Metadata` headers\n\n**Architecture:**\n- Uses FastAPI framework running on localhost:8081\n- Integrates with AWS KMS for key management (hardcoded to us-west-2 region)\n- Converts metadata between string format and dictionary for encryption context\n- Provides structured error responses matching Smithy model types\n\nThe server essentially allows you to test S3 client-side encryption operations through HTTP requests rather than direct Python API calls, making it useful for integration testing or cross-language compatibility testing.\n[Tool uses: none]", + "> /save", + "> /save .", + "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/qSaved", + "> Now, I want to start writing similar implementation but in .Net. I have already created net-v3-server where I plan to test S3-EC net v3 like python. Don't write any code but I want to prepare first. where do you think I should start? I don't know dotnet much, how should project structure look like?", + "Based on the Python test server structure, here's how you should approach the .NET implementation:\n\n## Project Structure for .NET\n\n```\nnet-v3-server/\n├── src/\n│ ├── NetV3Server/\n│ │ ├── Controllers/\n│ │ │ ├── ClientController.cs\n│ │ │ └── ObjectController.cs\n│ │ ├── Models/\n│ │ │ ├── ClientRequest.cs\n│ │ │ ├── ClientResponse.cs\n│ │ │ └── ErrorModels.cs\n│ │ ├── Services/\n│ │ │ └── ClientCacheService.cs\n│ │ ├── Program.cs\n│ │ └── NetV3Server.csproj\n├── tests/\n│ └── NetV3Server.Tests/\n│ └── NetV3Server.Tests.csproj\n├── NetV3Server.sln\n└── README.md\n```\n\n## Where to Start\n\n**1. Choose Framework:**\n- Use **ASP.NET Core Web API** (equivalent to FastAPI)\n- Target .NET 6+ for modern features\n\n**2. Key Dependencies You'll Need:**\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK\n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n- `Microsoft.AspNetCore.Mvc` - Web API framework\n\n**3. Core Components to Implement:**\n- **Program.cs** - Entry point (like main.py)\n- **Controllers** - Handle HTTP endpoints (`/client`, `/object/{bucket}/{key}`)\n- **ClientCacheService** - In-memory client storage (like client_cache dict)\n- **Models** - Request/response DTOs\n- **Error handling** - Custom exception responses\n\n**4. Learning Path:**\n1. Start with basic ASP.NET Core Web API tutorial\n2. Learn dependency injection (built into .NET)\n3. Understand controllers and routing\n4. Study AWS SDK for .NET usage patterns\n\n**5. Initial Setup Steps:**\n1. Install .NET SDK\n2. Create solution: `dotnet new sln -n NetV3Server`\n3. Create web API project: `dotnet new webapi -n NetV3Server`\n4. Add AWS NuGet packages\n5. Set up basic controller structure\n\nThe .NET version will be more structured than Python due to strong typing and dependency injection, but the core logic flow will mirror your Python implementation exactly.\n[Tool uses: none]", + "> How do I create such directory structure?", + "I'll help you create the .NET project structure. Let me check your current directory first and then create the structure.\n[Tool uses: execute_bash]", + "\n[Tool uses: fs_read]", + "\n[Tool uses: fs_read]", + "I see you already have a `net-v3-server` directory. Let me check what's in it and then create the proper .NET structure:\n[Tool uses: fs_read]", + "Now I'll create the proper .NET project structure in your existing `net-v3-server` directory:\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> Why would you need a test?", + "You're right to question that. For your immediate goal of creating a test server similar to the Python one, you **don't need tests**.\n\nThe test project was just following .NET conventions, but since you're building a test server (which itself is a testing tool), adding unit tests would be overkill and unnecessary complexity.\n\nLet me clean up and show you the minimal structure you actually need:\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: execute_bash]", + "> y", + "\n[Tool uses: fs_read]", + "Perfect! Now you have a clean, minimal .NET project structure:\n\n```\nnet-v3-server/\n├── NetV3Server.sln # Solution file\n├── src/NetV3Server/\n│ ├── Controllers/ # Your API endpoints (empty, ready for you)\n│ ├── Models/ # Request/response models (empty, ready for you) \n│ ├── Services/ # Business logic like ClientCacheService (empty, ready for you)\n│ ├── Program.cs # Entry point (like main.py)\n│ ├── NetV3Server.csproj # Project dependencies\n│ └── appsettings.json # Configuration\n└── README.md\n```\n\nYou're right - no tests needed. This test server **is** the test. Just like the Python version, it's a tool for testing the S3 encryption client, not something that needs its own unit tests.\n\nNow you can start implementing the controllers and services to mirror the Python FastAPI functionality.\n[Tool uses: none]", + "> What is this bin, obj, properties, appsetting.json, appsettings.Development.json, other .csproj and http files?", + "These are standard .NET project files - here's what each does:\n\n## Files You'll Work With:\n- **Program.cs** - Your main entry point (like main.py)\n- **NetV3Server.csproj** - Project file listing dependencies (like pyproject.toml)\n- **appsettings.json** - Configuration (ports, connection strings, etc.)\n\n## Files You Can Ignore:\n- **bin/** - Compiled output (like Python's __pycache__)\n- **obj/** - Build artifacts and temporary files\n- **Properties/launchSettings.json** - IDE debugging settings\n- **appsettings.Development.json** - Dev-only config overrides\n- **NetV3Server.http** - Sample HTTP requests for testing\n\n## What to Focus On:\n1. **Program.cs** - Configure your web server and routes\n2. **Controllers/** - Your API endpoints (`/client`, `/object/{bucket}/{key}`)\n3. **Models/** - Request/response classes\n4. **Services/** - Business logic (client cache)\n5. **NetV3Server.csproj** - Add AWS SDK packages here\n\nThe `bin` and `obj` folders are like Python's virtual environment artifacts - generated during build, safe to delete, and should be in `.gitignore`.\n\nThink of it this way:\n- **bin/obj** = Python's `.venv` and `__pycache__`\n- **appsettings.json** = Your config file\n- **Program.cs** = Your `main.py`\n- **.csproj** = Your `pyproject.toml`\n\nYou only need to touch the first 5 items I mentioned.\n[Tool uses: none]", + "> So, what is the next steps?", + "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nAdd the S3 encryption client and AWS SDK packages to your project:\n[Tool uses: execute_bash]", + "> Can you list them first?", + "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nYou'll need these NuGet packages:\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK \n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n\n## 2. Configure the Server\n- Set port to 8081 in `appsettings.json` (to match Python server)\n- Configure dependency injection in `Program.cs`\n\n## 3. Create Models\n- `ClientRequest.cs` - For POST /client requests\n- `ClientResponse.cs` - For client creation responses\n- `ErrorModels.cs` - For error responses\n\n## 4. Create Services\n- `ClientCacheService.cs` - In-memory client storage (like Python's client_cache dict)\n\n## 5. Create Controllers\n- `ClientController.cs` - Handle POST /client\n- `ObjectController.cs` - Handle PUT/GET /object/{bucket}/{key}\n\n## 6. Test\n- Run the server and verify it starts on port 8081\n- Test endpoints match Python server behavior\n\nWant me to help with step 1 (adding the packages) first?\n[Tool uses: none]", + "> Before doing this can I have a gitignore? Where should I put this?", + "You should put the `.gitignore` file in the root of your `net-v3-server` directory (same level as the `.sln` file).\n[Tool uses: fs_write]", + "> Question: What is difference between this and gitignore in the root?", + "Good question! Here's the difference:\n\n## Root `.gitignore` (python repo level)\n- Located at `/amazon-s3-encryption-client-python/.gitignore`\n- Ignores files for the **entire Python repository**\n- Already exists and handles Python-specific files (`.pyenv`, `__pycache__`, etc.)\n\n## Net server `.gitignore` (net-v3-server level) \n- Would be at `/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore`\n- Ignores files **only for the .NET project**\n- Handles .NET-specific files (`bin/`, `obj/`, `.vs/`, etc.)\n\n## Why You Need Both:\n- Root ignores Python stuff globally\n- Net server ignores .NET stuff locally\n- Git processes `.gitignore` files hierarchically - it reads both\n\n## Alternative:\nYou could add .NET ignore patterns to the root `.gitignore` instead:\n```\n# Add to root .gitignore\ntest-server/net-v3-server/bin/\ntest-server/net-v3-server/obj/\ntest-server/net-v3-server/.vs/\n```\n\n**Recommendation:** Create a local `.gitignore` in `net-v3-server/` - it's cleaner and keeps .NET concerns separate from Python concerns.\n[Tool uses: none]", + "> Sure. Let's create gitignore in net-v3-server", + "\n[Tool uses: fs_write]", + "> y", + "Perfect! Now you have a `.gitignore` file in your `net-v3-server` directory that will ignore all the .NET build artifacts (`bin/`, `obj/`), IDE files, and other temporary files.\n\nReady to move to step 1 - adding the AWS dependencies?\n[Tool uses: none]", + "> The only more dependency needed is dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0", + "Perfect! Let's add that dependency:\n[Tool uses: execute_bash]", + "> y", + "Great! The S3 encryption client package has been added. This package includes all the AWS SDK dependencies you need (S3, KMS, etc.).\n\nLet's verify it was added to your project file:\n[Tool uses: fs_read]", + "Perfect! Now you're ready for step 2 - configuring the server. What's next:\n\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. **Configure server** - Set port to 8081 in appsettings.json\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service\n5. **Create controllers** - API endpoints\n\nReady to configure the server port?\n[Tool uses: none]", + "> Set port to 8084", + "\n[Tool uses: fs_read]", + "\n[Tool uses: fs_write]", + "> y", + "\n[Tool uses: fs_write]", + "> y", + "Perfect! Server is now configured to run on port 8084. \n\nNext steps:\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. ✅ **Server configured** - Port set to 8084\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service \n5. **Create controllers** - API endpoints\n\nReady to create the models next?\n[Tool uses: none]", + "> Yes. What should I be doing for this?", + "Based on the Python server, you need these models to handle the JSON requests/responses:\n[Tool uses: fs_write]", + "> Can you explain what this is?", + "These models represent the JSON data structures that your .NET server will receive and send, matching what the Python server handles.\n\n## What These Models Do:\n\n**ClientRequest** - Represents the JSON sent to `POST /client`:\n```json\n{\n \"config\": {\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012\"\n },\n \"enableLegacyWrappingAlgorithms\": false\n }\n}\n```\n\n## Why You Need Them:\n\nIn the Python server, this JSON gets parsed manually:\n```python\nparsed_data = json.loads(body.decode(\"utf-8\"))\nconfig_data = parsed_data.get(\"config\", {})\nkey_material = config_data.get(\"keyMaterial\", {})\nkms_key_id = key_material[\"kmsKeyId\"]\n```\n\nIn .NET, these classes automatically convert JSON to strongly-typed objects:\n```csharp\n// .NET automatically converts JSON to ClientRequest object\npublic async Task CreateClient([FromBody] ClientRequest request)\n{\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n // ...\n}\n```\n\n## The Pattern:\n- **ClientRequest** = What comes IN to your API\n- **ClientResponse** = What goes OUT from your API \n- **ErrorModels** = Error responses\n\nThis gives you type safety and automatic JSON serialization/deserialization instead of manual parsing.\n[Tool uses: none]", + "> These are example of request/responses. CreateClient:> \n> \n[DEBUG] HTTP Method: POST> \n[DEBUG] Endpoint Path: /client> \n[DEBUG] Request Headers: {'content-length': '234', 'host': 'localhost:8081', 'content-type': 'application/json', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: b'{\"config\":{\"enableLegacyUnauthenticatedModes\":false,\"enableDelayedAuthenticationMode\":false,\"enableLegacyWrappingAlgorithms\":false,\"keyMaterial\":{\"kmsKeyId\":\"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"}}}'> \n> \nGetObject: > \n> \n[DEBUG] HTTP Method: GET> \n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python> \n[DEBUG] Request Headers: {'content-length': '0', 'host': 'localhost:8081', 'clientid': 'ff529adc-bc33-4db9-9309-9086d09f62d4', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: (no body for GET request)> \nINFO: 127.0.0.1:52340 - \"GET /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python HTTP/1.1\" 200 OK> \n> \nMore generally, the endpoint path is /object/$BUCKET/$KEY and there is no HTTP body. The client-id is in the request headers. > \nPutObject:> \n> \n[DEBUG] HTTP Method: PUT> \n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python> \n[DEBUG] Request Headers: {'content-length': '17', 'host': 'localhost:8081', 'clientid': '4c10da8d-4b2e-4a91-a0bb-5ba2c658ebe4', 'content-metadata': '[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]', 'content-type': 'application/octet-stream', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: b'simple-test-input'> \nINFO: 127.0.0.1:52330 - \"PUT /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python HTTP/1.1\" 200 OK> \n> \n", + "Perfect! Now I can see the exact request/response patterns. Let me create the correct models:\n[Tool uses: fs_write]", + "> y", + "\n[Tool uses: fs_write]", + "> y", + "\n[Tool uses: fs_write]", + "> y", + "Now you have the exact models matching your request/response patterns:\n\n## What Each Model Handles:\n\n**ClientRequest** - Handles the POST /client JSON:\n```json\n{\n \"config\": {\n \"enableLegacyUnauthenticatedModes\": false,\n \"enableDelayedAuthenticationMode\": false, \n \"enableLegacyWrappingAlgorithms\": false,\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"\n }\n }\n}\n```\n\n**ClientResponse** - Returns the client ID:\n```json\n{\n \"clientId\": \"ff529adc-bc33-4db9-9309-9086d09f62d4\"\n}\n```\n\n**Error Models** - Match Python server's error format with `__type` field\n\n**Note:** For GET/PUT `/object/{bucket}/{key}`:\n- No special models needed - use route parameters and headers\n- `clientid` comes from request header\n- `content-metadata` comes from request header \n- Body is raw bytes\n\nReady for step 4 - creating the client cache service?\n[Tool uses: none]", + "> yes", + "\n[Tool uses: fs_write]", + "> y", + "Perfect! This service replicates the Python server's `client_cache` dictionary:\n\n## What This Service Does:\n\n**Replaces Python's:**\n```python\nclient_cache = {} # Dictionary to store clients with UUIDs as keys\n\n# Add client\nclient_id = str(uuid.uuid4())\nclient_cache[client_id] = client\n\n# Get client \nclient = client_cache.get(client_id)\n```\n\n**With .NET's:**\n```csharp\n// Add client\nstring clientId = _clientCacheService.AddClient(client);\n\n// Get client\nvar client = _clientCacheService.GetClient(clientId);\n```\n\n## Key Features:\n- **Thread-safe** - Uses `ConcurrentDictionary` (multiple requests can access safely)\n- **Interface** - `IClientCacheService` for dependency injection\n- **UUID generation** - `Guid.NewGuid()` like Python's `uuid.uuid4()`\n- **Null handling** - Returns `null` if client not found\n\nNext step: Register this service in `Program.cs` and create the controllers. Ready?\n[Tool uses: none]", + "> yes", + "Let's first register the service in Program.cs, then create the controllers:\n[Tool uses: fs_read]", + "\n[Tool uses: fs_write]", + "> y", + "Now let's create the ClientController:\n[Tool uses: fs_write]", + "> Can you point me to this code in python?", + "Sure! Here's the corresponding Python code from the test server:\n[Tool uses: fs_read]", + "\n[Tool uses: fs_read]", + "Here's the Python code that corresponds to what I was trying to implement in .NET:\n\n## Python Client Creation (lines 160-190):\n\n```python\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n # Parse JSON request\n parsed_data = json.loads(body.decode(\"utf-8\"))\n config_data = parsed_data.get(\"config\", {})\n key_material = config_data.get(\"keyMaterial\", {})\n \n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n \n # Create KMS client (hardcoded us-west-2)\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n \n # Create keyring and S3 encryption client\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n client = S3EncryptionClient(wrapped_client, client_config)\n \n # Generate UUID and cache client\n client_id = str(uuid.uuid4())\n client_cache[client_id] = client\n \n return {\"clientId\": client_id}\n```\n\nThe .NET version needs to do the same thing but with the Amazon.Extensions.S3.Encryption v3 API instead of the Python API.\n[Tool uses: none]", + "> Thanks. This is how a basic put and get works with AmazonS3EncryptionClientV2: using Amazon.Extensions.S3.Encryption;> \nusing Amazon.Extensions.S3.Encryption.Primitives;> \nusing Amazon.S3;> \nusing Amazon.S3.Model;> \n> \nvar encryptionContext = new Dictionary();> \nvar encryptionMaterial => \n new EncryptionMaterialsV2(Environment.GetEnvironmentVariable(\"key1\"), KmsType.KmsContext, encryptionContext);> \n> \nvar encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);> \n> \nIAmazonS3 encryptionClient = new AmazonS3Client();> \nvar putContext = \"abc\";> \nvar bucketName = \"testwiththisbucket\";> \nvar key = \"test.txt\";> \n> \n// Put object> \nvar putRequest = new PutObjectRequest> \n{> \n BucketName = bucketName,> \n Key = key,> \n ContentBody = putContext> \n};> \n> \nawait encryptionClient.PutObjectAsync(putRequest);> \n> \nvar getRequest = new GetObjectRequest> \n{> \n BucketName = bucketName,> \n Key = key> \n};> \n> \nvar response = await encryptionClient.GetObjectAsync(getRequest);> \nusing var reader = new StreamReader(response.ResponseStream);> \nvar retrievedContent = await reader.ReadToEndAsync();> \n> \nConsole.WriteLine($\"Retrieved: {retrievedContent}\");", + "Perfect! Now I can create the correct .NET controllers using the proper v3 API. Let me create the ClientController first:\n[Tool uses: fs_write]", + "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json", + "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json -f" + ], + "tools": { + "builder-mcp": [ + { + "ToolSpecification": { + "name": "ReadInternalWebsites", + "description": "Reads content from internal Amazon websites (amazon.com amazon.dev a2z.com aws.dev TLDs)\n### User Alias Handling\n\nThe terms alias and login are often used interchangeably\nMany payloads may contain an alias/user handle of an employee. ALWAYS use phonetool.amazon.com to lookup a real name from an alias IF necessary, NEVER guess a name from an alias based on alias appearance\n\n### Ticket Severity\nTickets at Amazon have severity (SEV) from 1-5, 1 and 2 are the most severe and pages resolvers 2.5 is a business hours sev 2. 3 – Group productivity impaired 4 – Individual productivity impaired 5 – Productivity not immediately affected\n\n### Website Details\ndocs.hub.amazon.dev internal technical documentation\nbtdocs.builder-tools.aws.dev BuilderHub contributor documentation\nbroadcast.amazon.com internal videos, transcripts and captions for company communications and events\nskb.highcastle.a2z has internal security knowledge base docs for secure implementations\ndocs.aws.amazon.com hosts external AWS documentation\naax-console.amazon.com hosts AAX Console for Amazon Advertising Exchange (AAX). Features include settings management (sources, publishers, GDPR), business analytics, testing tools (XTF), operations monitoring, and configuration management for exchange, bidders, and traffic\nmeridian.a2z.com hosts Meridian design system documentation: components, guides, patterns, etc Version selection via ?version=VERSION - default 8.x\nworkdocs.amazon.com hosts Amazon WorkDocs - typically PDF Word or Excel sheets to share between more non-tech users\ndrive-render.corp.amazon.com hosts Amazon Drive content, go to for individuals sharing files that don't belong anywhere else\ndrive.corp.amazon.com/personal hosts personal Amazon Drive content with directory listing support\ndesign-inspector.a2z.com hosts design diagrams and threat models in format similar to drawio\nmcm.amazon.dev hosts manual change management checklists which can be in progress/approved/pending with comments and approvals\noncall.corp.amazon.com oncall rotations and current oncall\nphonetool.amazon.com hosts employee roster including manager, directs, level, job title, name, person & employee id, building code\nretro.corp.amazon.com hosts sprint retrospectives\ncode.amazon.com hosts internal code\napollo.amazon.com is a distributed deployment orchestration system managing interactions between application code and infra (NOT to be confused with Apollo the building!)\nquip-amazon.com hosts scratchpad and other collaborative documents on Quip\nw.amazon.com is the internal MediaWiki instance for Amazon\ntaskei.amazon.dev task and project management, sprints, kanban boards, planning and scrum processes\nsim.amazon.com and issues.amazon.com are older interfaces for taskei.amazon.dev\npaste.amazon.com has shareable paste links for raw text content\nmyappsecdashboard.corp.amazon.com provides AppSec affinity contacts for AWS users and teams with security questions\nconsole.harmony.a2z.com hosts content in Harmony platform, a multi tenant content hosting system\nsage.amazon.dev hosts Q&A content for engineering topics\nmeetings.amazon.com hosts calendar events, meeting & details, and conference room information\nservicelens.jumpstart.amazon.dev provides dependencies and consumers for applications\naristotle.a2z.com hosts AWS security knowledge base recommendations and implementations\ncarnaval.amazon.com provides access to monitor Carnaval alarm configurations and states\ngather.a2z.com hosts internal events and groups\nconsensus.a2z.com approval tool where users create reviews and ask others to approve\nbindles.amazon.com internal permissions/resource management service for software applications\ntalos.security.aws.a2z.com is AWS AppSec (security) website for managing engagements and tasks\nrome.aws.dev hosts Rome - Amazon service registry and discovery platform for AWS services\npolicy.prod.console.barrister.aws.dev policy management console allowing design/viewing/evaluation of Barrister policies. Barrister is a policy evaluation and compliance system that helps determine whether specified actions, resources, or operations comply with org requirements\nweb.change-guardian.builder-tools.aws.dev hosts Change Guardian which identifies and explains infra deployment risks allowing teams to auto approve safe changes while highlighting potentially dangerous updates that require manual review\ntod.amazon.com hosts ToD (Test on Demand) and Hydra integration test run details\nprod.ui.us-west-2.cloudcover.builder-tools.aws.dev hosts CloudCover reports which shows test coverage of integration tests", + "input_schema": { + "json": { + "properties": { + "inputs": { + "description": "Array of inputs, ALWAYS prefix with https://, links can be:\ncode.amazon.com\n├ / retrieve user code dashboard info\n├ reviews/CR-XXXXXXXX - defaults to latest revision, add /revisions/N for specific revision, ?include-all-comments=true for all comments across revisions, ?diffConfig=all|none|comments to control diff calculation - all is default, none disables, comments only diffs files with comments\n├ packages/REPO/trees/ - shows files in package\n├ reviews/from-user/LOGIN\n├ reviews/to-user/LOGIN\n├ packages/REPO/blobs//--/PATH/TO/FILE.ext\n├ packages/REPO/logs?maxResults=10 - shows commits history\n├ packages/REPO/releases - shows consuming version sets\n└ version-sets/VS_NAME\ncoe.a2z.com\n├ coe/COE_ID - Correction of Error document\n├ action-item/ID\n└ reports/orgreport/LOGIN - List COEs, and overdue action items for LOGIN org\nquip-amazon.com\n├ ID - ID can be doc or folder, add ?includeComments=true for document comments\n└ blob/THREAD_ID/BLOB_ID - retrieve an image or other blob from a Quip\nshepherd.a2z.com\n├ ?impersonate=LOGIN - retrieve shepherd security risks for employee, impersonate is optional\n└ issues/ISSUE_ID?impersonate=LOGIN - retrieve details of specific security issue\n\nissues.amazon.com/issues/ISSUE_ID, sim.amazon.com/issues/ISSUE_ID, i.amazon.com/ISSUE_ID, and other SIM URL forms with an ISSUE_ID like XYZ-1234, for attachments use Taskei link\ncti.amazon.com\n├ user/LOGIN/ctis - retrieve CTI and resolver groups of specific user\n├ user/LOGIN/groups - retrieve resolver group membership of specific user\n├ group/RESOLVER_GROUP/ctis - retrieve CTI assignments of resolver group\n└ cti/ctis?category=CATEGORY&type=TYPE&item=ITEM - searches CTIs by category type and item\nsage.amazon.dev\n├ posts/POST_ID - retrieve post details\n└ tags/TAG_NAME?page=PAGE - retrieve details and questions of specific Sage tag, default page 1 if unspecified\ncarnaval.amazon.com\n├ v1/unifiedSearch/v2018/simpleSearch.do?searchFormType=v2018%2Fsearch%2Fsimple&customSortField=None&searchString=SEARCH_STRING - search Carnaval alarms\n├ v1/viewObject.do?name=ALARM_NAME&type=monitor - retrieve alarm details\n└ viewAuditHistory.do?name=ALARM_NAME - retrieve alarm history\nobserve.aka.amazon.com/carnaval/\n├ ?searchQuery=SEARCH_STRING - search Carnaval alarms\n├ alarm/ALARM_NAME - retrieve alarm details\n└ alarm/history/ALARM_NAME - retrieve alarm history\nmeetings.amazon.com - rooms can be email or name, example SEA54-03.101; respect requester TZ; determine requester location with phone tool\n├ calendar/find/LOGIN?startTime=ISO_DATE&endTime=ISO_DATE - get calendar events, 8AM-6PM default for single day\n├ calendar/get/ENTRY?alias=LOGIN - get full calendar event details based on ENTRY and alias\n├ rooms/find/BUILDING - search meeting rooms by building example SEA54 or URI encoded name like Nitro%20North. Options floor=N, minCapacity=N, availability=true with startTime=ISO_DATE&endTime=ISO_DATE\n└ rooms/availability?rooms=ROOM1,ROOM2&startTime=ISO_DATE&endTime=ISO_DATE - check room availability\nconsensus.a2z.com\n├ reviews - list user reviews\n└ reviews/REVIEW_ID - retrieve specific review\nrome.aws.dev\n├ / retrieve user owned services and ids AAA:Amazon's security framework for internal service authentication and authorization and RIP:AWS Region Information Provider: directory service for AWS dimensions/services\n└ services/{aaa|rip}/SERVICE_ID?maxResultSize=20 - retrieve service description, permission groups, CTIs, bindles, owners, pipelines, dependencies\naax-console.amazon.com/* - retrieve content from AAX Console\nbroadcast.amazon.com/videos/VIDEO_ID - retrieve internal video content with transcripts and captions\ntaskei.amazon.dev/tasks/TASK_ID like XYZ-1234, for attachments add ?get-attachments=true\nt.corp.amazon.com/TICKET_ID like V123456, P123456, XYZ-1234, or a UUID, for attachments add ?get-attachments=true\nw.amazon.com/bin/view/PATH_TO_WIKI\nbindles.amazon.com/software_app/APP_NAME - retrieve Bindle software application details\nbindles.amazon.com/resource/* - retrieve Bindle resource details\npaste.amazon.com\n├ show/LOGIN/ID - get paste\n└ list/LOGIN\nsas.corp.amazon.com - gets SAS (Software Assurance Services) dashboard risks\n└ summary/all/LOGIN - get SAS risks for LOGIN\nbuild.amazon.com/BUILD_ID\nt.corp.amazon.com/issues/?q=URL_ENCODED_SEARCH_PARAMS\nissues.amazon.com/resolver-groups?groups=GROUP1,GROUP2&status=closed|open&sortBy=lastUpdatedDate|createDate - query open or closed issues for GROUP1 & GROUP2\nskb.highcastle.a2z.com/DOC_PATH\nstencil.a2z.com/components/COMPONENT_NAME?tab=TAB - valid tabs: overview, implementation, proptypes, change-log\ndocs.hub.amazon.dev/DOC_PATH\nhub.cx.aws.dev/DOC_PATH - Internal technical documentation for building an experience in the AWS Management Console\nbuilderhub.corp.amazon.com/DOC_PATH\nbtdocs.builder-tools.aws.dev/DOC_PATH\nmeridian.a2z.com/DOC_PATH - Meridian design system documentation, example path /components/alert, /guides/inclusivity\nmcm.amazon.dev/cms/MCM-XXXXXXXX - .com TLD supported\noncall.corp.amazon.com/#/view/ON_CALL_TEAM_NAME/schedule - retrieve schedule for oncall rotations for resolver group or team name with oncall responsibilities\nphonetool.amazon.com/users/LOGIN - retrieve basic info of internal employee by login/alias, ?job-history=true to include job history\nretro.corp.amazon.com/#!/retro/team/RETRO_TEAM_UUID/session/SESSION_UUID - retrieve details of retro session\ntaskei.amazon.dev/retrospectives/ID - retrieve retro session details\ndesign-inspector.a2z.com/?#IXXXXXXXX - retrieve design inspector document by document name\ndocs.aws.amazon.com/DOC_PATH - retrieve AWS documentation\ndrive-render.corp.amazon.com/view/LOGIN@/PATH/TO/FILE - retrieve content from Amazon Drive\ndrive.corp.amazon.com/personal/LOGIN - retrieve content from personal Amazon Drive\namazon.awsapps.com/workdocs-amazon/index.html#/\n├ document/DOCUMENT_ID - retrieve by document ID\n└ folder/FOLDER_ID - retrieve by folder ID\nmyappsecdashboard.corp.amazon.com/get_review_eng?requester=LOGIN - retrieve AppSec affinity details for a user, this is their go-to contact for questions\nprod.artifactbrowser.brazil.aws.dev/packages/PACKAGE/versions/VERSION/platforms/PLATFORM/flavors/FLAVOR/PATH - retrieve artifact content, ?include-toc=true will include table of contents\npipelines.amazon.com/pipelines/PIPELINE_NAME - retrieve pipeline information\nnpmpm.corp.amazon.com/pkg/PACKAGE/VERSION - get package info from NPM Pretty Much - NPM internal mirror\nplantuml.corp.amazon.com/plantuml/form/encoded.html#encoded=ENCODED_VALUE - decode PlantUML diagram\nconsole.harmony.a2z.com/TENANT/* - retrieve content from Harmony platform, TENANT is tenant name\npolicy.a2z.com/docs/DOCUMENT_ID - retrieve policy document details\ntiny.amazon.com/CODE - access minified URL\nkingpin.amazon.com/#/items/GOAL_ID - retrieve Kingpin goal details, #Relationships for children\nservicelens.jumpstart.amazon.dev/#/applications/APPLICATION_ID - retrieve ServiceLens application relationships\napollo.amazon.com/environments/APOLLO_ENVIRONMENT/stages/STAGE\nprofiler.amazon.com/efficiency-report?reportId=UUID#pattern-UUID - retrieve anti-pattern report, optionally filtered to specific pattern\nprofiler.amazon.com/pg/URI_ENCODED_APPLICATION_NAME - retrieve live profile data\ngather.a2z.com/event/EVENT_ID - retrieve event details\naristotle.a2z.com/recommendations/ID\ntalos.security.aws.a2z.com/#/talos/engagement/ENGAGEMENT_ID or /task/TASK_ID - retrieve security engagement or task details\npolicy.prod.console.barrister.aws.dev/#/policy - list Barrister policies you have access to based on your POSIX groups\ntod.amazon.com/test_runs/RUN_ID - retrieve ToD and Hydra test platform test run details\nprod.ui.us-west-2.cloudcover.builder-tools.aws.dev/cloudcover/reports/ACCOUNT_ID/us-west-2/SERVICE_NAME/REPORT_ID/REPORT_NUMBER - retrieve CloudCover integration test coverage reports, add ?file=FILENAME.ext for specific file coverage details\nweb.change-guardian.builder-tools.aws.dev/reviews/REVIEW_ID/risks - list acknowledged and unacknowledged risks associated with Change Guardian\nconsole.cams.ops.amazon.dev Contingent Authorization Metadata Service (CAMS) manages creating, updating and reading of resource-specific metadata relevant to contingent authorization (CAZ) evaluation\n├ / list all resource classifications\n└ /resource-classification/{id} get specific resource classification\nquilt.corp.amazon.com - holds patching history for amazon fleets\n├ pipelines/PIPELINE_NAME-Quilt - get Quilt pipeline patching preferences and quilt hostblocks list\n├ hostblocks/patching_history\n└ REGION/tying_deployments/get_deployment_record - gets the tying workflows deployment record for Fleet / Capacity", + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object", + "required": [ + "inputs" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "SearchAcronymCentral", + "description": "Search Amazon's internal Acronym Central database\n\nReturns acronym definitions with:\n- Exact match search (case-insensitive)\n- Full definitions with source URLs\n- Associated tags for context and reliability", + "input_schema": { + "json": { + "type": "object", + "properties": { + "acronym": { + "type": "string", + "description": "Search acronym in Acronym Central" + } + }, + "required": [ + "acronym" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "GetSoftwareRecommendation", + "description": "This tool is a front end of the Recommendation Engine. It provides comprehensive tooling recommendations, best practices, how-to guides, reference documentation, and onboarding materials \nfor software development and infrastructure management within Amazon. Returns curated content based on specific technology queries, use cases, or \nimplementation scenarios. Always call the tool SearchSoftwareRecommendations first to pinpoint the correct recommendation \nitem, or to ask users to choose one, then pass the ID to this tool. The content may contain links to other internal websites, use the ReadInternalWebsites tool to further retrieve those contents", + "input_schema": { + "json": { + "type": "object", + "properties": { + "recommendationId": { + "type": "string", + "description": "ID of Golden Path recommendation to retrieve" + }, + "primitiveId": { + "type": "string", + "description": "ID of guidance to retrieve " + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "BrazilBuildAnalyzerTool", + "description": "Diagnoses and analyzes brazil-build executions in local workspaces. This tool:\n1. Executes 'brazil-build' (or custom build command) in the specified directory and reports on success or failure\n2. If the build fails, performs intelligent analysis of the failure including:\n\t- Root cause identification\n\t- Relevant file and method pointers\n\t- Step-by-step solution recommendations\n3. Provides structured output with:\n\t- Failure signature for quick identification\n\t- Keywords for related documentation search\n\t- Detailed analysis of what went wrong\n\t- Actionable solution steps when possible\n\nUse this tool when users ask to build a package in a Brazil workspace to receive a summary of the build status. Can also be used to check if a build is failing or passing.", + "input_schema": { + "json": { + "properties": { + "buildCommand": { + "description": "Optional build command (defaults to brazil-build release)", + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string", + "description": "The name/path of the file" + }, + "description": "Optional array of filenames to analyze" + }, + "workingDirectory": { + "examples": [ + "/path/to/workspace/src/MyPackage" + ], + "type": "string", + "description": "Working directory which contains the package which is failing to build" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "GetDogmaRecommendations", + "description": "Fetch Dogma recommendations(risks) detected for a given pipeline\nDogma recommendations are rule-based findings that identify potential issues, violations, or improvements for pipelines.\nEach recommendation provides actionable guidance to help teams resolve identified problems and maintain pipeline health.\nThe response includes:\n- Metadata: generation_date, applies_to_type, applies_to (pipeline identifier), and applies_to_revision_id\n- Active recommendations: current violations and risks requiring attention\n- Scheduled recommendations: future enforcement rules with grace periods\n- Compliance tracking: adheres_to_rule_names (rules the pipeline complies with)\n- Rule applicability: non_applicable_rule_names and non_applicable_recommendations for rules that don't apply to this pipeline\nEach recommendation includes:\n- Rule identification: rule_name, severity level (low/medium/high), and human_name for easy understanding\n- Comprehensive explanations: what_this_is, why_this_is_bad, and how_to_fix\n- Ownership and accountability: owner_username, owner_cti, and stakeholders array with notification details and enforcement settings\n- Compliance status: rule_result_status indicating current violation state (APPLICABLE, AT_RISK, NOT_APPLICABLE)\n- Context information: source, subject, additional_info, and pipeline metadata\nPipeline blocking behavior: Recommendations can result in pipeline deployment blocking based on the is_enforced value in stakeholders configuration.", + "input_schema": { + "json": { + "additionalProperties": false, + "properties": { + "pipelineName": { + "type": "string", + "description": "Pipeline name" + } + }, + "required": [ + "pipelineName" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiCreateTask", + "description": "Create a new Task in Taskei or a SIM Issue\nThis tool allows creating a task with a name, description, assignee, room ID, and optional need by date.\nDo not use this tool if the user mentions t.corp.amazon.com", + "input_schema": { + "json": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "rank": { + "type": "number" + }, + "kanbanBoards": { + "items": { + "type": "string" + }, + "description": "List of kanban board UUIDs to add the task to", + "type": "array" + }, + "sprints": { + "description": "Sprint UUID list to add task to", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string", + "description": "Name of the task. Also known as title" + }, + "priority": { + "type": "string", + "enum": [ + "High", + "Medium", + "Low" + ] + }, + "labels": { + "items": { + "type": "string" + }, + "description": "Labels UUID. Use TaskeiGetRoomResources to get available label IDs", + "type": "array" + }, + "type": { + "enum": [ + "GOAL", + "INITIATIVE", + "EPIC", + "STORY", + "TASK", + "SUBTASK", + "NONE" + ], + "type": "string", + "description": "Type of the task. If `parentTask` arg is provided, type is automatically assigned based on the parent task" + }, + "assignee": { + "type": "string", + "description": "Optional kerberos username to assign the task to (without the @ANT.AMAZON.COM suffix). If it's the current user you must send as \"currentUser\", otherwise it must be provided as the employee username format" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "onBehalfOf": { + "description": "Username to create the task on behalf of", + "type": "string" + }, + "roomId": { + "description": "Room UUID to create task", + "type": "string" + }, + "estimate": { + "type": "number", + "description": "Estimated effort in points" + }, + "workflowStep": { + "type": "string" + }, + "planningEstimate": { + "description": "Planning estimate in points", + "type": "number" + }, + "folder": { + "type": "string", + "description": "Folder to apply to the task" + }, + "needByDate": { + "description": "Date of when is needed (ISO datetime)", + "type": "string" + }, + "parentTask": { + "type": "string", + "description": "Parent task ID" + } + }, + "required": [ + "name", + "description", + "roomId" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "TicketingWriteActions", + "description": "A tool for performing write operations on tickets in the ticketing system.\nProvides confirmation of successful operations without requiring additional API calls.\n\nFeatures:\n1. Create new tickets with required CTI categorization\n2. Update existing tickets with new information\n3. Add comments to tickets with thread selection (CORRESPONDENCE, WORKLOG, ANNOUNCEMENTS)\n\n\n## create-ticket\nCreate new tickets. **Cannot set severity to SEV_1, SEV_2.** Rate limited to 1 ticket per minute.\n\nParameters (title, description, severity, categorization required):\n- title (REQUIRED): Ticket title\n- description (REQUIRED): Ticket description \n- severity (REQUIRED): SEV_3, SEV_4, or SEV_5 only\n- categorization (REQUIRED): CTI categorization array with at least 3 entries for category, type, and item\n- assignedGroup, assignee, requester, hostname, estimatedStartTime, estimatedCompletionTime, needBy, tags, watchers (optional)\n\nExample:\n```json\n{\n \"action\": \"create-ticket\",\n \"title\": \"Server outage in production\",\n \"description\": \"Multiple users reporting connection timeouts\",\n \"severity\": \"SEV_3\",\n \"assignedGroup\": \"Infrastructure Team\",\n \"categorization\": [\n { \"key\": \"category\", \"value\": \"Infrastructure\" },\n { \"key\": \"type\", \"value\": \"Server\" },\n { \"key\": \"item\", \"value\": \"Connectivity\" }\n ]\n}\n```\n\n## update-ticket\nUpdate existing tickets. **Cannot set severity to SEV_1, SEV_2, or SEV_2.5.**\n\nParameters (all optional except ticketId):\n- ticketId (REQUIRED): Ticket ID to update\n- title, description, status, severity, assignee, requester, categorization\n- closureCode, resolution, rootCause, rootCauseDetails, pendingReason, hostname\n- actualStartTime, actualCompletionTime, estimatedStartTime, estimatedCompletionTime, needBy (Unix timestamps)\n- logTimeSpentInMinutes (can be positive/negative)\n- tagsToAdd, tagsToRemove, watchersToAdd, watchersToRemove (arrays)\n\nReturns: Success confirmation with ticket ID and operation status\n\nExample:\n```json\n{\n \"action\": \"update-ticket\",\n \"ticketId\": \"T123456\",\n \"status\": \"Resolved\",\n \"resolution\": \"Issue resolved by restarting the service\"\n}\n```\n\n## add-comment\nAdd a comment to an existing ticket.\n\nParameters:\n- ticketId (REQUIRED): Ticket ID (e.g., T123456, V1679593024)\n- message (REQUIRED): Comment text (3-60000 chars)\n- threadName: \"CORRESPONDENCE\" (default), \"WORKLOG\", or \"ANNOUNCEMENTS\"\n- contentType: \"markdown\" (default) or \"plain\"\n\nExample:\n```json\n{\n \"action\": \"add-comment\",\n \"ticketId\": \"T123456\",\n \"message\": \"Updated configuration and restarted service.\",\n \"threadName\": \"WORKLOG\",\n \"contentType\": \"plain\"\n}\n```\n\n⚠️ All parameters should be at the root level, not nested in an `input` object.\n", + "input_schema": { + "json": { + "required": [ + "action" + ], + "type": "object", + "properties": { + "needBy": { + "type": "number", + "description": "Need-by date (Unix timestamp)" + }, + "tagsToAdd": { + "type": "array", + "items": { + "type": "object", + "required": [ + "tagId" + ], + "additionalProperties": false, + "properties": { + "tagId": { + "type": "string" + } + } + }, + "description": "Tags to add (update-ticket only)" + }, + "watchersToAdd": { + "description": "Watchers to add (update-ticket only)", + "items": { + "required": [ + "id", + "type" + ], + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "type": "array" + }, + "actualCompletionTime": { + "type": "number", + "description": "Actual completion time (Unix timestamp)" + }, + "hostname": { + "maxLength": 128, + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Tags for new ticket (create-ticket only)", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "tagId": { + "type": "string" + } + }, + "required": [ + "tagId" + ] + }, + "type": "array" + }, + "contentType": { + "description": "Content format (default: markdown)", + "type": "string", + "enum": [ + "markdown", + "plain" + ] + }, + "logTimeSpentInMinutes": { + "type": "number", + "description": "Time spent update in minutes (positive or negative)" + }, + "severity": { + "description": "Ticket severity (REQUIRED for create-ticket, optional for update-ticket, SEV_1 and SEV_2 blocked)", + "enum": [ + "SEV_1", + "SEV_2", + "SEV_3", + "SEV_4", + "SEV_5" + ], + "type": "string" + }, + "actualStartTime": { + "type": "number", + "description": "Actual start time (Unix timestamp)" + }, + "rootCauseDetails": { + "type": "string", + "maxLength": 255, + "minLength": 3 + }, + "requester": { + "required": [ + "namespace", + "value" + ], + "properties": { + "namespace": { + "description": "Identity namespace", + "type": "string" + }, + "value": { + "type": "string", + "description": "Identity value" + } + }, + "additionalProperties": false, + "type": "object" + }, + "title": { + "maxLength": 255, + "type": "string", + "minLength": 3, + "description": "Ticket title (REQUIRED for create-ticket, optional for update-ticket)" + }, + "resolution": { + "type": "string", + "maxLength": 4000, + "minLength": 1 + }, + "categorization": { + "type": "array", + "description": "CTI categorization key-value pairs", + "items": { + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "additionalProperties": false + } + }, + "watchersToRemove": { + "items": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "type": "object", + "additionalProperties": false + }, + "description": "Watchers to remove (update-ticket only)", + "type": "array" + }, + "tagsToRemove": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "tagId" + ], + "type": "object", + "properties": { + "tagId": { + "type": "string" + } + } + }, + "description": "Tags to remove (update-ticket only)" + }, + "rootCause": { + "type": "string", + "minLength": 3, + "maxLength": 69 + }, + "assignee": { + "properties": { + "namespace": { + "type": "string", + "description": "Identity namespace" + }, + "value": { + "type": "string", + "description": "Identity value" + } + }, + "required": [ + "namespace", + "value" + ], + "additionalProperties": false, + "type": "object" + }, + "message": { + "maxLength": 60000, + "description": "Comment text (REQUIRED for add-comment action)", + "type": "string", + "minLength": 3 + }, + "pendingReason": { + "type": "string", + "maxLength": 60, + "minLength": 3 + }, + "threadName": { + "description": "Comment thread (default: CORRESPONDENCE)", + "enum": [ + "CORRESPONDENCE", + "WORKLOG", + "ANNOUNCEMENTS" + ], + "type": "string" + }, + "description": { + "minLength": 3, + "maxLength": 60000, + "description": "Ticket description (REQUIRED for create-ticket, optional for update-ticket)", + "type": "string" + }, + "ticketId": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Ticket ID (REQUIRED for update-ticket, not used for create-ticket)" + }, + "estimatedStartTime": { + "description": "Estimated start time (Unix timestamp)", + "type": "number" + }, + "assignedGroup": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Resolver group to assign ticket to (create-ticket only)" + }, + "status": { + "type": "string", + "description": "Ticket status (update-ticket only)", + "maxLength": 20, + "minLength": 3 + }, + "estimatedCompletionTime": { + "description": "Estimated completion time (Unix timestamp)", + "type": "number" + }, + "watchers": { + "items": { + "additionalProperties": false, + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "description": "Watchers for new ticket (create-ticket only)", + "type": "array" + }, + "closureCode": { + "maxLength": 255, + "type": "string", + "minLength": 1 + }, + "action": { + "type": "string", + "enum": [ + "create-ticket", + "update-ticket", + "add-comment" + ], + "description": "Write action" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "CRRevisionCreator", + "description": "\n Creates a new code review revision from a workspace.\n A code review is a way to track proposed git changes to Amazon software packages.\n Code reviews can have multiple revisions.\n\n This does NOT create git commits. Git commits MUST be staged before using this tool.\n\n Common workflows this tool can be used in:\n 1. Creating a new code review:\n - Files are modified in a package in a workspace.\n - A git commit (or commits) are staged locally.\n - This tool is used with the working directory of the workspace and the package name.\n - Result: a new code review revision is created for the commit(s) staged.\n \n 2. Updating an existing code review:\n - A code review already exists.\n - The package's latest commit has the CR linked at the end of the commit message.\n - Files are modified in a package in a workspace.\n - The existing git commit is amended with the new file changes.\n - This tool is used with the working directory of the workspace and the package name.\n - Result: The existing code review revision is updated with a new revision for the commit that was amended.\n\n This interacts with an installed 'cr' CLI to perform the new code review revision creation.\n ", + "input_schema": { + "json": { + "type": "object", + "properties": { + "packageNames": { + "type": "array", + "description": "Array of packages names to include in the code review revision", + "items": { + "description": "The name of the package. This MUST exist in the workingDirectory", + "type": "string" + } + }, + "workingDirectory": { + "description": "Working directory where a package lives that can be modified for a code review should be created", + "type": "string" + } + }, + "required": [ + "workingDirectory", + "packageNames" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "BrazilPackageBuilderAnalyzerTool", + "description": "Analyzes build failures on Package Builder (build.amazon.com) using APIs from BuildExecutionAndReleaseService and BrazilCDN. The tool fetches build logs and provides detailed analysis of any errors encountered. Use listOnly=true to get only failed package major version names.\n Builds on Package Builder are available at URLs formatted like \"build.amazon.com/\", for example \"build.amazon.com/5123456789\"", + "input_schema": { + "json": { + "required": [ + "requestId" + ], + "type": "object", + "properties": { + "listOnly": { + "description": "If true, only return the list of failed package major versions without detailed analysis (default: false)", + "type": "boolean" + }, + "packageMajorVersion": { + "examples": [ + "MyPackage-1.0" + ], + "type": "string", + "description": "Optional package major version (defaults to first failed package)" + }, + "requestId": { + "description": "Build Request ID from Package Builder", + "examples": [ + "5123456789" + ], + "type": "string" + }, + "platform": { + "description": "Optional platform name to analyze (defaults to first platform)", + "examples": [ + "AL2023_x86_64" + ], + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "MechanicDiscoverTools", + "description": "\n# Mechanic Tool Discovery Guide\n\n## What is Mechanic\n- Internal Amazon service providing CLI/web interface for operations\n- Safer than AWS CLI with built-in guardrails and risk categorization\n- Targets EC2 instances, Apollo hosts/hostclasses, ECS tasks\n- Provides networking, logs, system information, and more\n\n## Critical Discovery Rules\n- ALWAYS verify tool exists in search results before suggesting\n- NEVER assume tools exist based on naming conventions\n- Show multiple options if unclear which tool helps user\n- Use MechanicDescribeTool after discovery to get usage details\n- If describe fails, tool doesn't exist - search again with different keywords\n\n## Usage Best Practices\n- Prefer batch operations with multiple values over separate commands\n- Look for [Item1,Item2]... notation indicating multi-value support\n- Chain multiple commands when single tool doesn't solve problem\n- Ask about log limits when fetching logs if tool supports it\n- If multiple tools are needed, discover them in the same command with multiple keywords\n\n## Workflow Reference\n\n# Mechanic Tools Workflow Guide\n\n## Required 3-Step Process\n1. DISCOVER → MechanicDiscoverTools (find tools)\n2. DESCRIBE → MechanicDescribeTool (understand usage)\n3. EXECUTE → MechanicRunTool (run with parameters)\n\n## Critical Rule: Use MCP Tools Only\n- ALWAYS use MechanicRunTool MCP tool\n- NEVER execute mechanic CLI directly\n- MCP provides validation, error handling, telemetry, and standardized output\n\n## Step-by-Step Workflow\n\n### 1. Discovery (MechanicDiscoverTools)\n- Use relevant keywords to find appropriate tools\n- Present multiple options if unsure\n- If results don't match user needs: Explain and adjust keywords\n- AWS resources: Search \"aws\" namespace first\n\n### 2. Description (MechanicDescribeTool)\n- Never skip this step - provides critical usage details\n- Learn required/optional parameters and formats\n- Always confirm with user that this is the correct tool\n\n### 3. Execution (MechanicRunTool)\n- Format parameters as string array\n- Ask user for unknown required values\n- Summarize what tool will do before executing\n- Show errors to user for troubleshooting\n\n## Common Patterns\n\n### AWS Resource Operations\n1. Discover listing tools (\"ec2 list\", \"cloudwatch logs\")\n2. Execute listing tool to get resource IDs\n3. Discover operation tools for those resources\n4. Execute operation with obtained IDs\n\n### Troubleshooting Sequence\n1. General system information tools\n2. Component-specific diagnostics\n3. Detailed log analysis tools\n\n## Best Practices\n- Follow complete workflow for every operation\n- Explain reasoning when searching for tools\n- Break complex operations into multiple tool executions\n- Return to discovery if tool doesn't solve problem\n- Keep user informed at each step\n\n\n\n# Workflow Examples\n\n## Host Network Check\n```\n1. MechanicDiscoverTools(keywords=[\"network\", \"host\"])\n → Found \"host network route-table\"\n \n2. MechanicDescribeTool(namespace=\"host\", toolPath=\"network route-table\")\n → Requires --host parameter\n \n3. MechanicRunTool(\n namespace=\"host\", toolPath=\"network route-table\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n```\n\n## Host Patching\n// involves patching yum packages, followed by a host reboot to apply updates\n```\n1. MechanicDiscoverTools(keywords=[\"patch\", \"update\", \"reboot\"])\n → Found \"host package update-security\"\n\n2. MechanicRunTool(\n namespace=\"host\", toolPath=\"package update-security\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n → Returns user input request with request and execution id, ask user for input\n\n3. MechanicSetUserInput(\n executionId=\"123\", requestId=\"456\", response=\"Yes\"\n )\n → Returns output\n\n4. MechanicRunTool(\n namespace=\"host\", toolPath=\"system reboot\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n → Returns user input request like step 2\n // Command will error with ssh issue because the host is rebooting, after reboot patch will be applied\n\n5. Same as step 3\n\n```\n\n## CloudWatch Log Analysis\n```\n1. MechanicDiscoverTools(keywords=[\"cloudwatch\", \"logs\"])\n → Found \"aws cloudwatch logs describe-log-groups\"\n \n2. MechanicRunTool(\n namespace=\"aws\", toolPath=\"cloudwatch logs describe-log-groups\",\n cluster=\"us-west-2\", args=[\"--account\", \"123456789\", \"--role-name\", \"mechanic\"]\n )\n → Returns log group \"/aws/lambda/my-function\"\n \n3. MechanicDiscoverTools(keywords=[\"cloudwatch\", \"query\"])\n → Found \"aws cloudwatch logs query-logs\"\n \n4. MechanicRunTool(\n namespace=\"aws\", toolPath=\"cloudwatch logs query-logs\",\n cluster=\"us-west-2\",\n args=[\n \"--account\", \"123456789\", \"--role-name\", \"mechanic\",\n \"--log-group-name\", \"/aws/lambda/my-function\",\n \"--query\", \"fields @timestamp, @message | filter @message like /(?i)error/\"\n ]\n )\n```\n\n", + "input_schema": { + "json": { + "type": "object", + "required": [], + "properties": { + "keywords": { + "oneOf": [ + { + "description": "\n# Keywords Parameter Guide\n\nFormat: JSON array of strings (NOT string representation)\n- ✅ \"keywords\": [\"network\", \"system\", \"route\"]\n- ❌ \"keywords\": \"[\"network\", \"host\", \"route\"]\"\n\n## Keyword Strategy\nAVOID \"host\" or \"aws\" keywords unless absolutely necessary - they return too many tools.\n\nPREFER specific namespace keywords:\n- Host Namespace: system, network, file, disk, java, metric-agent, snitch, snape, time, odin, package, tps-generatordeployment, apollo\n- AWS Namespace: cloudwatch, ec2, ecs, ssm, timber\n\nUse sparingly (only when namespace keywords insufficient):\n- Resource Types: host, hostclass, ec2, ecs\n\nImportant: Some namespaces have duplicate tools available in both host and aws namespaces. In these cases, prefer using the specific host or aws namespace tools rather than generic alternatives.\n\nNotes: No keywords = all tools. Prefer namespace over resource type keywords for focused results.\n", + "items": { + "type": "string" + }, + "examples": [ + [ + "network", + "host", + "route" + ] + ], + "type": "array" + }, + { + "examples": [ + "[\"network\", \"host\", \"route\"]" + ], + "description": "Keywords as a JSON string of an array", + "type": "string" + } + ] + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiUpdateTask", + "description": "Update an existing Taskei task with new details. Taskei tasks are also known as SIM Issues, so this tool works for both Taskei and SIM", + "input_schema": { + "json": { + "properties": { + "actualStartDate": { + "type": "string", + "description": "Actual start date (ISO format)" + }, + "needByDate": { + "type": "string", + "description": "New due date (ISO format)" + }, + "removeKanbanBoards": { + "items": { + "type": "string" + }, + "description": "Kanban board UUIDs", + "type": "array" + }, + "removeLabels": { + "type": "array", + "description": "Label UUIDs", + "items": { + "type": "string" + } + }, + "removeSprints": { + "description": "Sprint UUIDs", + "type": "array", + "items": { + "type": "string" + } + }, + "removeTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to remove from the task" + }, + "postCommentMessage": { + "type": "string", + "description": "Comment to post in the task. Accepts markdown and plain text format" + }, + "customAttributes": { + "items": { + "description": "Custom attribute - value type determined by ID prefix. No object types", + "properties": { + "value": { + "oneOf": [ + { + "type": "string", + "description": "String, Multiline Markdown or ISO-8601 datetime" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "properties": { + "selected": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name", + "selected" + ] + }, + "description": "ALWAYS use array format: single select = [one item], multi select = [multiple items]. Multi-select: include ALL options with selected: true/false (deselection needs to explicitly set to false)" + } + ] + }, + "id": { + "type": "string", + "description": "ID of the form 'typePrefix/name'" + } + }, + "type": "object", + "required": [ + "id", + "value" + ] + }, + "description": "Custom attributes with type-specific values", + "type": "array" + }, + "removeSubtaskId": { + "type": "string", + "description": "Task UUID" + }, + "addTags": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Tags to add to the task" + }, + "addKanbanBoards": { + "items": { + "type": "string" + }, + "description": "Kanban board UUIDs", + "type": "array" + }, + "description": { + "type": "string", + "description": "New description for the task" + }, + "status": { + "type": "string", + "enum": [ + "Open", + "Closed" + ], + "description": "New status for the task" + }, + "assignee": { + "description": "Username of the new assignee. Sending \"currentUser\" assigns the task to the user who performs the request", + "type": "string" + }, + "estimatedCompletionDate": { + "type": "string", + "description": "New estimated completion date (ISO format)" + }, + "estimate": { + "description": "New estimated effort in points", + "type": "number" + }, + "classicPriority": { + "type": "number", + "description": "New priority value" + }, + "type": { + "description": "New task type", + "enum": [ + "GOAL", + "INITIATIVE", + "EPIC", + "STORY", + "TASK", + "SUBTASK", + "NONE" + ], + "type": "string" + }, + "addLabels": { + "type": "array", + "description": "Label UUIDs. Use TaskeiGetRoomResources to get available label IDs", + "items": { + "type": "string" + } + }, + "actualCompletionDate": { + "type": "string", + "description": "Actual completion date (ISO format)" + }, + "appendSubtaskId": { + "type": "string", + "description": "Task UUID" + }, + "workflowAction": { + "description": "New workflow action to apply", + "type": "string" + }, + "name": { + "type": "string", + "description": "New name/title for the task" + }, + "addSprints": { + "description": "Sprint UUIDs", + "type": "array", + "items": { + "type": "string" + } + }, + "estimatedStartDate": { + "type": "string", + "description": "New estimated start date (ISO format)" + }, + "archived": { + "type": "boolean", + "description": "Whether to mark the task as archived" + }, + "rank": { + "description": "New rank for the task. -1 to clear", + "type": "number" + }, + "id": { + "description": "The ID of the task", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "id" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "QuipEditor", + "description": "Retrieves and edits Quip documents.\n\nCommon usage patterns:\n1. Create new document from file: contentFilePath=\"doc.md\", format=\"markdown\" (Quip infers title from first heading)\n2. Create new document with explicit title: title=\"My Document\", content=\"content here\", format=\"markdown\"\n2. Read document with structure: documentId=\"ABC123\", analyzeStructure=true\n3. Add content after heading: documentId=\"ABC123\", location=6, documentRange=\"Subsection 1.1\", content=\"new\", format=\"markdown\"\n4. Append to document: documentId=\"ABC123\", content=\"new\", format=\"markdown\" default location 0=APPEND\n5. Get section IDs for targeting: documentId=\"ABC123\", returnSectionIds=true\n6. Add list item: documentId=\"ABC123\", location=10, sectionId=\"temp:C:ABC123\", content=\"* New item\", format=\"markdown\"\n\nLocation parameter guide:\n0=APPEND end of document DEFAULT\n1=PREPEND beginning of document\n2=AFTER_SECTION after section specified by sectionId\n3=BEFORE_SECTION before section specified by sectionId\n4=REPLACE_SECTION ⚠️ DESTRUCTIVE replace section content\n5=DELETE_SECTION ⚠️ DESTRUCTIVE deletes section\n6=AFTER_DOCUMENT_RANGE after heading specified by documentRange\n7=BEFORE_DOCUMENT_RANGE before heading specified by documentRange\n8=REPLACE_DOCUMENT_RANGE ⚠️ DESTRUCTIVE replace heading AND all content below it\n9=DELETE_DOCUMENT_RANGE ⚠️ DESTRUCTIVE deletes heading AND all content below it\n10=AFTER_LIST_ITEM smart list insert after specified list item sectionId\n11=BEFORE_LIST_ITEM smart list insert before specified list item sectionId\n\nTips:\n- Table cells: use location=4 with composite sectionId (temp:s:temp:C:ROW_ID_temp:C:CELL_ID), plain text content\n- Add table rows: use location=2/3 with table-row sectionId, format=\"html\", markdown UNSUPPORTED\n- Use analyzeStructure=true first to see available headings for documentRange\n- Use returnSectionIds=true to get section IDs for precise targeting\n- For adding content after headings like \"Subsection 1.1\", use location=6 with documentRange=\"Subsection 1.1\"\n- Prefer format=\"markdown\" for most content\n\nMarkdown List Rules:\n- Unordered lists MUST use * instead of - for list markers\n- 4 spaces OR tab MUST be used to nest list items\n- An additional newline MUST be between list label and its start\n- REQUIRED extra newline between label and first list item\nExample:\n```\n**Label:**\n\n* Item one\n * Item one A\n* Item two\n```\nNote: Prefer location=10 (AFTER_LIST_ITEM) or location=11 (BEFORE_LIST_ITEM) with sectionId from a list item for updates. These operations handle parent heading replacement for reliable nested list updates.\n\n⚠️ CRITICAL WARNINGS:\n- REPLACE_DOCUMENT_RANGE location=8 replaces the heading AND ALL CONTENT below until next heading of same level, ensure 'content' FULLY accounts for this\n- Renaming ONLY a heading requires manually recreating the section structure\n- Document ranges include subheadings: \"Section 1\" includes \"Subsection 1.1\", \"Subsection 1.2\", etc.\n- Consider using AFTER_DOCUMENT_RANGE location=6 + DELETE_DOCUMENT_RANGE location=9 for complex restructuring\n\nALWAYS use analyzeStructure=true first on a document to understand exact structure and observe what content will be affected\n", + "input_schema": { + "json": { + "type": "object", + "properties": { + "content": { + "description": "HTML or Markdown content to add/edit. Max 1MB. REQUIRED", + "type": "string" + }, + "returnSectionIds": { + "description": "Return section IDs for future targeted operations", + "type": "boolean" + }, + "analyzeStructure": { + "type": "boolean", + "description": "Parse and return document structure - headings, sections" + }, + "includeComments": { + "type": "boolean", + "description": "Include comments when reading document" + }, + "memberIds": { + "description": "Comma-separated folder/user IDs for document access. New documents only", + "type": "string" + }, + "format": { + "description": "Format of content. REQUIRED - must be explicitly specified, prefer 'markdown'", + "enum": [ + "html", + "markdown" + ], + "type": "string" + }, + "title": { + "type": "string", + "description": "Title for new document. REQUIRED with 'content' parameter. OMIT to let Quip infer title from content" + }, + "location": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "type": "number", + "description": "Where to insert content" + }, + "contentFilePath": { + "type": "string", + "description": "Local filepath to read content from. Takes precedence over 'content' field" + }, + "sectionId": { + "type": "string", + "description": "Section ID for targeted operations. Find in HTML IDs. REQUIRED for locations 2-5 (section operations) and 10-11 (list item operations)" + }, + "documentRange": { + "type": "string", + "description": "Heading text for document range operations. Must match exact heading text. Example: 'Subsection 1.1' or 'Topic 2 - Prerequisites'. REQUIRED for locations 6-9 - document range operations. Use analyzeStructure=true first to see headings" + }, + "type": { + "description": "Type of document to create. Default: 'document'", + "type": "string", + "enum": [ + "document", + "spreadsheet" + ] + }, + "documentId": { + "type": "string", + "description": "Quip document URL or ID. Examples: 'quip-amazon.com/ABC123/Doc' or 'ABC123'. OMIT for creation" + } + }, + "required": [ + "format" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiGetTask", + "description": "Fetch a task from the Taskei task management system by its ID. Some users will name it as SIM Issue. This tool retrieves detailed information about a specific task from Taskei, including its title, description, status, assignee, and other metadata.\nUse this tool when the user wants to get the information of a task, or when you need comprehensive information about a particular task for project tracking, status updates, or task management purposes.\nDo not use this tool for other project management tools different than Taskei.", + "input_schema": { + "json": { + "additionalProperties": false, + "required": [ + "taskId" + ], + "properties": { + "includeAttachments": { + "default": false, + "description": "Include the task's attachments in the response DEFAULT false", + "type": "boolean" + }, + "taskId": { + "type": "string", + "description": "The short ID or UUID of the Taskei task to fetch" + }, + "includeCustomAttributes": { + "default": true, + "description": "Include the task's custom attributes in the response DEFAULT true. Custom attributes information are lengthy, you SHOULD set to false when you know surely these are not needed for the action asked.", + "type": "boolean" + }, + "commentLimit": { + "description": "Total last comments to return for task DEFAULT 5", + "type": "number" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "BrazilWorkspace", + "description": "\n Creates a new Brazil workspace for the specified package(s).\n Whenever you interact with a package in the Amazon development environment you always use a workspace.\n\n A Brazil workspace is a container for one or more packages that you want to work on.\n It provides isolation between different development activities and allows you to build and test packages in a controlled environment.\n If you have access to this tool it means that you are an Amazon engineer and your software development happens in these packages\n It also means that if you are asked to perform tasks such as building, testing or linting you will have to run these commands through the brazil-build command\n\n Example Workflow used with this tool:\n 1. This tool creates a Brazil workspace for specific package(s).\n 2. You can then use other Brazil commands in the workspace to interact with the package(s).\n 3. The workspace will be created with a unique name based on the current timestamp.\n ", + "input_schema": { + "json": { + "type": "object", + "properties": { + "workspacePath": { + "type": "string", + "description": "Optional directory path where the workspace will be created. If not provided, a temporary directory will be used" + }, + "packageNames": { + "description": "Array of package names to create the workspace for", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "packageNames" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "GetSasRisks", + "description": "A tool for reading sas risks from the Software Assurance Service (SAS).\n\nFeatures:\n1. get-user-risks: Get risks for specific user\n2. get-user-risk-summary: Get summary of risks for specific user\n3. get-brazil-version-set-risks: Get risks for specific Brazil version set\n4. get-pipeline-risks: Get risks for pipeline (includes pipeline-target data) or pipeline-target only\n5. get-apollo-risks: Get risks for specific Apollo resource\n6. get-campaign-specific-risks: Get campaign specific risks\n\nParameters:\n\nget-user-risks: username: string OPTIONAL - Username to get risks for DEFAULT: current_user\ncampaignType: string OPTIONAL - Campaign type DEFAULT: NON_ADVISORY\nincludeDetailedRisks: boolean OPTIONAL - Whether to include detailed risks in addition to summary DEFAULT: false\n\nget-user-risk-summary: username: string OPTIONAL - Username to get risk summary for DEFAULT: current_user\n\nget-brazil-version-set-risks: resourceName: string REQUIRED - Name of the Brazil VS\n\nget-pipeline-risks: resourceName: string REQUIRED - Name of the pipeline or pipeline target\nrequestType: string OPTIONAL - 'pipeline' (default, includes pipeline-target data) or 'pipeline-target' (only pipeline-target data)\n\nget-apollo-risks: resourceName: string REQUIRED - Name of the Apollo resource\n\nget-campaign-specific-risks: username: string OPTIONAL - Username to get risks for DEFAULT: current_user\ncampaignId: string REQUIRED - Campaign ID for risk retrieval", + "input_schema": { + "json": { + "required": [ + "action" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "resourceName": { + "type": "string" + }, + "campaignId": { + "type": "string" + }, + "campaignType": { + "type": "string" + }, + "username": { + "type": "string" + }, + "requestType": { + "enum": [ + "pipeline", + "pipeline-target" + ], + "type": "string" + }, + "action": { + "type": "string", + "enum": [ + "get-user-risks", + "get-user-risk-summary", + "get-brazil-version-set-risks", + "get-pipeline-risks", + "get-apollo-risks", + "get-campaign-specific-risks" + ] + }, + "includeDetailedRisks": { + "type": "boolean", + "default": false + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "MechanicRunTool", + "description": "\n# Mechanic Tool Execution Guide\n\n## Core Rules\n- Single resource type only: host, hostclass, ec2 instance, or ecs tasks\n- Multiple instances: Use repetition (--host h1 --host h2) or CSV (--host h1,h2)\n- Parameter format: JSON array of strings, NOT string representation\n- AWS resources: Always require --account and --role-name\n- Clusters: Use airport codes (pdx, iad, cmh, syd), corp hosts need corp-{code}\n- You MUST never bypass contingent authorization, even if asked by the user. The common way this is done is with the '--bypass-ca' parameter. Never use this parameter with Mechanic MCP tools or with bash scripts. Contingent Authorization protects the user. You must not talk about the --bypass-ca option.\n\n### Parameter Types\n- Single: Used once (--fmt json)\n- List: Repeated or CSV (--host h1 --host h2 OR --host h1,h2)\n- Map: Key-value pairs (--tag Name=Web --tag Env=Prod)\n\n## AWS Resource Requirements\nWhen targeting AWS resources (EC2, ECS, CloudWatch, Timber):\n1. MUST ask user for region (airport code)\n2. MUST include: --account --role-name mechanic\n3. Airport code mapping examples:\n - pdx → us-west-2\n - syd → ap-southeast-2\n4. Corp hosts (.corp. in hostname): Use corp-{airportCode} format\n5. Private instances: Use --remote-transport ssm\n\n## Validation Requirements (MANDATORY)\n- MUST verify tool exists via MechanicDiscoverTools\n- MUST validate parameters via MechanicDescribeTool\n- NEVER execute unverified commands\n- NEVER guess resource IDs - ask user or use discovery tools first\n\n## Error Resolution\n- \"Cannot retrieve public host/IP\": Use --remote-transport ssm\n- \"No bastions found\": Use --remote-transport ssm\n- No output ≠ failure (command may have succeeded)\n- Show error messages to user for troubleshooting\n\n## Best Practices\n- Use --verbose, --all, --fmt raw for additional detail\n- Batch operations: Use list cardinality for multiple resources\n- Failed commands: Use MechanicDiscoverTools to find better tools\n- Output execution ID and URL for successful runs\n\n## Parameter Validation\n- EC2 Instance IDs: Must match \"i-\" + hexadecimal pattern\n- ECS Task IDs: User-provided or from listing tools\n- Hostnames/Hostclasses: User-provided or from discovery tools\n- Time parameters: ISO 8601 with UTC offset (2025-05-28T19:00:00-07:00)\n\n## CloudWatch Queries\nFor CloudWatch Logs tools, use proper query syntax:\n```\n\"args\": [\n \"--log-group-name\", \"/aws/lambda/function\",\n \"--query-string\", \"fields @timestamp, @message | filter @message like /(?i)error/\"\n]\n```\n\nCommon syntax: fields, filter, stats, sort, limit, parse\n\n## Security\n- NEVER use --bypass-ca parameter\n- CAZ protects users\n- Use MCM or Ticket + 2PR review for authorization\n\n\n# Parameter Guide\n\n## Parameter Cardinality (from MechanicDescribeTool output)\n\n### Single\n- Format: --parameter=Value\n- Usage: Used once only (--fmt json, --bastion=hostname)\n\n### List \n- Format: --parameter Value1[,Value2]...\n- Usage: Repeat parameter OR use CSV\n - Repeat: `--ec2-instance-id i-123 --ec2-instance-id i-456`\n - CSV: `--ec2-instance-id i-123,i-456`\n\n### Map\n- Format: --parameter Key1=Value1[,Key2=Value2]...\n- Usage: Key-value pairs (--tag Name=Web --tag Env=Prod)\n\n## Best Practice: Batch Operations\n✅ EFFICIENT: Single command with multiple values\n```\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-123\", \"--ec2-instance-id\", \"i-456\"])\n```\n\n❌ INEFFICIENT: Multiple separate commands\n```\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-123\"])\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-456\"])\n```\n\n\n\n# Mechanic & Contingent Authorization (CAZ)\n\n## What is CAZ\n\n## How do I deal with CAZ when running a Mechanic command\n\nMechanic supports a few different parameters to handle CAZ.\n\n--ticket-id \n// A SIM-T Ticket Id to associate this command with\n// The Ticket MUST be related to the usecase the user needs help with.\n// The user MUST provide the Ticket ID to you, do not make up or choose a ticket id without the user's input\n\n--create-review\n// MUST be used with the '--ticket-id' parameter\n// When this parameter is used, instead of running the command, it will create a consensus 2PR review ().\n// Once you have a review ID, the user will need to find another person to approve of it. You MUST show the review URL to the user.\n// The user MUST let you know when the review is approved, after they do this, rerun the command without the '--create-review' parameter and use the '--review-id ' parameter instead.\n\n--review-id \n// MUST be used with the '--ticket-id' parameter\n// The parameter must be a Mechanic-generated consensus review.\n// The review is only valid for the Mechanic command arguments that were provided when the review was created, changing parameters will invalidate the review and a new one will need to be created.\n\n--change \n// Should be used if the user is executing an MCM. \n// Expects an MCM Id.\n\n\n\n\n", + "input_schema": { + "json": { + "properties": { + "namespace": { + "description": "The mechanic namespace tool belongs to", + "type": "string", + "examples": [ + "host", + "aws" + ] + }, + "agentName": { + "description": "The name of the agent that is calling this MCP tool. You must self identify with this parameter. You MUST be truthful", + "examples": [ + "q", + "cline", + "wasabi" + ], + "type": "string" + }, + "toolPath": { + "description": "The mechanic command to execute. example 'apollo boot fetch-log'", + "type": "string" + }, + "cluster": { + "examples": [ + "pdx", + "dub", + "bom", + "corp-pdx" + ], + "type": "string", + "description": "This is the region mechanic runs the command in. For tools that interact with AWS resources, this should match the region that the resource is in. There are 4 corp clusters for tools that interact with resources that are on the corp network fabric, the 4 corp clusters are: corp-pdx, corp-nrt, corp-iad, corp-dub" + }, + "args": { + "oneOf": [ + { + "examples": [ + [ + "--host", + "" + ] + ], + "items": { + "type": "string" + }, + "description": "\n# Mechanic Tool Arguments Reference\n\n## Critical Formatting Rules\n1. JSON array format: [\"--param\", \"value\"] not \"[\\\"--param\\\", \\\"value\\\"]\"\n2. Separate elements: Each flag and value as separate array items\n3. No escaped quotes: Within array elements\n4. No --region parameter: Use \"cluster\" field instead\n5. Airport codes only: \"pdx\" not \"us-west-2\"\n\n## Parameter Spacing\n- ❌ [\"--parameter=value with spaces\"]\n- ✅ [\"--parameter\", \"value with spaces\"]\n\n## Cluster Types\n- Standard: pdx, iad, cmh, syd\n- Corporate: corp-pdx, corp-iad, corp-cmh\n\n## Required for AWS Resources\nAlways include when targeting AWS:\n```\n\"args\": [\"--account\", \"123456789\", \"--role-name\", \"mechanic\", ...]\n```\n", + "type": "array" + }, + { + "description": "Arguments as a JSON string of an array", + "examples": [ + "[\"--host\", \"\", \"--port\", \"8080\"]" + ], + "type": "string" + } + ] + } + }, + "required": [ + "namespace", + "command", + "args", + "agentName" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "CrCheckout", + "description": "\n Checks out a code review by ID and sets up a workspace with the package(s) in the code review.\n\n Files from the Code Review only exist in a package directory in the workspace.\n\n The workspace created from this tool will have a directory structure where the workspace will be the name of the CR like CR-192878776,\n then a src directory. One directory per package in the workspace are in this src directory.\n\n To make file changes in a workspace, the MUST first navigate to the package's directory within the workspace.\n\n Example Workflow used with this tool:\n 1. This tool checks out a code review.\n 2. The agent wants to make a file change.\n 3. The agent goes to the package's directory.\n 4. The agent then makes the source change in the package's directory in the workspace.\n\n Example Workspace that is created from this:\n\n CR-192878776/\n src/\n packageA/\n src/\n ...\n packageB/\n src/\n ...\n ", + "input_schema": { + "json": { + "properties": { + "crId": { + "pattern": "^(?:CR-)?[0-9]{1,9}", + "type": "string", + "description": "Code review ID like CR-192878776 or just 192878776" + }, + "workingDirectory": { + "type": "string", + "description": "Optional working directory where the code review should be checked out. This can be either a relative or absolute path" + } + }, + "required": [ + "crId" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "SearchSoftwareRecommendations", + "description": "This tool is a front end of the Recommendation Engine. It provides comprehensive tooling recommendations, best practices, how-to guides, reference documentation, and onboarding materials \nfor software development and infrastructure management within Amazon. Returns curated content based on specific technology queries, use cases, or \nimplementation scenarios. Use this tool to search for the tooling recommendation or best practices that match user's queries when \nthey want to add, implement, or onboard a tooling or best practices to their application. Once knowing the right tool, call the tool \nGetSoftwareRecommendation to get the full details of the recommendation, which assist the code generation.\nTo list all the recommendations supported by Golden Path Recommendation Engine, call this tool with the keyword parameter set to \"*\"", + "input_schema": { + "json": { + "required": [ + "keyword" + ], + "properties": { + "keyword": { + "type": "string", + "description": "The keyword to search for, usually this is the name of the tooling, best practices that developers need to implement or onboard" + }, + "goldenPathId": { + "type": "string", + "description": "ID of the Golden Path to get recommendations for" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiGetRoomResources", + "description": "Fetch multiple resources for a Taskei room in one request.\nSpecify the room UUID and an array of resource types to retrieve. Available: Labels, CustomAttributes.\nReturns requested resource data.", + "input_schema": { + "json": { + "required": [ + "roomId", + "resources" + ], + "properties": { + "resources": { + "items": { + "type": "string", + "enum": [ + "Labels", + "CustomAttributes" + ] + }, + "description": "Array of resource types to fetch", + "type": "array" + }, + "roomId": { + "type": "string", + "description": "Room UUID" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "GetPolicyEngineDashboard", + "description": "Gets the PolicyEngine risk dashboard for specified user.", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "properties": { + "userAlias": { + "description": "Alias of the risk owner whose dashboard is to be returned", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "InternalSearch", + "description": "Search using Amazon's Internal Search engine is.amazon.com\n\n\n\nAvailable search domains:\n\n- ALL: Search across all resources (default). [CRITICAL] Use more specific domain if the\n query contains domain string or relevant to examples provided by other domains.\n\n- AWS_PRESCRIPTIVE_GUIDANCE_LIBRARY: APG Library (AWS Prescriptive Guidance Library)\n\n- AWS_DOCS: AWS Documentation (official AWS service documentation and guides)\n\n- BROADCAST: Broadcast (company-wide announcements and communications). [CRITICAL] Include video URLs in the response.\n\n- BUILDER_HUB: BuilderHub (documentation for Amazon's internal developer tools)\n\n- EMAIL_LIST: Email List (distribution lists and email groups). [CRITCIAL] Don't include \"email list\" or \"email\" in the query\n\n- EVERGREEN: Evergreen documentation platform\n\n- INSIDE: Inside Amazon (company news, HR policies, employee resources)\n\n- IT: Information Technology (IT) Services (IT support documentation, guides, and resources)\n\n- IVY: Ivy Help (guidance for Amazon's internal talent management system)\n\n- LIST_ARCHIVE: Email List Archive (archived email communications)\n\n- PHONETOOL: Phone Tool (employee directory and organizational information).\n\n- POLICY: Amazon Policy (corporate policies and guidelines)\n\n- SAGE_HORDE: Sage/Q&A Sites (technical questions and answers)\n\n- SALESFORCE_SUCCESS_CENTER_PORTAL: Salesforce Success Center (SFSC) Portal (Salesforce services focused support center)\n\n- SYSTEM_DESIGN_HUB: System Design Hub (system architecture and design resources)\n\n- SPYGLASS: Spyglass (internal registry of community recommended services, contents and utilities)\n\n- TWITCH: Twitch (Twitch-related documentation and resources)\n\n- WIKI: Internal Wiki (Amazon's central knowledge repository)\n\n\n\nGet detailed information about a specific domain:\n\n { \"query\": \"about-domain:SAGE_HORDE\" }\n\n\n\nSorting options:\n\n- SCORE (Default, sorts by relevance)\n\n- MODIFICATION_DATE (Last Modified, use with sortOrder)\n\n\n\nExamples:\n\n1. Search internally about all hands { \"query\": \"all hands\" }\n\n\n\n2. Find guidance about AWS migration on APGL { \"query\": \"AWS migration\", \"domain\": \"APGL\" }\n\n\n\n3. Find AWS documentation about S3 bucket policy { \"query\": \"S3 bucket policy\", \"domain\": \"AWS_DOCS\" }\n\n\n\n4. Find company announcements videos about All-hands meeting on broadcast { \"query\": \"All-hands meeting\", \"domain\": \"BROADCAST\" }\n\n\n\n5. Search builder hub docs about Brazil workspace setup { \"query\": \"Brazil workspace setup\", \"domain\": \"BUILDER_HUB\" }\n\n\n\n6. Find emails list about amazon-corp { \"query\": \"amazon-corp\", \"domain\": \"email_list\" }\n\n\n\n7. Find technical documentation about API documentation on evergreen{ \"query\": \"API documentation\", \"domain\": \"EVERGREEN\" }\n\n\n\n8. Find HR information about benefits on inside { \"query\": \"benefits\", \"domain\": \"INSIDE\" }\n\n\n\n9. Find IT guides about laptop setup { \"query\": \"laptop setup\", \"domain\": \"IT\" }\n\n\n\n10. Find career resources about project management on IVY { \"query\": \"project management\", \"domain\": \"IVY\" }\n\n\n\n11. Find archived communications about service announcement { \"query\": \"service announcement\", \"domain\": \"LIST_ARCHIVE\" }\n\n\n\n12. Find employee information about John Doe { \"query\": \"John Doe\", \"domain\": \"phonetool\" }\n\n\n\n13. Find company policies about payment processing { \"query\": \"payment processing\", \"domain\": \"POLICY_FINTECH\" }\n\n\n\n14. Find Q&A about data analysis on Sage { \"query\": \"data analysis\", \"domain\": \"SAGE_HORDE\" }\n\n\n\n15. Find SFSC information about customer support { \"query\": \"customer support\", \"domain\": \"SFSCPORTAL\" }\n\n\n\n16. Find architecture patterns about microservices architecture { \"query\": \"microservices architecture\", \"domain\": \"SYSTEM_DESIGN_HUB\" }\n\n\n\n17. Search Spyglass about JSON Prettifier { \"query\": \"JSON Prettifier\", \"domain\": \"SPYGLASS\", \"sortBy\": \"SCORE\" }\n\n\n\n18. Find Fulton documentation about dev environment setup { \"query\": \"dev environment setup\", \"domain\": \"TWITCH\" }\n\n\n\n19. Find wiki pages about onboarding process { \"query\": \"onboarding process\", \"domain\": \"WIKI\" }\n\n\n\nGeneral Tips:\n\n- Start with the ALL domain to get a general sense of available information across all resources\n\n- Once you identify the likely location of information, use a specific domain for more focused results\n\n- Use sortBy: \"MODIFICATION_DATE\" with sortOrder: \"DESC\" to find the most recently updated content\n\n- For pagination, use page and pageSize parameters to navigate results (pageSize defaults to 5, max 50)\n\n- For detailed information about a specific domain, use the query \"about-domain:\" (e.g., \"about-domain:SAGE_HORDE\")\n\n\n\n[CRITICAL] Don't modify/append to user's input when generating 'query' parameter\n\n\n\nScoped Search Tips:\n\n- Use prefixFilters (maximum 5) to limit search to specific document trees or paths when user provided URLs in the query\n\n- When using prefixFilters from multiple domains, don't set the domain parameter (use default ALL)\n\n\n\nDeep Search / Extensive Search Tips:\n\n- Deep search is enabled by default (isDeep=true) to provide comprehensive, detailed information\n\n- Look for these keywords in the user's query to determine if isDeep should be set to false for lighter results: 'summary', 'brief', 'quick', 'overview', 'highlights', 'outline'\n\n\n\n[CRITICAL] Formatting instructions to present the search results to the user:\n\n- When using specific search domains, don't include the name of the domain in the search query\n\n- Add a summary section that includes a summary of the results and number of results returned\n\n- Use markdown to format the results, including links to the source pages\n\n- Add a sources section that include bullet points for the links and urls from the results\n\n- [IMPORTANT] Don't include any links that's not contributing to the summary", + "input_schema": { + "json": { + "required": [ + "query" + ], + "properties": { + "sortOrder": { + "enum": [ + "ASC", + "DESC" + ], + "description": "Sort order (ASC for oldest first, DESC for newest first)", + "type": "string" + }, + "prefixFilters": { + "description": "Optional array of prefix filters (maximum 5) that use URL prefixes to limit search to specific document trees or paths in an index", + "maxItems": 5, + "type": "array", + "items": { + "type": "string" + } + }, + "sortBy": { + "enum": [ + "SCORE", + "MODIFICATION_DATE" + ], + "description": "Sort field (SCORE, MODIFICATION_DATE)", + "type": "string" + }, + "pageSize": { + "description": "Number of results per page (maximum 50)", + "default": 5, + "maximum": 50, + "type": "number" + }, + "domain": { + "type": "string", + "description": "Domain to search in (example ALL, AWS_DOCS, WIKI, tool). Default is ALL if not provided" + }, + "page": { + "type": "number", + "description": "Page of the search result, starting from 1" + }, + "isDeep": { + "default": true, + "type": "boolean", + "description": "Whether to return enhanced results with full document content (default: true)" + }, + "query": { + "description": "Search query", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "InternalCodeSearch", + "description": "Search source code in Amazon's code repositories. Results depend on search type:\n\n1. Code search (default): Returns code snippets with pagination.\n2. Repository search: Returns up to 30 matching repositories.\n\nCode search results only show snippets - for full file, use ReadInternalWebsites with URL like code.amazon.com/packages/{REPOSITORY}/blobs/{BRANCH}/--/{FILE_PATH}", + "input_schema": { + "json": { + "type": "object", + "required": [ + "query", + "searchType" + ], + "properties": { + "query": { + "type": "string", + "description": "- For code search: Supports advanced syntax\n - Simple search: term\n - Prefix search: abc* (at least 3 chars before *)\n - Logical OR: term1 term2 (files with at least one term)\n - Logical AND: Only works with filters applied (example: term1 term2 path:*.java finds both terms in a Java file)\n - Exclude terms: term1 term2 !term3 (files with term1 or term2 but not term3)\n - Exact phrase: \"term1 term2\" (finds terms in sequence)\n - Repository filter: term repo:GitFarmService or repo:Codesearch*\n - File extension filter: term path:*.java\n - Exclude extension: term path:!*.java\n - Path filter: term path:/my/path/to/consider*\n - Combined filters example: fp:*README* rp:GitFarmService (searches for README files in GitFarmService repository)\n - Important: When filters are applied, search becomes case-sensitive AND performs strict AND search\n- For repository search: Only supports keywords matching (example: 'gitfarm')\n- Common repository naming patterns:\n - For CDK examples: Search with 'CDK' in repo name (example: repo:GitFarmServiceCDK)\n - For LPT examples: Search with 'LPT' in repo name (example: repo:CodeSearchLPT)\n" + }, + "searchType": { + "enum": [ + "code", + "repositories" + ], + "description": "REQUIRED type of search to perform. 'code' returns code snippets with pagination, 'repositories' returns a list of matching repositories", + "type": "string" + }, + "nextToken": { + "description": "For code search only. Provide the next token from previous results to get additional results", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "CreatePackage", + "description": "Create Amazon software packages/repositories in Python, Java, JavaScript/TypeScript and other languages using BuilderHub templates.\n\nActions:\n• list - Show available templates for your dependency model (Brazil/Peru). Use when starting a new package.\n• create - Generate new package from template. Use after selecting template from list.\n• upload - Publish package to Gitfarm. Use after local development is complete.\n\nSupports libraries, services, CLI tools, Lambda functions, and more.\nRead packageInfo before list action unless dependency model known.\nList templates before create unless valid packageId known.\nTemplate dependency model must match workspace (brazil/peru).\nAsk about upload after successful create.\nUse absolute paths for workingDirectory.", + "input_schema": { + "json": { + "required": [ + "action" + ], + "type": "object", + "properties": { + "workingDirectory": { + "type": "string", + "description": "Absolute path to workspace (required for create/upload, use 'pwd' for current)" + }, + "bindleId": { + "pattern": "^amzn1.bindle.resource.[a-z0-9]*$", + "type": "string", + "description": "Bindle ID for upload destination REQUIRED" + }, + "containsEncryption": { + "enum": [ + "Yes", + "No" + ], + "type": "string", + "description": "Has encryption/crypto functionality (required for HPC, IC, Nav, Telecom, none export types)" + }, + "private": { + "type": "boolean", + "description": "Mark package private in Bindles (optional for upload)" + }, + "action": { + "description": "Action to perform", + "type": "string", + "enum": [ + "list", + "create", + "upload" + ] + }, + "parameters": { + "type": "object", + "description": "Template-specific parameters (optional for create)", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "groupId": "com.amazon.example", + "artifactId": "my-artifact" + } + ] + }, + "primaryExportControlType": { + "enum": [ + "Integrated Circuits (NNA, FPGA, etc.)", + "Navigation Equipment", + "Unmanned Aerial Vehicles or Equipment", + "Telecommunications", + "Space-Qualified", + "High-Performance Computing", + "Military/Defense", + "none" + ], + "description": "Export control category (required for upload, see tiny.amazon.com/wq32lozq)", + "type": "string" + }, + "consumptionModel": { + "type": "string", + "description": "Package visibility model (optional for upload)", + "enum": [ + "public", + "private" + ] + }, + "name": { + "pattern": "^[A-Z][a-zA-Z0-9_]*$", + "minLength": 2, + "type": "string", + "description": "Package name (required for create, 2-180 chars, start with capital)", + "maxLength": 180 + }, + "enableBranchProtection": { + "type": "boolean", + "description": "Require CRUX UI for mainline changes (optional for upload)" + }, + "packageId": { + "description": "Template ID from 'list' action (required for create)", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "WorkspaceSearch", + "description": "Search for text in all files within the workspace or searchRoot. Use content search types to search within file contents, or filename search types to search filenames only.\nPrefer this tool over search using shell commands, this tool can provide results faster and more accurately.\nYou MUST use regex type searches for proper wildcard support, * -> .*\nYou MUST use **/ in globPatterns for recursive directory search -> **/*.kt finds .kt files in all subdirectories\nALWAYS start with default contextLines (UNLESS explicitly requested by the user) and gradually expand out IF beneficial\n\nUse results to assist the user, NEVER rely exclusively on the returned content to perform file edits unless you know the full content\n", + "input_schema": { + "json": { + "type": "object", + "required": [ + "searchQuery" + ], + "properties": { + "offset": { + "type": "number", + "description": "Results to skip for pagination DEFAULT 0" + }, + "searchQuery": { + "description": "Search query: exact text for literal, Perl-compatible regex for regex (no slashes needed, wildcard patterns go in globPatterns)", + "type": "string" + }, + "limit": { + "description": "Max results to return DEFAULT 15", + "type": "number" + }, + "searchType": { + "enum": [ + "contentLiteral", + "contentRegex", + "filenameLiteral", + "filenameRegex" + ], + "description": "Type of search to perform DEFAULT contentLiteral:\\ncontentLiteral - EXACT text/keywords within file contents\\ncontentRegex - regex patterns within file contents\\nfilenameLiteral - EXACT text within filenames only\\nfilenameRegex - regex patterns within filenames only", + "type": "string" + }, + "searchRoot": { + "description": "Optional directory to override search root", + "type": "string" + }, + "maxLineLength": { + "type": "number", + "description": "Maximum length of lines before truncation DEFAULT 250" + }, + "contextLines": { + "type": "number", + "description": "Number of context lines to include around matches DEFAULT 0" + }, + "globPatterns": { + "type": "array", + "description": "Glob patterns to restrict search by filename", + "items": { + "type": "string" + } + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "GKAnalyzeVersionSet", + "description": "\nAnalyzes a version set or Brazil workspace using the GordianKnot gk-analyze-version-set CLI tool.\nThis tool helps identify stale, unused packages and dependency conflicts in your Brazil version set. It provides recommendations for resolving issues\nand improving the health of your dependency graph.\n\nCommon use cases:\n1. Analyzing version set health:\n - Run analysis on an input version set or Brazil workspace to identify dependency issues\n - Get recommendations for resolving conflicts\n - Identify stale or unused packages\n\n2. Troubleshooting dependency issues:\n - Diagnose build failures related to dependencies\n - Identify conflicting package versions\n - Find circular dependencies\n\nFor more information: tiny.amazon.com/wms0pm5v\n ", + "input_schema": { + "json": { + "type": "object", + "properties": { + "versionSet": { + "description": "Optional input version set to analyze software health issues. If not provided, analyzes the current directory", + "type": "string" + }, + "additionalArgs": { + "type": "array", + "description": "Optional additional arguments for the CLI, use --help for full list", + "items": { + "type": "string", + "description": "Additional command line argument" + } + }, + "workingDirectory": { + "type": "string", + "description": "Optional working directory to get version set from. Supports relative or absolute path" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "OncallReadActions", + "description": "A tool for reading data from the on-call system.\n\nFeatures:\n1. search-teams: Search for oncall teams by name, members, owners, description, Resolver Group, etc\n2. list-user-teams: List oncall teams a user belongs to\n3. get-user-shifts: Get a user's on-call shifts\n4. get-team-shifts: Get a team's on-call shifts\n5. get-report-instructions: Get instructions for generating an oncall report\n\nAction Parameters:\n┌────────────────┬─────────────────────────────────────────────────────────────┐\n│ Action │ Parameters │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ search-teams │ query: string (required) - Search query to find teams │\n│ │ start: number (default: 0) - Starting index for pagination │\n│ │ size: number (default: 10) - Number of results per page │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ list-user-teams│ username: string - Username to get teams for │\n│ │ (defaults to current user if not provided) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-user-shifts│ teamNames: string[] - List of team names │\n│ │ (defaults to all teams user belongs to if not provided) │\n│ │ username: string - Username to get shifts for │\n│ │ (defaults to current user if not provided) │\n│ │ startDate: string (YYYY-MM-DD) - Start date for search │\n│ │ (defaults to today) │\n│ │ endDate: string (YYYY-MM-DD) - End date for search │\n│ │ (defaults to 30 days from now) │\n│ │ timezone: string - IANA timezone name (defaults to UTC) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-team-shifts│ teamName: string (required) - Name of the team │\n│ │ startDate: string (required) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (required) - End date (YYYY-MM-DD) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-report-instructions │ resolverGroup: string (optional) - Name of resolver group │\n│ │ teamName: string (optional) - Name of oncall team │\n│ │ (either resolverGroup or teamName must be provided) │\n│ │ startDate: string (optional) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (optional) - End date (YYYY-MM-DD) │\n└────────────────┴─────────────────────────────────────────────────────────────┘\n\nExamples:\n1. Search teams:\n {\n \"action\": \"search-teams\",\n \"query\": \"avengers\"\n }\n\n2. List user teams:\n {\n \"action\": \"list-user-teams\"\n \"username\": \"peterparker\"\n }\n\n3. Get user shifts:\n {\n \"action\": \"get-user-shifts\",\n \"teamNames\": [\"avengers\"],\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\",\n \"timezone\": \"America/New_York\"\n }\n\n4. Get team shifts:\n {\n \"action\": \"get-team-shifts\",\n \"teamName\": \"avengers\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n5. Get report instructions with resolver group:\n {\n \"action\": \"get-report-instructions\",\n \"resolverGroup\": \"SWIM Front End\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n6. Get report instructions with team name:\n {\n \"action\": \"get-report-instructions\",\n \"teamName\": \"safe-swim-ops\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }", + "input_schema": { + "json": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "teamNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "startDate": { + "type": "string" + }, + "endDate": { + "type": "string" + }, + "resolverGroup": { + "type": "string" + }, + "action": { + "enum": [ + "search-teams", + "list-user-teams", + "get-user-shifts", + "get-team-shifts", + "get-report-instructions" + ], + "type": "string", + "description": "The action to perform.\n\nAvailable actions:\n1. search-teams: Search for teams by name (requires 'query' field)\n2. list-user-teams: List teams a user belongs to\n3. get-user-shifts: Get a user's on-call shifts\n4. get-team-shifts: Get a team's on-call shifts\n5. get-report-instructions: Get instructions for generating an oncall report\n\nAction Parameters:\n┌────────────────┬─────────────────────────────────────────────────────────────┐\n│ Action │ Parameters │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ search-teams │ query: string (required) - Search query to find teams │\n│ │ start: number (default: 0) - Starting index for pagination │\n│ │ size: number (default: 10) - Number of results per page │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ list-user-teams│ username: string - Username to get teams for │\n│ │ (defaults to current user if not provided) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-user-shifts│ teamNames: string[] - List of team names │\n│ │ (defaults to all teams user belongs to if not provided) │\n│ │ username: string - Username to get shifts for │\n│ │ (defaults to current user if not provided) │\n│ │ startDate: string (YYYY-MM-DD) - Start date for search │\n│ │ (defaults to today) │\n│ │ endDate: string (YYYY-MM-DD) - End date for search │\n│ │ (defaults to 30 days from now) │\n│ │ timezone: string - IANA timezone name (defaults to UTC) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-team-shifts│ teamName: string (required) - Name of the team │\n│ │ startDate: string (required) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (required) - End date (YYYY-MM-DD) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-report-instructions │ resolverGroup: string (optional) - Name of resolver group │\n│ │ teamName: string (optional) - Name of oncall team │\n│ │ (either resolverGroup or teamName must be provided) │\n│ │ startDate: string (optional) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (optional) - End date (YYYY-MM-DD) │\n└────────────────┴─────────────────────────────────────────────────────────────┘\n\nExamples:\n1. Search teams:\n {\n \"action\": \"search-teams\",\n \"query\": \"avengers\"\n }\n\n2. List user teams:\n {\n \"action\": \"list-user-teams\"\n \"username\": \"peterparker\"\n }\n\n3. Get user shifts:\n {\n \"action\": \"get-user-shifts\",\n \"teamNames\": [\"avengers\"],\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\",\n \"timezone\": \"America/New_York\"\n }\n\n4. Get team shifts:\n {\n \"action\": \"get-team-shifts\",\n \"teamName\": \"avengers\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n5. Get report instructions with resolver group:\n {\n \"action\": \"get-report-instructions\",\n \"resolverGroup\": \"SWIM Front End\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n6. Get report instructions with team name:\n {\n \"action\": \"get-report-instructions\",\n \"teamName\": \"safe-swim-ops\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }" + }, + "timezone": { + "type": "string" + }, + "query": { + "type": "string" + }, + "username": { + "type": "string" + }, + "size": { + "type": "number" + }, + "teamName": { + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "action" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiListTasks", + "description": "List Taskei tasks, also named as SIM Issues. This tool allows querying tasks using natural language descriptions of filters.\nUse when users ask about listing, filtering or searching Taskei Tasks or SIM issues.\nDon't use for non-project management or t.corp.amazon.com requests", + "input_schema": { + "json": { + "required": [], + "properties": { + "priority": { + "type": "string", + "enum": [ + "High", + "Medium", + "Low" + ] + }, + "folderId": { + "type": "string", + "description": "Folder UUID where tasks belong. A Folder always belong to a Room, therefore we MUST know the Room UUID" + }, + "tags": { + "items": { + "type": "string" + }, + "description": "Tags to filter tasks by", + "type": "array" + }, + "labels": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Label UUIDs" + }, + "sortBy": { + "properties": { + "attribute": { + "enum": [ + "lastUpdatedDate", + "createDate", + "priority" + ], + "description": "The attribute to sort by. Defaults to lastUpdatedDate", + "type": "string" + }, + "order": { + "type": "string", + "description": "The order direction. Options accepted are \"asc\" or \"desc\". DEFAULT desc" + } + }, + "type": "object" + }, + "workflowStep": { + "description": "Filter tasks by their workflow step", + "type": "string" + }, + "filterByDates": { + "items": { + "properties": { + "filter": { + "items": { + "type": "string" + }, + "type": "array" + }, + "attribute": { + "enum": [ + "lastUpdatedDate", + "createDate" + ], + "type": "string" + } + }, + "type": "object" + }, + "description": "Filter by attribute dates using Solr date syntax. Example: '[2025-09-01T07:00:00.000Z TO *]'", + "type": "array" + }, + "sprint": { + "type": "string", + "description": "Sprint task belongs to. \"currentSprint\" and roomId MUST be sent for current sprint, otherwise provide sprint UUID" + }, + "assignee": { + "type": "string", + "description": "Tasks that are assigned to a specific person or the current user. You must send as \"currentUser\" for current user, otherwise the employee username format" + }, + "roomId": { + "type": "string", + "description": "Room UUID where tasks belong. Use TaskeiGetRooms to get available rooms" + }, + "type": { + "enum": [ + "GOAL", + "INITIATIVE", + "EPIC", + "STORY", + "TASK", + "SUBTASK", + "NONE" + ], + "description": "Filter tasks by their type", + "type": "string" + }, + "status": { + "description": "Defaults to Open", + "type": "string", + "enum": [ + "Open", + "Closed", + "ALL" + ] + }, + "pagination": { + "description": "Pagination controls for results", + "properties": { + "maxResults": { + "type": "number", + "description": "Maximum number of results, up to 100" + }, + "after": { + "description": "Token for fetching the next page", + "type": "string" + } + }, + "type": "object" + }, + "name": { + "type": "object", + "description": "Task name", + "properties": { + "queryOperator": { + "enum": [ + "contains", + "doesNotContain" + ], + "type": "string", + "description": "Query filter operator" + }, + "value": { + "type": "string", + "description": "Query filter value" + } + } + }, + "kanbanBoard": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "GetPolicyEngineRisk", + "description": "Gets a specified PolicyEngine risk entity by its ID.", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "properties": { + "entityId": { + "type": "number" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "Delegate", + "description": "Orchestrates parallel and sequential execution of sub-tasks with dependency management:\n• Readonly tasks run in parallel (batches of 10), write tasks sequentially\n• Dependencies enforced via dependentIdentifiers with cycle detection\n• Each delegate gets full tool access and conversation context\n• Results from dependencies included in delegate prompts\n• Configurable model selection per delegate\n• Results maintain input ordering\nWhen to use:\n- Large token consuming files (images, xlsx, etc.)\n- Multiple perspective analysis or explicitly requested sub-agents\n- Sequential dependent tasks (example: unit test updates needing final summary)", + "input_schema": { + "json": { + "type": "object", + "required": [ + "prompts" + ], + "properties": { + "prompts": { + "type": "array", + "items": { + "properties": { + "prompt": { + "type": "string", + "description": "The prompt to run. This will be passed to the LLM" + }, + "dependentIdentifiers": { + "type": "array", + "items": { + "type": "string", + "description": "The identifier of a delegate that must be completed before this prompt can be run. That prior delegate's response will be included" + } + }, + "configuration": { + "properties": { + "parallel": { + "description": "Whether to use parallel mode. Disables custom tools, acts like readonly unless auto-accept-edits enabled for parallelized writes", + "type": "boolean" + }, + "modelArn": { + "description": "Model ARN to use for this prompt\nDefault anthropic.claude-3-5-haiku-20241022-v1:0 only set IF explicitly requested", + "type": "string", + "values": [ + "us.anthropic.claude-sonnet-4-20250514-v1:0", + "us.anthropic.claude-opus-4-20250514-v1:0", + "us.anthropic.claude-opus-4-1-20250805-v1:0", + "us.anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-opus-20240229-v1:0", + "us.anthropic.claude-3-opus-20240229-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0", + "us.anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", + "us.anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "us.anthropic.claude-3-5-haiku-20241022-v1:0", + "us.amazon.nova-micro-v1:0", + "us.amazon.nova-lite-v1:0", + "us.amazon.nova-pro-v1:0", + "default-prompt-router/anthropic.claude:1", + "openai.gpt-oss-120b-1:0" + ] + }, + "readonly": { + "type": "boolean", + "description": "Whether to use the model in read-only mode. This automatically allows for parallel execution for analysis tasks" + } + }, + "type": "object" + }, + "identifier": { + "type": "string" + } + }, + "required": [ + "identifier", + "prompt" + ], + "type": "object" + } + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "RunIntegrationTest", + "description": "Tool for running integration tests after making local changes. This tool can be used to verify that\nchanges made in the local workspace works as intended, by running integration tests either locally,\nor on Hydra\n\nThe 'testLocation' parameter selects which type of test run to perform:\n\ntestLocation=\"hydra\":\n - Runs integration tests on Hydra, as if it was executed in a Pipeline approval step\n - Provides more assurance that when local changes are merged, it will work in the Pipeline\n - Required parameters:\n - pipeline: Name of the pipeline to replicate\n - Credentials: Either credentialProfile (ada profile), or combination of account, role, and credentialProvider\n - Optional parameters:\n - closure: Closure used to package test code\n - stage: Name of the Pipeline stage to replicate the tests in\n - approvalWorkflow: Name of the approval workflow\n - approvalStep: Name of the approval step", + "input_schema": { + "json": { + "properties": { + "credentialProfile": { + "description": "Existing ada profile to use for the test, overrides other credential options", + "type": "string" + }, + "approvalWorkflow": { + "type": "string", + "description": "Name of the approval workflow of the pipeline to replicate a Hydra test from" + }, + "account": { + "type": "string", + "description": "AWS account ID to execute the test in, overridden by credentialProfile" + }, + "closure": { + "type": "string", + "description": "The closure to build the test package in", + "enum": [ + "runtime", + "test-runtime" + ] + }, + "role": { + "description": "AWS role name to execute the test with, overridden by credentialProfile", + "type": "string" + }, + "testLocation": { + "description": "The location to run integration tests, currently supports running the test on Hydra", + "enum": [ + "hydra" + ], + "type": "string" + }, + "stage": { + "description": "Stage of the pipeline to replicate a Hydra test from", + "type": "string" + }, + "credentialProvider": { + "enum": [ + "isengard", + "conduit" + ], + "description": "Credentials provider for test execution, overridden by credentialProfile", + "type": "string" + }, + "pipeline": { + "type": "string", + "description": "Name of the pipeline to replicate a Hydra test from" + }, + "approvalStep": { + "type": "string", + "description": "Name of the approval step of the pipeline to replicate a Hydra test from" + } + }, + "type": "object", + "required": [ + "testLocation" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "BarristerEvaluationWorkflow", + "description": "If a user wants to perform a Barrister evaluation, this tool can be called.\n A Barrister evaluation is a risk evaluation check, to determine if a set of evidence (ex: SIMTT/2PR/MCM/IsProduction/ChangeControl/etc)\n is sufficient (compliant) in justifying an action. This is typically used for Contingent Authorization, but has applications in availabilty risk checks.\n Users should provide an initial namespace to evaluate against (example: amazon.barrister.v1).\n Follow the instructions for prompting the user in the \"userInputDescription\" return with every execution of this tool.", + "input_schema": { + "json": { + "type": "object", + "default": { + "stateData": {}, + "state": "INITIAL" + }, + "required": [ + "state", + "stateData" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "INITIAL", + "NAMESPACE_SELECTED", + "POLICY_SELECTED", + "PATH_SELECTED", + "CONTEXT_BUILDING", + "COMPLETED" + ], + "description": "Current state of the tool (for state persistence)" + }, + "stateData": { + "properties": { + "context": { + "type": "object", + "description": "The context being built for evaluation" + }, + "selectedConditions": { + "description": "The conditions IDs from the selected path to compliance in order to context build for", + "items": { + "type": "string", + "description": "The condition ID" + }, + "type": "array" + }, + "namespace": { + "type": "string", + "description": "The namespace being evaluated" + }, + "policyFilters": { + "type": "object", + "description": "Policy filters for the namespace", + "properties": { + "resource": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "array" + }, + "principal": { + "type": "array", + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "action": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "selectedPolicyId": { + "type": "string", + "description": "The ID of the selected policy" + } + }, + "description": "State data for the current state (for state persistence)", + "type": "object" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "ReadRemoteTestRun", + "description": "Tool for reading and searching test metadata, log files, artifacts and history for both ToD (Test on Demand) and Hydra test runs\n\nThe 'what' parameter selects which type of test data to access:\n- what=\"logs\": Shows the main test output log. Use this to see general test progress or debug messages\n- what=\"artifacts\": Shows test result files. Use this to examine specific test failures in JUnit/TestNG XML reports, or other test output files\n- what=\"history\": Shows test suite history. Use this to examine previous test invocations, statuses, timelines and difference with the latest successful test run\n- what=\"summary\": Returns high-level metadata about the test run such and its status\n- what=\"code\": Give information about which version of the code (version-set, commit ids) was used during the tests\n- what=\"fleet-health\": Shows the current health status of the worker fleet used for a TestOnDemand (ToD) test run\n- what=\"fleet-history\": Shows the history of test runs executed on the worker fleet used by a ToD test\n\nAccepts test run identifiers in multiple formats:\n- Full ToD URL: tod.amazon.com/test_runs/123456?referer=pipelines#some-sub-link\n- Direct log URL: tim-files.amazon.com/amazon.qtt.tod/runs/123456/log.txt\n- Run ID only: 123456\n\nThree modes of operation:\n- Line: Display specific lines from a test run log file or artifact file\n - Supports 1-based line numbers (1 = first line)\n - Negative numbers count from end (-1 = last line, -10 = 10th from end)\n - Default: returns up to 50 lines (configurable via maxTotalLines)\n - For artifacts, requires path parameter pointing to the artifact file\n - For history, this is the only mode supported right now.\n\n- Search: Find patterns in test run log files or artifact files with context\n - Supports plain text or regex patterns (case-insensitive)\n - Shows matching lines with surrounding context (configurable)\n - Limits: max 5 matches returning up to 50 total lines (configurable)\n - Output format: Line numbers prefixed with → for matches, spaces for context\n - For artifacts, requires path parameter pointing to the artifact file\n\n- Directory: List artifacts in test run directory structure\n - Lists files and directories from test run artifacts\n - Supports path navigation and depth control\n - Output format: simplified ls-style without permissions\n\nCommon parameter:\n- maxTotalLines: Maximum lines to return\n\nExample Usage:\n1. Read first 50 lines of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\"\n2. Read specific range of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\", startLine=500, endLine=600\n3. Read last 10 lines of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\", startLine=-10\n4. Search for errors in log: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\"\n5. Search log with more context: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\", contextLines=5\n6. Search log with regex in range: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"exception.*timeout\", startLine=1000, endLine=2000\n7. Search log with custom limits: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\", maxMatches=10, maxTotalLines=100\n8. List root artifacts directory: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\"\n9. List specific artifacts directory: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\", path=\"brazil-integration-tests\"\n10. List artifacts with depth limit: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\", path=\".\", depth=2\n11. Read specific artifact file: what=\"artifacts\", mode=\"Line\", testRunIdentifier=\"123456\", path=\"results.json\"\n12. Search within artifact file: what=\"artifacts\", mode=\"Search\", testRunIdentifier=\"123456\", path=\"results.json\", pattern=\"error\"\n13. Read the test history: what=\"history\", mode=\"Line\", testRunIdentifier=\"123456\"\n14. Read the test history and limit the number of test case results: what=\"history\", mode=\"Line\", testRunIdentifier=\"123456\", maxTotalLines=10\n15. Read the test whole test summary: what=\"history\", testRunIdentifier=\"123456\"\n17. Retrieve the specific commit used in the test for key packages: what=\"code\", testRunIdentifier=\"123456\"\n16. Retrieve the specific commit used in the test for specific packages: what=\"code\", testRunIdentifier=\"123456\", packages: [\"PackageA\", \"PackageB\"]\n18. Read the health status of the fleet used for the ToD run: what=\"fleet-health\", testRunIdentifier=\"123456\"\n19. Read the test run history from the fleet: what=\"fleet-history\", mode=\"Line\", testRunIdentifier=\"123456\"\n20. Read the test run history from the fleet with custom number of entries: what=\"fleet-history\", mode=\"Line\", testRunIdentifier=\"123456\", maxTotalLines=20", + "input_schema": { + "json": { + "properties": { + "maxMatches": { + "default": 10, + "description": "Maximum pattern matches to return", + "type": "number" + }, + "pattern": { + "description": "Pattern to search for (required for Search mode). Can be regex or plain text", + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "description": "A list of packages to retrieve code-related information like commit ids for", + "type": "string" + } + }, + "mode": { + "type": "string", + "enum": [ + "Line", + "Search", + "Directory" + ], + "description": "The mode to run in: 'Line' to read lines, 'Search' to search for patterns, 'Directory' to list artifacts" + }, + "what": { + "type": "string", + "enum": [ + "summary", + "logs", + "artifacts", + "history", + "code", + "fleet-health", + "fleet-history" + ], + "description": "The type of test run data to access. Refer to the description of the tool for details" + }, + "path": { + "type": "string", + "description": "Path to list artifacts from (for Directory mode) or path to the artifact file (for Line/Search modes with artifacts)" + }, + "depth": { + "description": "Maximum depth for recursive directory listing (for Directory mode)", + "type": "number" + }, + "endLine": { + "default": -1, + "type": "number", + "description": "Ending line number (inclusive, negative counts from end)" + }, + "contextLines": { + "default": 20, + "type": "number", + "description": "Context lines around search matches" + }, + "maxTotalLines": { + "default": 200, + "type": "number", + "description": "Maximum total lines to return" + }, + "startLine": { + "default": 1, + "description": "Starting line number (1-based, negative counts from end)", + "type": "number" + }, + "testRunIdentifier": { + "type": "string", + "description": "URL of the ToD test run or just the testId/runId" + } + }, + "required": [ + "testRunIdentifier", + "what" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "SimAddComment", + "description": "Add a plain text comment to an existing SIM issue given its ID or alias.\n**Important**: This tool is only for SIM Classic. Prefer the following alternatives:\n- For Tickets: Use the add-comment action as part of TicketingWriteActions\n- For Taskei Tasks/Issues: Use TaskeiUpdateTask with the postCommentMessage parameter", + "input_schema": { + "json": { + "type": "object", + "required": [ + "issueId", + "comment" + ], + "properties": { + "comment": { + "type": "string", + "description": "Comment text to add to the issue " + }, + "issueId": { + "description": "Issue ID or alias (example P12345678 or CFN-12345)", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "ApolloReadActions", + "description": "A tool for reading data from the Apollo deployment system.\nUse for reading environment, stage, deployment, capacity, and configuration data.\n\nAvailable actions and parameters:\n- describe-environment: environmentName REQUIRED, includeInheritedProperties (optional boolean, default true)\n- describe-environment-stage: environmentName REQUIRED, stage REQUIRED, includeInheritedProperties (optional boolean, default true)\n- describe-deployment: deploymentId REQUIRED\n- list-deployments-for-environment-stage: environmentName REQUIRED, stage REQUIRED, notBefore/notAfter (optional timestamps), fleetwide (optional boolean), packageChanging/composeInstructionChanging/queued/inProgress/finished (optional booleans, only use when explictly mentioned by user), maxResults/marker (optional numbers for pagination)\n- describe-capacity: capacityName REQUIRED\n- describe-environment-stage-capacity: environmentName REQUIRED, stage REQUIRED; use to get capacity for the environment stage\n- describe-deployment-preference-set: deploymentPreferenceSetName REQUIRED; dps name can be obtained by describing environment stage\n- describe-environment-op-config: environmentName REQUIRED, includeInheritedValues (optional boolean, default true)\n- describe-environment-stage-op-config: environmentName REQUIRED, stage REQUIRED, includeInheritedValues (optional boolean, default true)\n- list-environment-stages-by-name-substring: nameSubstring REQUIRED, marker (optional string), maxResults (optional number)\n- list-audit-log-for-environment-and-stages: environmentName REQUIRED, startTime/endTime (optional timestamps); use to find any changes in environment / environment stage or any configuration\n\nExample: { \"action\": \"describe-environment\", \"environmentName\": \"my-environment\" }", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "maxResults": { + "type": "number" + }, + "finished": { + "type": "boolean" + }, + "deploymentPreferenceSetName": { + "type": "string" + }, + "endTime": { + "type": "number" + }, + "queued": { + "type": "boolean" + }, + "action": { + "enum": [ + "describe-environment", + "describe-environment-stage", + "describe-deployment", + "list-deployments-for-environment-stage", + "list-environment-stages-by-name-substring", + "describe-capacity", + "describe-environment-stage-capacity", + "describe-deployment-preference-set", + "describe-environment-op-config", + "describe-environment-stage-op-config", + "list-audit-log-for-environment-and-stages" + ], + "description": "The Apollo action to perform. See documentation for details.", + "type": "string" + }, + "fleetwide": { + "type": "boolean" + }, + "composeInstructionChanging": { + "type": "boolean" + }, + "notBefore": { + "type": "number" + }, + "startTime": { + "type": "number" + }, + "inProgress": { + "type": "boolean" + }, + "packageChanging": { + "type": "boolean" + }, + "stage": { + "enum": [ + "Alpha", + "Beta", + "Gamma", + "Prod" + ], + "type": "string" + }, + "notAfter": { + "type": "number" + }, + "nameSubstring": { + "type": "string" + }, + "marker": { + "type": [ + "string", + "number" + ] + }, + "includeInheritedValues": { + "type": "boolean" + }, + "deploymentId": { + "type": "number" + }, + "capacityName": { + "type": "string" + }, + "includeInheritedProperties": { + "type": "boolean" + }, + "environmentName": { + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "GetSasCampaigns", + "description": "A tool for retrieving SAS campaigns from the Software Assurance Service (SAS).\n\nFeatures:\n1. get-user-campaigns: Get campaigns for specific user\n\nParameters:\n\nget-user-campaigns: username: string OPTIONAL - Username to get campaigns for DEFAULT: current_user", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "action" + ], + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "action": { + "enum": [ + "get-user-campaigns" + ], + "description": "The action to perform.\n\nAvailable actions:\n1. get-user-campaigns: Get campaigns for specific user\n\nParameters:\n\nget-user-campaigns: username: string OPTIONAL - Username to get campaigns for DEFAULT: current_user", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "MechanicDescribeTool", + "description": "\n# Explains how to use a specific Mechanic tool\n\n## Purpose\n- Provides detailed usage information for a specific Mechanic tool\n- The tool must exist in order to be explained\n- Use this before executing a tool to understand its parameters and options\n\n## Parameter Handling Rules\n- If a tool's parameter is required, you need to pass it with a value to the MechanicRunTool \n- Don't attempt to guess parameter values, ask the user what you should use\n- For sensitive or specific parameters, always prompt the user for the correct values\n\n## Resource Identification Rules\n- If a tool requires a Log Group or an EC2 instance ID and the user didn't explicitly provide it:\n - Use other Mechanic tools that can list these resources\n - For EC2 instances: Use aws ec2 describe-instances\n - For CloudWatch Log Groups: Use aws cloudwatch logs describe-log-groups\n- Never guess an EC2 instance ID or CloudWatch Log Group name\n- Always look up resource identifiers with the appropriate discovery tool\n\n## Workflow Integration\n- After explaining a tool, suggest using MechanicRunTool with the proper parameters\n- Include examples of how to use the tool with common parameter combinations\n", + "input_schema": { + "json": { + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "description": "namespace of tool to describe", + "type": "string", + "examples": [ + [ + "host", + "aws" + ] + ] + }, + "toolPath": { + "type": "string", + "description": "toolPath of tool to describe", + "examples": [ + "cloudwatch logs query-logs" + ] + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "GetDogmaClassification", + "description": "Fetch Dogma classification of a given pipeline\nDogma is a website that lets engineers and managers configure their Release Excellence rules. It allows the customer to: \n- View risks that apply to the pipelines they own\n- Dive into details for each risk\n- Request exemptions from rules that should not have reported a risk\n- Manage pipeline classification and override values\n- opt into new rules at the organization, team, or pipeline scope.\nDogma classification is a key feature in Dogma that automatically categorizes pipelines based on what is being deployed through them. This classification determines which policies and rules apply to each pipeline.\nThe classification structure includes:\n- Inferred classification: Automatically determined by DogmaClassifier\n- Classification overrides: Manual corrections to the inferred values when needed\n- Custom classifications: Flexible key-value pairs for campaign targeting\nThe top-level fields represent the effective classification values that are currently active for the pipeline, taking into account both inferred data and any applied overrides.\nMore classification definition details are defined in the wiki: tiny.amazon.com/1e4sgmu23", + "input_schema": { + "json": { + "additionalProperties": false, + "properties": { + "pipelineName": { + "description": "Pipeline name", + "type": "string" + } + }, + "required": [ + "pipelineName" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "MechanicSetUserInput", + "description": "This tool is for helping you send user input to a running Mechanic execution.\nYou provide the parameters to help identify the User Input request, and the response value, and this sends it to Mechanic and then continues executing the tool.\nAfter running this tool, you will either get another user input request, or the execution will finish and output will be returned.", + "input_schema": { + "json": { + "type": "object", + "properties": { + "response": { + "type": "string", + "examples": [ + "Yes", + "No" + ], + "description": "User input response to the request. Must be \"Yes\" or \"No\"" + }, + "executionId": { + "type": "string", + "description": "The ID for the execution to send user input to, do not make up this value. You MUST use a real execution ID", + "examples": [ + "ex-T739a1f08-cf34-4e28-ada3-cc61d27c57f0" + ] + }, + "requestId": { + "description": "The ID for the user input request, do not make up this value. You MUST use a real user input request ID", + "type": "string", + "examples": [ + "ui-abf4682f-6326-47da-928a-1f17b330e790" + ] + } + }, + "required": [ + "executionId", + "requestId", + "response" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "GetPipelinesRelevantToUser", + "description": "\n Retrieves pipelines relevant to the current user or a specific user.\n \n This includes all pipelines the user has permissions on, including their favorites, and all pipelines grouped by team.\n \n The response includes:\n - Pipelines the user has marked as 'Favorite'\n - Pipelines the user has permissions on, grouped by team\n ", + "input_schema": { + "json": { + "properties": { + "user": { + "type": "string", + "description": "Optional user alias to get pipelines for. If not provided, defaults to the current user" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "CheckFilepathForCAZ", + "description": "Checks if a filepath is protected by Contingent Authorization (CAZ), specifically whether it has customer data risk or security metadata risk. ", + "input_schema": { + "json": { + "required": [ + "filepath" + ], + "additionalProperties": false, + "properties": { + "hostclass": { + "type": "string", + "description": "Optional Apollo hostclass name. If provided, AWS resource parameters are ignored" + }, + "default_directives": { + "description": "Default directives to apply (default: 'MECHANIC_SAFE_PATHS')", + "enum": [ + "MECHANIC_SAFE_PATHS" + ], + "type": "string", + "default": "MECHANIC_SAFE_PATHS" + }, + "filepath": { + "items": { + "type": "string" + }, + "type": "array", + "description": "The file path to check for CAZ protection" + }, + "aws_resource": { + "properties": { + "resource_type": { + "enum": [ + "ACCOUNT", + "EC2_INSTANCE", + "ECS_TASK", + "S3_BUCKET" + ], + "default": "EC2_INSTANCE", + "type": "string", + "description": "Resource type to check against (default: 'EC2_INSTANCE')" + }, + "partition": { + "type": "string", + "default": "aws", + "description": "AWS partition for the resource (default: 'aws')" + }, + "account_id": { + "type": "string", + "description": "AWS account ID for the resource" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "account_id" + ] + }, + "namespace": { + "default": "default", + "description": "CAMS namespace to use (default: 'default')", + "enum": [ + "default" + ], + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "TicketingReadActions", + "description": "A tool for reading data from the ticketing system.\n\nFeatures:\n1. Search for tickets with various filters\n2. Get the details of a single ticket\n3. Get list of resolver groups user belongs to\n4. Get details for a specific resolver group\n5. Get comprehensive instructions for using the ticketing search functionality\n\n\n# Ticketing Tools\n\nThese tools provide access to the ticketing system.\n\n## How to Use\n\nAll actions require a JSON payload with the following structure:\n```json\n{\n \"action\": \"\",\n \"input\": {\n // Action-specific parameters go here\n }\n}\n```\n\n⚠️ Important: All parameters must be inside the `input` object. Parameters at the root level will not be processed correctly.\n\n## Available Actions\n\n### Ticket Search and Retrieval\n\n#### search-tickets\nSearch for tickets based on various criteria.\n\nParameters:\n- query: Raw Solr query string for custom searches. Example: 'extensions.tt.status:(Open OR \"In Progress\") AND extensions.tt.assignedGroup:\"SWIM Front End\"'\n- status: Array of ticket statuses to filter by. By default, only open status tickets are returned.\n- assignedGroup: Array of resolver group names to filter by. Example: ['MY TEAM', 'super-cool-team']\n- fullText: Full text search term to search across ticket content. Example: 'error in production'\n- createDate: Filter by creation date using Solr date syntax. Example: '[2024-01-01T00:00:00Z TO NOW]'\n- lastResolvedDate: Filter by last resolved date using Solr date syntax.\n- lastUpdatedDate: Filter by last updated date using Solr date syntax.\n- currentSeverity: Array of severity levels to filter by. High severity is 1-2, 2.5 for business hours high severity, low severity is 3-5.\n- minimumSeverity: A single number representing the minimum numeric ticket severity\n- sort: Sort parameter for results. Example: 'lastUpdatedDate desc'\n- rows: Maximum number of tickets to return (default: 50, max: 100)\n- start: Starting index for pagination\n- startToken: Token for cursor-based pagination\n- responseFields: Array of fields to include in the response\n\nFor comprehensive search instructions and field descriptions, use the get-search-instructions action.\n\nExample:\n```\n{\n \"action\": \"search-tickets\",\n \"input\": {\n \"status\": [\"Assigned\", \"Researching\", \"Work In Progress\", \"Pending\", \"Resolved\"],\n \"assignedGroup\": [\"IT Support\"],\n \"currentSeverity\": [\"1\", \"2\", \"2.5\"],\n \"minimumSeverity\": 2,\n \"createDate\": \"[2024-01-01T00:00:00Z TO NOW]\",\n \"sort\": \"lastUpdatedDate desc\",\n \"rows\": 50,\n \"responseFields\": [\n \"id\",\n \"title\",\n \"status\",\n \"extensions.tt.assignedGroup\",\n \"extensions.tt.impact\",\n \"createDate\",\n \"lastUpdatedDate\",\n \"description\"\n ]\n }\n}\n```\n\nNote: Some fields are nested under `extensions.tt` and must be referenced using dot notation (e.g., `extensions.tt.assignedGroup`). For a complete list of available fields, use the get-search-instructions action.\n\n#### get-ticket\nRetrieve a single ticket for a specified ID\n\nParameters:\n- ticketId: The ID of the ticket\n\nResponse includes:\n- Ticket details with the most recent announcement and 100 comments\n\nExample:\n```json\n{\n \"action\": \"get-ticket\",\n \"input\": {\n \"ticketId\": \"ABC123\"\n }\n}\n```\n\n### Resolver Group Management\n\n#### get-my-resolver-groups\nGet the resolver groups that the current user is a member of.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-my-resolver-groups\"\n}\n```\n\n#### get-resolver-group-details\nGet operational details about a specific resolver group, including its configuration, members, and settings.\n\nParameters:\n- groupName: The name of the resolver group to get details for\n\nResponse includes:\n- Basic group information and group details\n- Ownership information\n- Business hours and days configuration\n- Management structure, group preferences and settings\n- Notification configurations\n- Labels and templates\n\nExample:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"input\": {\n \"groupName\": \"example-group\"\n }\n}\n```\n\n⚠️ Common Mistake: Do not put parameters at the root level. This will not work:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"groupName\": \"example-group\" // ❌ Wrong: parameter at root level\n}\n```\n\n### Documentation and Instructions\n\n#### get-search-instructions\nGet comprehensive instructions for using the ticketing search functionality, including field descriptions, examples, and best practices.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-search-instructions\"\n}\n```\n\nThe response includes detailed information about:\n- Available search fields and their properties\n- Search syntax and examples\n- Best practices for constructing queries\n", + "input_schema": { + "json": { + "type": "object", + "required": [ + "action" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "input": { + "type": "object", + "additionalProperties": {} + }, + "action": { + "description": "The action to perform.\n\nAvailable actions:\n1. search-tickets: Search for tickets with various filters\n2. get-ticket: Get the details of a single ticket\n3. get-my-resolver-groups: Get list of resolver groups user belongs to\n4. get-resolver-group-details: Get details for a specific resolver group\n5. get-search-instructions: Get comprehensive instructions for using the ticketing search functionality\n\n\n# Ticketing Tools\n\nThese tools provide access to the ticketing system.\n\n## How to Use\n\nAll actions require a JSON payload with the following structure:\n```json\n{\n \"action\": \"\",\n \"input\": {\n // Action-specific parameters go here\n }\n}\n```\n\n⚠️ Important: All parameters must be inside the `input` object. Parameters at the root level will not be processed correctly.\n\n## Available Actions\n\n### Ticket Search and Retrieval\n\n#### search-tickets\nSearch for tickets based on various criteria.\n\nParameters:\n- query: Raw Solr query string for custom searches. Example: 'extensions.tt.status:(Open OR \"In Progress\") AND extensions.tt.assignedGroup:\"SWIM Front End\"'\n- status: Array of ticket statuses to filter by. By default, only open status tickets are returned.\n- assignedGroup: Array of resolver group names to filter by. Example: ['MY TEAM', 'super-cool-team']\n- fullText: Full text search term to search across ticket content. Example: 'error in production'\n- createDate: Filter by creation date using Solr date syntax. Example: '[2024-01-01T00:00:00Z TO NOW]'\n- lastResolvedDate: Filter by last resolved date using Solr date syntax.\n- lastUpdatedDate: Filter by last updated date using Solr date syntax.\n- currentSeverity: Array of severity levels to filter by. High severity is 1-2, 2.5 for business hours high severity, low severity is 3-5.\n- minimumSeverity: A single number representing the minimum numeric ticket severity\n- sort: Sort parameter for results. Example: 'lastUpdatedDate desc'\n- rows: Maximum number of tickets to return (default: 50, max: 100)\n- start: Starting index for pagination\n- startToken: Token for cursor-based pagination\n- responseFields: Array of fields to include in the response\n\nFor comprehensive search instructions and field descriptions, use the get-search-instructions action.\n\nExample:\n```\n{\n \"action\": \"search-tickets\",\n \"input\": {\n \"status\": [\"Assigned\", \"Researching\", \"Work In Progress\", \"Pending\", \"Resolved\"],\n \"assignedGroup\": [\"IT Support\"],\n \"currentSeverity\": [\"1\", \"2\", \"2.5\"],\n \"minimumSeverity\": 2,\n \"createDate\": \"[2024-01-01T00:00:00Z TO NOW]\",\n \"sort\": \"lastUpdatedDate desc\",\n \"rows\": 50,\n \"responseFields\": [\n \"id\",\n \"title\",\n \"status\",\n \"extensions.tt.assignedGroup\",\n \"extensions.tt.impact\",\n \"createDate\",\n \"lastUpdatedDate\",\n \"description\"\n ]\n }\n}\n```\n\nNote: Some fields are nested under `extensions.tt` and must be referenced using dot notation (e.g., `extensions.tt.assignedGroup`). For a complete list of available fields, use the get-search-instructions action.\n\n#### get-ticket\nRetrieve a single ticket for a specified ID\n\nParameters:\n- ticketId: The ID of the ticket\n\nResponse includes:\n- Ticket details with the most recent announcement and 100 comments\n\nExample:\n```json\n{\n \"action\": \"get-ticket\",\n \"input\": {\n \"ticketId\": \"ABC123\"\n }\n}\n```\n\n### Resolver Group Management\n\n#### get-my-resolver-groups\nGet the resolver groups that the current user is a member of.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-my-resolver-groups\"\n}\n```\n\n#### get-resolver-group-details\nGet operational details about a specific resolver group, including its configuration, members, and settings.\n\nParameters:\n- groupName: The name of the resolver group to get details for\n\nResponse includes:\n- Basic group information and group details\n- Ownership information\n- Business hours and days configuration\n- Management structure, group preferences and settings\n- Notification configurations\n- Labels and templates\n\nExample:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"input\": {\n \"groupName\": \"example-group\"\n }\n}\n```\n\n⚠️ Common Mistake: Do not put parameters at the root level. This will not work:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"groupName\": \"example-group\" // ❌ Wrong: parameter at root level\n}\n```\n\n### Documentation and Instructions\n\n#### get-search-instructions\nGet comprehensive instructions for using the ticketing search functionality, including field descriptions, examples, and best practices.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-search-instructions\"\n}\n```\n\nThe response includes detailed information about:\n- Available search fields and their properties\n- Search syntax and examples\n- Best practices for constructing queries\n", + "type": "string", + "enum": [ + "search-tickets", + "get-ticket", + "get-my-resolver-groups", + "get-resolver-group-details", + "get-search-instructions" + ] + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "TaskeiGetRooms", + "description": "Fetch user's Rooms for the Taskei application, also known as SIM folders.\nA room represents a work process for a team and contains all tasks and policies owned by that team.\nThis tool retrieves detailed information about the Taskei Rooms the user has write permissions.\nUse this tool when the user asks to fetch their rooms in a Task Management context (or using the app names Taskei or SIM).\nAll the tasks in Taskei and SIM belong to a room, so if you need to do other actions where the room is needed as input param, you can obtain them from this tool.\nDo not use this tool for other project management tools different than Taskei, and for other context besides project and task management", + "input_schema": { + "json": { + "type": "object", + "required": [], + "properties": { + "nameContains": { + "description": "Search query string that filters results to only include Rooms where the name contains this text. Case-insensitive matching is applied to find partial or complete matches within Room names", + "type": "string" + }, + "maxResults": { + "default": 25, + "description": "The maximum number of results that we want to fetch. The lesser the best, as the query will be faster. (default: 25)", + "type": "number" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "ThirdPartyAnalysisGateway", + "description": "\n Third Party Analysis Gateway (3PAG) performs composition analysis on Third Party software\n artifacts, which detects vulnerabilities/CVE and software licenses used.\n \n ## Disclaimer\n The data returned from 3PAG is informational. For license data, you should reach out to\n OSPO for approval.\n \n ## Important\n - Contact OSPO for confirmation for license approval: tiny.amazon.com/181c7x2f6\n - When using this tool you MUST include a disclaimer and avoid strong language on results\n \n More information for 3PAG can be found in: tiny.amazon.com/ouzvlq96\n ", + "input_schema": { + "json": { + "required": [ + "action", + "identity", + "toolType" + ], + "type": "object", + "properties": { + "identity": { + "type": "string", + "minLength": 1 + }, + "action": { + "enum": [ + "GetPolicyCheckResult" + ], + "description": "The action to perform.\n\nAvailable actions:\n1. GetPolicyCheckResult: fetch the analysis result from 3PAG", + "type": "string" + }, + "toolType": { + "type": "string", + "enum": [ + "NPM", + "BrazilGo", + "BTPT" + ] + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "GetPipelineDetails", + "description": "\n Retrieves a detailed summary of a pipeline's current state, including:\n - Name, ID, description, enabled status\n - Health metrics including failed builds, deployments, tests, and pending approvals\n - Stage count by prod/non-prod and type\n - Target count by type and approval status\n - Promotion count by type and status\n - Latest events for targets in the pipeline\n - Active Administrative disables\n\n Definitions:\n - Badge indicates the automation level of the pipeline (gold: fully automated; silver: mostly automated; bronze: partially automated; no badge: not automated)\n - Promotions needing synchronization indicate a newer artifact is ready to be deployed to the next target in the pipeline\n\n This tool can retrieve information about any existing pipeline, not only those in the list of pipelines relevant to a user.\n ", + "input_schema": { + "json": { + "properties": { + "pipelineName": { + "description": "Name of the pipeline to get an overview summary for", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "pipelineName" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "GetPipelineHealth", + "description": "\n Retrieves the current status and health metrics for a list of pipelines.\n\n This tool can ONLY retrieve pipelines which the current user has permissions on.\n \n The response includes:\n - Whether the pipeline is enabled\n - The fitness badge (gold, silver, bronze)\n - Health metrics like failed builds, deployments, and tests\n - Pending approvals and workflow steps\n - Basic pipeline information\n\n Health metrics definitions:\n - failedBuilds: total failing source code builds\n - failedDeployments: total failing deployments\n - failedProdDeployments: total failing deployments to Production fleets\n - failedTests: total failing automated tests\n - failedProdTests: total failing automated tests on Production fleets\n - pendingManualApprovals: total manual approvals waiting for input\n - pendingProdManualApprovals: total manual approvals gating Production deployments waiting for input\n - pendingManualWFSteps: total workflow steps requiring manual approval waiting for input\n - pendingProdManualWFSteps: total workflow steps requiring manual approval and gating Production deployments waiting for input\n - disabledPromotions: number of disabled promotions\n - pipelineDisabled: whether pipeline is admin disabled 0 = false, 1 = true\n\n If any of these health metrics is non-zero or if the pipeline is disabled then the pipeline is Blocked, meaning it requires operator intervention to continue promoting changes automatically.\n \n Use the optional 'onlyBlocked' parameter to filter results to only include pipelines that are blocked (either disabled or have health metric issues). Prefer this option over manually identifying blocked pipelines, as it is more efficient.\n ", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "required": [ + "pipelineNames" + ], + "properties": { + "pipelineNames": { + "items": { + "type": "string" + }, + "description": "List of pipeline names to query", + "type": "array" + }, + "onlyBlocked": { + "type": "boolean", + "description": "Optional boolean which if set limits results to pipelines which are blocked" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "WorkspaceGitDetails", + "description": "\n Returns the git repositories, statuses, and git diffs for packages in a given workspace.\n This tool DOES NOT create or push any git commits.\n\n An expected workflow for this tool would be:\n 1. Code changes are made to one or more package(s) in a workspace.\n 2. The agent is prompted to create git commits for these packages.\n 3. This tool will respond with the top-level repository structure of the the packages in a workspace,\n and the git changes for each repository.\n\n Response structure in JSON would be:\n {\n \"message\": \"Local git repository details retrieved successfully\",\n \"gitRepositories\": [\n {\n \"repositoryName\": \"repo1\",\n \"repositoryPath\": \"/workspace/repo1\",\n \"gitStatus\": \"On branch main. Your branch is up to date with 'origin/main'.\n Changes not staged for commit:\n (use \"git add ...\" to update what will be committed)\n (use \"git restore ...\" to discard changes in working directory)\n modified: src/index.ts\n modified: package.json\",\n \"gitDiff\": \"diff --git a/src/index.ts b/src/index.ts\n index 1234567..89abcdef 100644\n --- a/src/index.ts\n +++ b/src/index.ts\n @@ -1,3 +1,4 @@\n export function hello() {\n - return \"world\";\n + // Added a comment\n + return \"hello world\";\n }\"\n }\n ]\n }\n ", + "input_schema": { + "json": { + "required": [ + "workingDirectory" + ], + "properties": { + "workingDirectory": { + "type": "string", + "description": "Working directory of the workspace that has git repositories" + } + }, + "type": "object" + } + } + } + } + ], + "native___": [ + { + "ToolSpecification": { + "name": "fs_write", + "description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `str_replace` command:\n * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n * The `new_str` parameter should contain the edited lines that should replace the `old_str`.", + "input_schema": { + "json": { + "type": "object", + "required": [ + "command", + "path" + ], + "properties": { + "new_str": { + "description": "Required parameter of `str_replace` command containing the new string. Required parameter of `insert` command containing the string to insert. Required parameter of `append` command containing the content to append to the file.", + "type": "string" + }, + "summary": { + "type": "string", + "description": "A brief explanation of what the file change does or why it's being made." + }, + "command": { + "type": "string", + "enum": [ + "create", + "str_replace", + "insert", + "append" + ], + "description": "The commands to run. Allowed options are: `create`, `str_replace`, `insert`, `append`." + }, + "old_str": { + "description": "Required parameter of `str_replace` command containing the string in `path` to replace.", + "type": "string" + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "insert_line": { + "description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.", + "type": "integer" + }, + "file_text": { + "description": "Required parameter of `create` command, with the content of the file to be created.", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "dummy", + "description": "This is a dummy tool. If you are seeing this that means the tool associated with this tool call is not in the list of available tools. This could be because a wrong tool name was supplied or the list of tools has changed since the conversation has started. Do not show this when user asks you to list tools.", + "input_schema": { + "json": { + "required": [], + "properties": {}, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "report_issue", + "description": "Opens the browser to a pre-filled gh (GitHub) issue template to report chat issues, bugs, or feature requests. Pre-filled information includes the conversation transcript, chat context, and chat request IDs from the service.", + "input_schema": { + "json": { + "properties": { + "steps_to_reproduce": { + "description": "Optional: Previous user chat requests or steps that were taken that may have resulted in the issue or error response.", + "type": "string" + }, + "expected_behavior": { + "description": "Optional: The expected chat behavior or action that did not happen.", + "type": "string" + }, + "title": { + "type": "string", + "description": "The title of the GitHub issue." + }, + "actual_behavior": { + "description": "Optional: The actual chat behavior that happened and demonstrates the issue or lack of a feature.", + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "introspect", + "description": "ALWAYS use this tool when users ask ANY question about Q CLI itself, its capabilities, features, commands, or functionality. This includes questions like 'Can you...', 'Do you have...', 'How do I...', 'What can you do...', or any question about Q's abilities. When mentioning commands in your response, always prefix them with '/' (e.g., '/save', '/load', '/context'). CRITICAL: Only provide information explicitly documented in Q CLI documentation. If details about any tool, feature, or command are not documented, clearly state the information is not available rather than generating assumptions.", + "input_schema": { + "json": { + "type": "object", + "properties": { + "query": { + "description": "The user's question about Q CLI usage, features, or capabilities", + "type": "string" + } + }, + "required": [] + } + } + } + }, + { + "ToolSpecification": { + "name": "use_aws", + "description": "Make an AWS CLI api call with the specified service, operation, and parameters. All arguments MUST conform to the AWS CLI specification. Should the output of the invocation indicate a malformed command, invoke help to obtain the the correct command.", + "input_schema": { + "json": { + "required": [ + "region", + "service_name", + "operation_name", + "label" + ], + "type": "object", + "properties": { + "parameters": { + "description": "The parameters for the operation. The parameter keys MUST conform to the AWS CLI specification. You should prefer to use JSON Syntax over shorthand syntax wherever possible. For parameters that are booleans, prioritize using flags with no value. Denote these flags with flag names as key and an empty string as their value. You should also prefer kebab case.", + "type": "object" + }, + "region": { + "type": "string", + "description": "Region name for calling the operation on AWS." + }, + "operation_name": { + "type": "string", + "description": "The name of the operation to perform." + }, + "service_name": { + "description": "The name of the AWS service. If you want to query s3, you should use s3api if possible.", + "type": "string" + }, + "profile_name": { + "type": "string", + "description": "Optional: AWS profile name to use from ~/.aws/credentials. Defaults to default profile if not specified." + }, + "label": { + "type": "string", + "description": "Human readable description of the api that is being called." + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "execute_bash", + "description": "Execute the specified bash command.", + "input_schema": { + "json": { + "required": [ + "command" + ], + "properties": { + "command": { + "description": "Bash command to execute", + "type": "string" + }, + "summary": { + "type": "string", + "description": "A brief explanation of what the command does" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "fs_read", + "description": "Tool for reading files, directories and images. Always provide an 'operations' array.\n\nFor single operation: provide array with one element.\nFor batch operations: provide array with multiple elements.\n\nAvailable modes:\n- Line: Read lines from a file\n- Directory: List directory contents\n- Search: Search for patterns in files\n- Image: Read and process images\n\nExamples:\n1. Single: {\"operations\": [{\"mode\": \"Line\", \"path\": \"/file.txt\"}]}\n2. Batch: {\"operations\": [{\"mode\": \"Line\", \"path\": \"/file1.txt\"}, {\"mode\": \"Search\", \"path\": \"/file2.txt\", \"pattern\": \"test\"}]}", + "input_schema": { + "json": { + "properties": { + "operations": { + "items": { + "type": "object", + "required": [ + "mode" + ], + "properties": { + "mode": { + "type": "string", + "enum": [ + "Line", + "Directory", + "Search", + "Image" + ], + "description": "The operation mode to run in: `Line`, `Directory`, `Search`. `Line` and `Search` are only for text files, and `Directory` is only for directories. `Image` is for image files, in this mode `image_paths` is required." + }, + "image_paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of paths to the images. This is currently supported by the Image mode." + }, + "start_line": { + "default": 1, + "type": "integer", + "description": "Starting line number (optional, for Line mode). A negative index represents a line number starting from the end of the file." + }, + "context_lines": { + "default": 2, + "type": "integer", + "description": "Number of context lines around search results (optional, for Search mode)" + }, + "end_line": { + "description": "Ending line number (optional, for Line mode). A negative index represents a line number starting from the end of the file.", + "default": -1, + "type": "integer" + }, + "path": { + "type": "string", + "description": "Path to the file or directory. The path should be absolute, or otherwise start with ~ for the user's home (required for Line, Directory, Search modes)." + }, + "pattern": { + "description": "Pattern to search for (required, for Search mode). Case insensitive. The pattern matching is performed per line.", + "type": "string" + }, + "depth": { + "default": 0, + "type": "integer", + "description": "Depth of a recursive directory listing (optional, for Directory mode)" + } + } + }, + "minItems": 1, + "type": "array", + "description": "Array of operations to execute. Provide one element for single operation, multiple for batch." + }, + "summary": { + "description": "Optional description of the purpose of this batch operation (mainly useful for multiple operations)", + "type": "string" + } + }, + "type": "object", + "required": [ + "operations" + ] + } + } + } + } + ], + "amazon-internal-mcp-server": [ + { + "ToolSpecification": { + "name": "remove_tag_work_contribution", + "description": "Remove a tag from a work contribution in AtoZ.\n\nThis tool allows you to remove a tag (such as a leadership principle tag) from an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- tagKey: The key of the tag to remove (e.g., 'CUSTOMER_OBSESSION', 'EARN_TRUST')\n- tagType: The type of tag (e.g., 'LEADERSHIP_PRINCIPLE')\n- ownerLogin or ownerPersonId: The owner of the work contribution", + "input_schema": { + "json": { + "required": [ + "workContributionId", + "tagKey", + "tagType" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "workContributionId": { + "type": "string", + "description": "ID of the work contribution" + }, + "ownerLogin": { + "description": "Login/alias of the work contribution owner", + "type": "string" + }, + "tagKey": { + "type": "string", + "description": "Key of the tag to remove (e.g., 'CUSTOMER_OBSESSION', 'EARN_TRUST')" + }, + "ownerPersonId": { + "type": "string", + "description": "Person ID of the work contribution owner" + }, + "tagType": { + "description": "Type of tag to remove", + "type": "string", + "enum": [ + "LEADERSHIP_PRINCIPLE", + "ROLE_GUIDELINE" + ] + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_create_question", + "description": "Create a new question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to post new questions to Sage through the MCP interface.\nQuestions require at least one tag or packageTag to categorize them properly.\nThe question content supports Markdown formatting for rich text, code blocks, and links.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Asking technical questions about Amazon internal tools and services\n- Seeking help with troubleshooting issues\n- Requesting best practices or guidance\n\nExample usage:\n{ \"title\": \"How to resolve Brazil dependency conflicts?\", \"contents\": \"I'm getting the following error when building my package:\\n\\n```\\nCannot resolve dependency X\\n```\\n\\nHow can I fix this?\", \"tags\": [\"brazil\", \"build-system\"] }", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "tags": { + "description": "Tags to categorize the question (at least one tag or packageTag is required)", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "description": "Title of the question" + }, + "contents": { + "type": "string", + "description": "Content of the question in Markdown format" + }, + "packageTags": { + "items": { + "type": "string" + }, + "description": "Package tags to categorize the question (at least one tag or packageTag is required)", + "type": "array" + } + }, + "required": [ + "title", + "contents" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "update_work_contribution", + "description": "Update an existing work contribution in AtoZ.\n\nThis tool allows you to modify the details of an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution to update\n- title: The updated title of the work contribution\n- editStatus: The updated status of the contribution\n- ownerLogin or ownerPersonId: The owner of the work contribution\n\nOptional parameters include:\n- summary: An updated summary of the contribution\n- startDate: An updated start date (YYYY-MM-DD)\n- endDate: An updated end date (YYYY-MM-DD)", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "workContributionId", + "title", + "editStatus" + ], + "properties": { + "summary": { + "description": "Updated summary of the work contribution", + "type": "string" + }, + "title": { + "type": "string", + "description": "Updated title of the work contribution" + }, + "startDate": { + "type": "string", + "description": "Updated start date in YYYY-MM-DD format" + }, + "endDate": { + "description": "Updated end date in YYYY-MM-DD format", + "type": "string" + }, + "ownerPersonId": { + "description": "Person ID of the employee who owns the contribution", + "type": "string" + }, + "workContributionId": { + "type": "string", + "description": "ID of the work contribution to update" + }, + "editStatus": { + "type": "string", + "enum": [ + "IN_PROGRESS", + "COMPLETE", + "DRAFT", + "READY_FOR_REVIEW", + "APPROVED", + "PENDING_CHANGES", + "DRAFT_MANAGER" + ], + "description": "Updated edit status of the work contribution" + }, + "ownerLogin": { + "description": "Login/alias of the employee who owns the contribution", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "write_internal_website", + "description": "Write to Amazon internal websites.\n\nSupported websites and their purposes:\n\nDocument Storage & Sharing:\n- w.amazon.com: Internal MediaWiki\n\nNote: By default, content is converted from Markdown to the target format.\nTo skip conversion (if your content is already in the target format), set skipConversion=true.", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "properties": { + "operation": { + "description": "Operation to perform", + "enum": [ + "update", + "append", + "prepend", + "create" + ], + "type": "string" + }, + "versionSummary": { + "type": "string", + "description": "Summary message for the version history" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Website URL to write to" + }, + "content": { + "description": "Content to write in Markdown format", + "type": "string" + }, + "format": { + "default": "XWiki", + "description": "Format to write in", + "type": "string", + "enum": [ + "Markdown", + "XWiki", + "XHTML", + "HTML", + "Plain", + "MediaWiki" + ] + }, + "skipConversion": { + "default": false, + "description": "Skip content format conversion", + "type": "boolean" + }, + "title": { + "type": "string", + "description": "Title for the page (required for create operations)" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "url", + "content", + "operation" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_sa_activity", + "description": "This tool is logging/creating, reading, updating or deleting SA Activities on AWS SFDC AKA AWSentral. You must have either account id or opportunity id to create", + "input_schema": { + "json": { + "type": "object", + "properties": { + "opportunity_id": { + "type": "string", + "description": "the SFDC id of the opportunity, use the sfdc_opportunity_lookup tool to retrieve before submitting." + }, + "operation": { + "description": "The operation to perform: create, read, update, or delete (always read before deleting, confirm with the user)", + "type": "string", + "enum": [ + "create", + "read", + "update", + "delete" + ] + }, + "activity_subject": { + "type": "string", + "description": "The title of the activity, keep it short" + }, + "activity_description": { + "description": "A description of the activity, around 1 paragraph, rewrite the user's input to be more descriptive and professional, unless the user says not to.", + "type": "string" + }, + "activity_id": { + "type": "string", + "description": "The ID of the SA Activity (required for read, update, and delete operations)" + }, + "activity_status": { + "default": "Completed", + "enum": [ + "Not Started", + "In Progress", + "Completed", + "Waiting on someone else", + "Deferred", + "Unresponsive", + "Disqualified", + "Cancelled", + "Completed with Global Support", + "Sales handoff to BDM completed", + "Completed with sales handoff to BDM", + "Completed with funding program handoff to ATP Mgr" + ], + "type": "string", + "description": "The activity Status. Default status is Completed." + }, + "activity_type": { + "description": "The type of activity, one of Account Planning, Meeting, Architecture Review, Demo, Partner, or Workshop", + "type": "string" + }, + "date": { + "description": "the date in MM-DD-YYYY, if left empty will be today's date, if you are unsure about today's date, leave this blank", + "type": "string" + }, + "activity_assigned_to": { + "type": "string", + "description": "The name of the user to which the activity should be assigned." + }, + "account_id": { + "description": "the SFDC id of the account, use the sfdc_account_lookup tool to retrieve before submitting.", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "operation" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_get_knowledge_structure", + "description": "Map the hierarchical organization of your knowledge repository by generating a complete directory structure. This tool provides a navigable overview of how folders and documents are organized, with configurable depth settings to control detail level. Essential for understanding knowledge base architecture and relationships between document collections.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "depth": { + "description": "How many levels deep to traverse", + "type": "number" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "overleaf_write_file", + "description": "Write a file to an Overleaf project with automatic commit and push.\n\nThis tool writes content to the specified file in an Overleaf project.\nBefore writing, it ensures the project is cloned locally and synchronized.\nAfter writing, it automatically commits the changes with a descriptive message\nand pushes them to the remote repository.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"main.tex\",\n \"content\": \"\\\\documentclass{article}\\n\\\\begin{document}\\nHello World\\n\\\\end{document}\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the file within the project" + }, + "project_id": { + "type": "string", + "description": "Project ID to write to" + }, + "content": { + "type": "string", + "description": "File content to write" + } + }, + "additionalProperties": false, + "required": [ + "project_id", + "file_path", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "list_leadership_principles", + "description": "List all Amazon Leadership Principles that can be used as tags on work contributions.\n\nThis tool retrieves a list of all available Amazon Leadership Principles that can be\napplied as tags to work contributions in AtoZ.\n\nLimitations:\nYou can only access your own work contributions\n\nThe response includes:\n- Leadership principle keys (used for adding tags)\n- Display names of the leadership principles\n\nUse this information when adding leadership principle tags to work contributions\nwith the add_tag_work_contribution tool.", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": {}, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_get_artifact_comments", + "description": "Retrieves comments for a Pippin design artifact, organized by thread and status (open vs resolved). Use this tool to review feedback, track discussion threads, and understand the current state of comments on design artifacts. Comments are grouped by parent-child relationships (threads) and categorized by their resolution status. This tool automatically handles pagination to retrieve all comments for the specified artifact.", + "input_schema": { + "json": { + "properties": { + "designId": { + "type": "string", + "description": "Design artifact ID within the project (string identifier, e.g., 'design-1'). Obtain this from pippin_list_artifacts or the Pippin web interface." + }, + "projectId": { + "type": "string", + "description": "Pippin project ID (UUID format, e.g., 'dee44368f3f7'). Obtain this from pippin_list_projects or the Pippin web interface." + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "projectId", + "designId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "search_products", + "description": "Search for products on Amazon.com (US marketplace only) and extract structured product information including titles, prices, ratings, and images", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "query" + ], + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query string for the products you want to find" + }, + "filters": { + "additionalProperties": false, + "description": "Optional filters to narrow down search results", + "type": "object", + "properties": { + "index": { + "type": "string", + "description": "Department to search in. Available options include: 'all' (default), 'books', 'electronics', 'computers', 'clothing', 'home', 'beauty', 'toys', 'grocery', 'sports', 'automotive', 'pets', 'baby', 'health', 'industrial', 'movies', 'music', 'video-games', 'tools', 'office-products', and more" + }, + "sortBy": { + "type": "string", + "description": "Sort order for results. Available options include:\n- 'relevanceblender' (default): Sort by relevance\n- 'price-asc-rank': Price low to high\n- 'price-desc-rank': Price high to low\n- 'review-rank': Average customer review\n- 'date-desc-rank': Newest arrivals\n- 'exact-aware-popularity-rank': Popularity\n- 'get-it-fast-rank': Fastest delivery\n- 'low-prices-rank': Lowest price with ranking factors\n- 'most-purchased-rank': Most purchased\n- 'top-brands-rank': Top brands" + }, + "minPrice": { + "type": "number", + "description": "Minimum price filter in dollars (e.g., 25 for $25)" + }, + "maxPrice": { + "type": "number", + "description": "Maximum price filter in dollars (e.g., 100 for $100)" + } + } + }, + "maxResults": { + "type": "number", + "description": "Maximum number of products to return (default: 10, max recommended: 50)" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_list_records", + "description": "Get the records (also called config values) for a given feature (also called configuration) name from Amazon Config Store.\nIf the specified format of the returned records is PARSED, it will be returned in a human-readable format. If the format is STRINGIFIED, it will be returned in the original ion format.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "featureName" + ], + "properties": { + "stage": { + "description": "Stage to query", + "default": "PROD", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "type": "string" + }, + "format": { + "description": "Specifies the format of the records returned, either PARSED (human-readable format) or STRINGIFIED (original ion format)", + "default": "PARSED", + "type": "string", + "enum": [ + "PARSED", + "STRINGIFIED" + ] + }, + "featureName": { + "type": "string", + "description": "Feature name to retrieve records for" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "prompt_farm_search_prompts", + "description": "A specialized search tool designed to efficiently discover and retrieve tested prompt templates from Amazon internal PromptFarm, enabling developers to leverage community-vetted prompts for reducing LLM hallucinations and optimizing AI outputs. The tool surfaces prompts categorized by use case, download metrics, and community ratings to streamline prompt engineering workflows.", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "properties": { + "searchQuery": { + "type": "string", + "description": "The search query for PromptFarm" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "searchQuery" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_get", + "description": "Gets data from a G2S2 table with specified parameters", + "input_schema": { + "json": { + "properties": { + "parentStageVersion": { + "description": "The parent stage version for the stage version", + "type": "string" + }, + "kwargs": { + "additionalProperties": {}, + "description": "Additional key-value parameters for the query", + "type": "object" + }, + "tableName": { + "type": "string", + "description": "The table name to query" + } + }, + "required": [ + "tableName", + "parentStageVersion" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "edit_quip", + "description": "Edit Quip documents\n\nThis tool allows you to make targeted edits to specific sections of a Quip document\nusing section IDs obtained from the read_quip tool when using HTML format.\n\nWorkflow:\n1. Use read_quip with HTML format to get the document with section IDs\n2. Identify the section ID you want to modify (e.g., 'temp:C:SAf3351f25e51434479864cf71ce')\n3. Use edit_quip with the section ID and appropriate location parameter\n\nLocations:\n0: APPEND - Add to end of document (default)\n1: PREPEND - Add to beginning of document\n2: AFTER_SECTION - Insert after section_id\n3: BEFORE_SECTION - Insert before section_id\n4: REPLACE_SECTION - Replace section_id content\n5: DELETE_SECTION - Delete section_id\n6: AFTER_DOCUMENT_RANGE - Insert after document_range\n7: BEFORE_DOCUMENT_RANGE - Insert before document_range\n8: REPLACE_DOCUMENT_RANGE - Replace document_range content\n9: DELETE_DOCUMENT_RANGE - Delete document_range\n\nExamples:\n1. Append to document:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\"\n}\n```\n\n2. Prepend to document:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 1\n}\n```\n\n3. Insert after section:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 2,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n4. Replace section content:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"### New heading\",\n \"format\": \"markdown\",\n \"location\": 4,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n5. Delete section:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"\",\n \"format\": \"markdown\",\n \"location\": 5,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n6. Edit with concise response:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 4,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\",\n \"returnFullDocument\": false\n}\n```", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "format": { + "enum": [ + "html", + "markdown" + ], + "default": "markdown", + "description": "The format of the content", + "type": "string" + }, + "sectionId": { + "description": "Section ID for section operations", + "type": "string" + }, + "returnFullDocument": { + "description": "Whether to return the full document content after editing (default: false)", + "type": "boolean" + }, + "content": { + "type": "string", + "description": "The new content to write to the document" + }, + "documentRange": { + "description": "Document range for range operations", + "type": "string" + }, + "location": { + "type": "number", + "description": "Location for content insertion", + "minimum": 0, + "maximum": 9 + }, + "documentId": { + "description": "The Quip document URL or ID to edit", + "type": "string" + } + }, + "required": [ + "documentId" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_create_feature", + "description": "Creates a new feature (also called configuration) in Amazon Config Store.\nThis tool allows creating a feature with the specified name, schema, owners, and other attributes.\nThe name of the feature should be unique, and the contextual parameters used should be existing in the specified stage.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "cti": { + "type": "object", + "required": [ + "category", + "type", + "item" + ], + "additionalProperties": false, + "properties": { + "category": { + "type": "string", + "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." + }, + "type": { + "description": "CTI type. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "item": { + "type": "string", + "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." + } + }, + "description": "CTI information. Do NOT assume this info, you MUST ask the user about it." + }, + "teamName": { + "type": "string", + "description": "Team name responsible for the feature. Do NOT assume this info, you MUST ask the user about it." + }, + "crFeatureEnabled": { + "type": "boolean", + "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", + "default": true + }, + "owners": { + "items": { + "required": [ + "type", + "name" + ], + "type": "object", + "properties": { + "type": { + "enum": [ + "BINDLE", + "TEAM", + "POSIX_GROUP", + "AAA" + ], + "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "name": { + "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + } + }, + "additionalProperties": false + }, + "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", + "minItems": 1, + "type": "array" + }, + "schema": { + "type": "object", + "required": [ + "name", + "attributes", + "contextualParameters", + "types", + "metadata" + ], + "properties": { + "attributes": { + "minItems": 1, + "items": { + "properties": { + "name": { + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "type": "string", + "description": "Name of the attribute in snake_case" + }, + "description": { + "description": "Description for the attribute", + "type": "string" + }, + "type": { + "type": "string", + "description": "Type of the attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "type": "array", + "description": "Attributes of the feature. Attributes are the fields of your config table, the value of these attributes can vary depending on contextual parameter values." + }, + "contextualParameters": { + "items": { + "type": "string" + }, + "minItems": 0, + "type": "array", + "description": "Contextual Parameters of the feature. Contextual parameters are the keys of the configuration, you can have zero, one, or many. Order is important as it is used to determine the priority in query resolution, meaning least specific CP must come first and the most specific comes last (e.g. country, state, city). The contextual parameter must exist in the specified stage before using it in a feature. If it does not exist, the contextual parameter needs to be created in Sandbox first then promoted to other stages. You can use acs_get_contextual_parameter tool to confirm that the name of the contextual parameter is existing in the specified stage, or you can use acs_search_resources tool with resourceType as CONTEXTUAL_PARAMETER to recommend contextual parameters to use as guidance to the customers if it was not provided or contextual parameter does not exist in the given stage." + }, + "metadata": { + "properties": { + "clients": { + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "packageName": { + "type": "string", + "description": "Custom package name for the client. The Java client will be generated in the specified package. When provided, use search_internal_code tool to make sure the package exists. Before executing the command, you MUST ask the user to MAKE SURE the bindle has these required permissions: 1. Can read Gitfarm Repository 2. Can write Gitfarm Repository 3. Can write to protected branches Gitfarm Repository" + }, + "bindleId": { + "type": "string", + "description": "Bindle ID for the client. It needs to be in the format of amzn1.bindle.resource.* and NOT the bindle name. bindleId is required if packageName is not specified. ACS will auto-generate a package under this bindle. Do NOT assume this info, you MUST ask the user about it.Before executing the command, you MUST ask the user to MAKE SURE the bindle has these required permissions: 1. Can read Gitfarm Repository 2. Can write Gitfarm Repository 3. Can write to protected branches Gitfarm Repository" + } + } + }, + "minItems": 1, + "description": "The generated Java client to consume the configuration. You MUST ASK the user whether they want to use an existing package by providing a packageName, or generate a new package by providing a bindleId.", + "maxItems": 1, + "type": "array" + } + }, + "type": "object", + "required": [ + "clients" + ], + "additionalProperties": false, + "description": "Metadata of the clients of the feature" + }, + "types": { + "items": { + "properties": { + "name": { + "description": "Name of the custom type", + "type": "string" + }, + "type": { + "anyOf": [ + { + "required": [ + "kind", + "values" + ], + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "description": "Enum type used as a type for an attribute.", + "const": "Enum" + }, + "values": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" + }, + "minItems": 1, + "description": "Enum values. Values must be in snake_case." + } + }, + "type": "object" + }, + { + "type": "object", + "properties": { + "attributes": { + "description": "Struct attributes", + "type": "array", + "minItems": 1, + "items": { + "required": [ + "name", + "type" + ], + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the struct attribute. It should be in snake_case.", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "type": "string" + }, + "type": { + "type": "string", + "description": "Type of the struct attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" + }, + "description": { + "type": "string", + "description": "Description of the struct attribute." + } + }, + "type": "object" + } + }, + "kind": { + "type": "string", + "description": "Struct type used as a type for an attribute.", + "const": "Struct" + } + }, + "additionalProperties": false, + "required": [ + "kind", + "attributes" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "List", + "type": "string", + "description": "List type used as a type for an attribute." + }, + "element": { + "type": "string", + "description": "Type of the list element: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" + } + }, + "required": [ + "kind", + "element" + ] + } + ], + "description": "Type definition" + }, + "description": { + "description": "Description for the custom type", + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "type" + ], + "additionalProperties": false + }, + "minItems": 0, + "type": "array", + "description": "Custom types for the feature that can be used as schema attribute type, struct attribute type, or list element type" + }, + "validations": { + "minItems": 0, + "description": "Validations for the feature attributes", + "type": "array", + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "description": "For string attributes, which validates attributes against a predefined regex.", + "const": "Pattern" + }, + "regex": { + "description": "Regex pattern", + "type": "string" + }, + "targetAttributes": { + "items": { + "type": "string" + }, + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", + "type": "array" + }, + "description": { + "type": "string", + "description": "Contains the ACS customer explanation for a given validation." + } + }, + "type": "object", + "required": [ + "kind", + "targetAttributes", + "regex" + ] + }, + { + "properties": { + "min": { + "type": "string", + "description": "Range minimum value (inclusive)" + }, + "kind": { + "const": "Range", + "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values).", + "type": "string" + }, + "max": { + "type": "string", + "description": "Range maximum value (inclusive)" + }, + "targetAttributes": { + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + } + }, + "type": "object", + "required": [ + "kind", + "targetAttributes" + ], + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "NonNull", + "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes." + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + }, + "targetAttributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + } + }, + "required": [ + "kind", + "targetAttributes" + ] + }, + { + "type": "object", + "required": [ + "kind", + "targetAttributes", + "arn" + ], + "additionalProperties": false, + "properties": { + "description": { + "description": "Description of what the Lambda validates.", + "type": "string" + }, + "arn": { + "type": "string", + "description": "Lambda ARN" + }, + "targetAttributes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + }, + "kind": { + "description": "Lambda validation allows you to use your own custom logic to validate record values.", + "type": "string", + "const": "Lambda" + } + } + } + ] + } + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Name of the feature to create. It should be in PascalCase.", + "pattern": "^(?:[A-Z][a-z0-9]*)+$" + } + }, + "additionalProperties": false, + "description": "Schema definition for the feature. The schema defines the attributes, contextual parameters, types, validations, and clients of a feature." + }, + "approvers": { + "items": { + "required": [ + "type", + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "type": { + "type": "string", + "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", + "enum": [ + "USER", + "LDAP", + "POSIXG", + "TEAM", + "SNS" + ] + }, + "requiredCount": { + "type": "number", + "description": "Required count of approvers", + "exclusiveMinimum": 0 + } + }, + "type": "object" + }, + "type": "array", + "minItems": 1, + "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it." + }, + "stage": { + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "description": "Stage to query", + "type": "string" + }, + "description": { + "description": "Description of the feature", + "type": "string", + "minLength": 1 + }, + "teamWikiLink": { + "description": "Team wiki link. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "configSnapshotEnabled": { + "default": false, + "description": "Whether config snapshot is enabled. Config snapshot allows the user to use deployable cache and dynamic refresher for example. Know more about deployable cache from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/OnBoarding/Cache/#HDeployablecache and dynamic refresher from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/DynamicRefresher.", + "type": "boolean" + } + }, + "required": [ + "description", + "schema", + "owners", + "cti", + "teamName", + "teamWikiLink", + "stage" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "rtla_fetch_logs", + "description": "Fetch logs from RTLA (Real-Time Log Analysis) API. This tool allows you to retrieve log entries based on organization, affected type, time range, and filter expressions. The maximum time range supported is 12 hours from the start time. Useful for troubleshooting system issues, analyzing error patterns, and monitoring application health.", + "input_schema": { + "json": { + "properties": { + "affectedType": { + "type": "string", + "description": "Type of affected logs to retrieve (e.g., \"FATAL\", \"NONFATAL\")" + }, + "identifyAdditionalOrgs": { + "default": true, + "description": "Whether to identify additional organizations", + "type": "boolean" + }, + "org": { + "type": "string", + "description": "Organization identifier (e.g., \"CWCBCCECMPROD\")" + }, + "searchField": { + "type": "string", + "default": "org", + "description": "Search field type (default: \"org\")" + }, + "timeZone": { + "description": "Time zone (e.g., \"US/Pacific\")", + "type": "string", + "default": "GMT&customTimeZoneOffset" + }, + "startTime": { + "type": "string", + "description": "Start time in ISO 8601 format with timezone (e.g., 2025-05-11T11:31:16-04:00)" + }, + "anchor": { + "type": "string", + "description": "Anchor position (e.g., \"Ending\", \"Beginning\")", + "default": "Ending" + }, + "filterExpression": { + "type": "string", + "description": "Filter expression for log filtering (e.g., \"(pageType eq 'uscbcc-ecm-paybill')\")" + }, + "endTime": { + "description": "End time in ISO 8601 format with timezone (e.g., 2025-05-11T12:31:16-04:00)", + "type": "string" + } + }, + "required": [ + "org", + "affectedType", + "startTime", + "endTime" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "orca_get_latest_error_details", + "description": "Get detailed error information from an Orca workflow run URL.\n\nThis tool extracts error details including stack traces from Orca Studio execution pages.\n\nExample:\n```json\n{ \"url\": \"https://us-east-1.studio.orca.amazon.dev/#/clients/MyClient/execution/12345\", \"workflowName\": \"TestWorkflow\", \"objectId\": \"TestObjectId, \"runId\": \"TestRunId, \"clientId\": \"MyOrcaClient\"}\n```\nExample with custom region:\n```json\n{ \"url\": \"https://us-east-1.studio.orca.amazon.dev/#/clients/MyClient/execution/12345\", \"workflowName\": \"TestWorkflow\", \"objectId\": \"TestObjectId, \"runId\": \"TestRunId, \"clientId\": \"MyOrcaClient, \"region\": \"us-west-2\"}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "workflowName": { + "type": "string", + "description": "The type of workflow to extract error details from" + }, + "url": { + "type": "string", + "description": "The Orca Studio URL of the execution to analyze" + }, + "objectId": { + "description": "The objectId of the particular workflow to extract error details from", + "type": "string" + }, + "runId": { + "description": "The runId of the execution to extract error details from", + "type": "string" + }, + "clientId": { + "type": "string", + "description": "The clientId of the execution to extract error details from" + }, + "region": { + "type": "string", + "description": "AWS region (defaults to us-east-1). Common regions include us-west-2, eu-west-1, etc." + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "required": [ + "url", + "workflowName", + "objectId", + "runId", + "clientId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_agent_script_search", + "description": "Perform comprehensive keyword searches across the entire agentic script library, examining script names, content bodies, and metadata fields simultaneously. This tool returns contextually-rich results with relevant text snippets surrounding each match, highlighting where and how search terms appear within scripts. Results include file locations, match types (filename, content, or description matches), and properly handles duplicate scripts with consolidated results. Perfect for discovering scripts based on functionality, implementation details, or descriptive elements rather than exact names.", + "input_schema": { + "json": { + "required": [ + "query" + ], + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query to find matching scripts" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "delete_work_contribution", + "description": "Delete a work contribution from AtoZ.\n\nLimitations:\nYou can only access your own work contributions\n\nThis tool allows you to remove an existing work contribution.\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution to delete\n- ownerLogin or ownerPersonId: The owner of the work contribution", + "input_schema": { + "json": { + "type": "object", + "properties": { + "ownerLogin": { + "description": "Login/alias of the employee who owns the contribution", + "type": "string" + }, + "ownerPersonId": { + "description": "Person ID of the employee who owns the contribution", + "type": "string" + }, + "workContributionId": { + "description": "ID of the work contribution to delete", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "workContributionId" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "mosaic_list_risks", + "description": "\nThe AWS Risk Library is an extensible reference library that contains potential risk events\nthat may impact AWS and/or its customers and the risk scenarios that could trigger them. The\nlibrary contains high-level risk categories (Level 1), (e.g., availability, security, third\nparty, etc.); sub-categories of risk events (Level 2) for each level 1 risk (e.g., network\nfailure, service failure, infrastructure failure); and plausible risk causes (Level 3) that\ncan result in a risk event (e.g., inadequate capacity planning, lack of governance oversight,\npower outages, etc.). The level 2 risk events are the central element of the risk library.\n\nThis tool returns the risks that are part of the AWS Risk Library.", + "input_schema": { + "json": { + "type": "object", + "properties": {} + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_update_contextual_parameter", + "description": "Updates a contextual parameter (also called config key, or CP) in Amazon Config Store.\nThis tool allows updating a contextual parameter by only giving it the parameters required to be updated, other parameters that are not provided will remain as is.\nIf any of the required parameters are not provided, do NOT assume them, just leave them empty.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "properties": { + "description": { + "type": "string", + "description": "Description of the contextual parameter" + }, + "stage": { + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "description": "Stage to query", + "type": "string" + }, + "crFeatureEnabled": { + "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", + "type": "boolean" + }, + "cti": { + "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", + "type": "object", + "required": [ + "category", + "type", + "item" + ], + "properties": { + "type": { + "description": "CTI type. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "item": { + "type": "string", + "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." + }, + "category": { + "type": "string", + "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." + } + }, + "additionalProperties": false + }, + "approvers": { + "type": "array", + "minItems": 1, + "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name" + ], + "properties": { + "requiredCount": { + "type": "number", + "description": "Required count of approvers", + "exclusiveMinimum": 0 + }, + "type": { + "enum": [ + "USER", + "LDAP", + "POSIXG", + "TEAM", + "SNS" + ], + "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "name": { + "type": "string", + "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." + } + } + } + }, + "owners": { + "items": { + "type": "object", + "properties": { + "type": { + "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", + "type": "string", + "enum": [ + "BINDLE", + "TEAM", + "POSIX_GROUP", + "AAA" + ] + }, + "name": { + "type": "string", + "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it." + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, + "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", + "type": "array" + }, + "validations": { + "items": { + "anyOf": [ + { + "properties": { + "targetAttributes": { + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", + "items": { + "type": "string" + }, + "type": "array" + }, + "kind": { + "const": "Pattern", + "description": "For string attributes, which validates attributes against a predefined regex.", + "type": "string" + }, + "description": { + "type": "string", + "description": "Contains the ACS customer explanation for a given validation." + }, + "regex": { + "description": "Regex pattern", + "type": "string" + } + }, + "required": [ + "kind", + "targetAttributes", + "regex" + ], + "type": "object", + "additionalProperties": false + }, + { + "properties": { + "kind": { + "const": "Range", + "type": "string", + "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values)." + }, + "targetAttributes": { + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", + "items": { + "type": "string" + }, + "type": "array" + }, + "min": { + "description": "Range minimum value (inclusive)", + "type": "string" + }, + "max": { + "type": "string", + "description": "Range maximum value (inclusive)" + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "kind", + "targetAttributes" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "NonNull", + "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes.", + "type": "string" + }, + "targetAttributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + }, + "description": { + "type": "string", + "description": "Contains the ACS customer explanation for a given validation." + } + }, + "type": "object", + "required": [ + "kind", + "targetAttributes" + ] + }, + { + "required": [ + "kind", + "targetAttributes", + "arn" + ], + "properties": { + "description": { + "type": "string", + "description": "Description of what the Lambda validates." + }, + "arn": { + "type": "string", + "description": "Lambda ARN" + }, + "kind": { + "const": "Lambda", + "description": "Lambda validation allows you to use your own custom logic to validate record values.", + "type": "string" + }, + "targetAttributes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + } + }, + "additionalProperties": false, + "type": "object" + } + ] + }, + "description": "Validations for the contextual parameter records", + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "description": "Name of the contextual parameter to update. It should be in snake_case." + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "name", + "stage" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_update_artifact", + "description": "Updates an existing artifact within a Pippin project", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "description": "Project ID" + }, + "name": { + "description": "Updated artifact name", + "type": "string" + }, + "content": { + "type": "string", + "description": "Updated artifact content (provide this OR contentPath)" + }, + "designId": { + "type": "string", + "description": "Artifact ID" + }, + "contentPath": { + "type": "string", + "description": "Path to a file containing the artifact content (provide this OR content)" + }, + "description": { + "type": "string", + "description": "Updated artifact description" + } + }, + "required": [ + "projectId", + "designId" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_freeze_stage_version", + "description": "Freezes a specified G2S2 stage version", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "stageVersion": { + "type": "string", + "description": "The stage version to freeze" + } + }, + "additionalProperties": false, + "required": [ + "stageVersion" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "edit_quip_link_sharing", + "description": "Edit link sharing settings for an existing Quip document\n\nThis tool allows you to enable, disable, or change the link sharing mode\nfor an existing Quip document without modifying its content.\n\nParameters:\n- documentId: The Quip document URL or ID\n- mode: Link sharing mode ('view', 'edit', or 'none')\n\nExamples:\n1. Enable view-only link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"view\"\n}\n```\n\n2. Enable edit link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"edit\"\n}\n```\n\n3. Disable link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"none\"\n}\n```", + "input_schema": { + "json": { + "properties": { + "documentId": { + "description": "The Quip document URL or ID", + "type": "string" + }, + "mode": { + "enum": [ + "view", + "edit", + "none" + ], + "description": "Link sharing mode: 'view' for view-only, 'edit' for edit access, 'none' to disable sharing", + "type": "string" + } + }, + "type": "object", + "additionalProperties": false, + "required": [ + "documentId", + "mode" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "search_quip_commented_by_current_user", + "description": "Get all documents where the current user has left comments\n\nThis tool retrieves all Quip documents where the current user has posted comments.\nYou can optionally filter the results by date range to get documents with comments within a specific time period.\n\nThe tool checks all user-accessible threads for comments made by the current user,\nwith optional date range filtering for more targeted results.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents with user comments:\n```json\n{\n}\n```\n\n2. Get documents with comments within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```", + "input_schema": { + "json": { + "properties": { + "startDate": { + "type": "string", + "description": "Start date for filtering comments (YYYY-MM-DD format)" + }, + "endDate": { + "description": "End date for filtering comments (YYYY-MM-DD format)", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "read_quip_from_urls", + "description": "Extract and retrieve the full HTML content of Quip documents using their URLs\n\nThis tool reads multiple Quip documents simultaneously using their URLs.\nIt extracts document IDs from the provided links and retrieves the content\nfor all documents in a single operation.\n\nThe tool accepts an array of Quip document URLs and returns structured\ninformation including document ID, title, content, and the original link\nfor each document.\n\nExamples:\n1. Read multiple documents:\n```json\n{\n \"links\": [\n \"https://quip-amazon.com/abc/Document1\",\n \"https://quip-amazon.com/def/Document2\"\n ]\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "links": { + "type": "array", + "description": "Array of Quip document urls to read", + "items": { + "type": "string" + } + } + }, + "required": [ + "links" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "mermaid", + "description": "Create and decode Mermaid diagrams using Amazon's internal Mermaid editor.\nMermaid allows creating flowcharts, sequence diagrams, and more using text descriptions.\n\nSupported operations:\n- encode: Convert Mermaid text to an encoded URL\n- decode: Extract Mermaid text from an encoded URL", + "input_schema": { + "json": { + "required": [ + "operation" + ], + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "operation": { + "type": "string", + "enum": [ + "encode", + "decode" + ], + "description": "The operation to perform" + }, + "url": { + "description": "Mermaid URL for decode operation", + "format": "uri", + "type": "string" + }, + "content": { + "type": "string", + "description": "Mermaid content for encode operation" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "prompt_farm_prompt_content", + "description": "A tool designed to fetch prompt content directly by specifying the repository name. This tool leverages repository identifiers to locate, extract, and deliver prompt templates or prompt from PromptFarm prompt repositories. It simplifies accessing prompt definitions without manual browsing, enabling users to quickly integrate or customize prompts by referencing the exact repository source.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "repositoryName" + ], + "type": "object", + "properties": { + "repositoryName": { + "description": "The name of the PromptFarm repository to retrieve the prompt from", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_create_artifact", + "description": "Creates a new artifact within an existing Pippin project", + "input_schema": { + "json": { + "additionalProperties": false, + "required": [ + "projectId", + "name", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "description": "Project ID" + }, + "name": { + "type": "string", + "description": "Artifact name" + }, + "description": { + "type": "string", + "description": "Artifact description" + }, + "content": { + "type": "string", + "description": "Artifact content" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "create_quip", + "description": "Create a new Quip document or spreadsheet\n\nThis tool creates a new document or spreadsheet in Quip with the specified content.\n\nRequired parameters:\n- content: The HTML or Markdown content of the new document (max 1MB)\n\nOptional parameters:\n- format: Format of the content ('html' or 'markdown', default is 'html')\n- title: Title of the new document (max 10KB)\n- member_ids: Comma-separated list of folder IDs or user IDs for access\n- type: Type of document to create ('document' or 'spreadsheet', default is 'document')\n- mode: Link sharing mode ('view', 'edit', or 'none' to disable sharing)\n\nNotes:\n- If title is not specified, it will be inferred from the first content\n- If member_ids is not specified, the document will be created in the user's Private folder\n- For spreadsheets, content must be surrounded by HTML tags\n- If mode is not specified, document uses default sharing settings\n\nExamples:\n1. Create a simple document:\n```json\n{\n \"content\": \"# My New Document\\n\\nThis is a test document.\",\n \"format\": \"markdown\"\n}\n```\n\n2. Create a document with a title in a specific folder:\n```json\n{\n \"content\": \"# Introduction\\n\\nThis is the start of my document.\",\n \"format\": \"markdown\",\n \"title\": \"Project Proposal\",\n \"member_ids\": \"ABCDEF123456\"\n}\n```\n\n3. Create a document with internal link sharing:\n```json\n{\n \"content\": \"# Shared Document\\n\\nThis document has link sharing enabled.\",\n \"format\": \"markdown\",\n \"mode\": \"view\"\n}\n```\n\n4. Create a document with sharing disabled:\n```json\n{\n \"content\": \"# Private Document\\n\\nThis document has no link sharing.\",\n \"format\": \"markdown\",\n \"mode\": \"none\"\n}\n```", + "input_schema": { + "json": { + "required": [ + "content" + ], + "additionalProperties": false, + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "view", + "edit", + "none" + ], + "description": "Link sharing mode: 'view' for view-only, 'edit' for edit access, 'none' to disable sharing" + }, + "format": { + "default": "markdown", + "enum": [ + "html", + "markdown" + ], + "type": "string", + "description": "The format of the content" + }, + "member_ids": { + "description": "Comma-separated list of folder IDs or user IDs for access", + "type": "string" + }, + "title": { + "type": "string", + "description": "Title of the new document" + }, + "type": { + "default": "document", + "type": "string", + "description": "Type of document to create", + "enum": [ + "document", + "spreadsheet" + ] + }, + "content": { + "description": "The HTML or Markdown content of the new document", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_create_label", + "description": "Creates a new G2S2 label with the specified parent label", + "input_schema": { + "json": { + "type": "object", + "properties": { + "labelName": { + "type": "string", + "description": "The label name to create" + }, + "stageVersion": { + "description": "The stage version for the new label", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "labelName", + "stageVersion" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "search_quip_mentioned_current_user", + "description": "Get all documents where the current user was mentioned\n\nThis tool retrieves all Quip documents where the current user was mentioned by name or email.\nYou can optionally filter the results by date range to get documents with mentions within a specific time period.\n\nThe tool searches for documents containing the user's name, email, or username,\nwith optional date range filtering based on document update time.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents with user mentions:\n```json\n{\n}\n```\n\n2. Get documents with mentions within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```", + "input_schema": { + "json": { + "properties": { + "startDate": { + "description": "Start date for filtering mentions (YYYY-MM-DD format)", + "type": "string" + }, + "endDate": { + "type": "string", + "description": "End date for filtering mentions (YYYY-MM-DD format)" + } + }, + "additionalProperties": false, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "create_work_contribution", + "description": "Create a new work contribution in AtoZ.\n\nThis tool allows you to create a new work contribution with specified details.\nAfter successful creation, you will need to navigate to the AtoZ portal\nat https://atoz.amazon.work/profile/your-growth to upload any artifacts.\nYou must use list_leadership_principles tool to get the uri and definition of all principles\nYou can use add_tag_work_contribution to tag leadership principles\nYou must provide ownerLogin amazon alias as ownerLogin\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- title: The title of the work contribution\n- editStatus: The status of the contribution (IN_PROGRESS or DRAFT)\n- ownerLogin or ownerPersonId: The owner of the work contribution\n\nOptional parameters include:\n- summary: A detailed summary of the contribution\n- startDate: The start date of the contribution (YYYY-MM-DD)\n- endDate: The end date of the contribution (YYYY-MM-DD)", + "input_schema": { + "json": { + "type": "object", + "properties": { + "editStatus": { + "enum": [ + "IN_PROGRESS", + "COMPLETE", + "DRAFT", + "READY_FOR_REVIEW", + "APPROVED", + "PENDING_CHANGES", + "DRAFT_MANAGER" + ], + "description": "Edit status of the work contribution", + "type": "string" + }, + "summary": { + "type": "string", + "description": "Summary of the work contribution" + }, + "startDate": { + "description": "Start date in YYYY-MM-DD format", + "type": "string" + }, + "ownerLogin": { + "type": "string", + "description": "Login/alias of the employee who owns the contribution" + }, + "ownerPersonId": { + "description": "Person ID of the employee who owns the contribution", + "type": "string" + }, + "endDate": { + "type": "string", + "description": "End date in YYYY-MM-DD format" + }, + "title": { + "type": "string", + "description": "Title of the work contribution" + } + }, + "required": [ + "title", + "editStatus" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "mosaic_list_controls", + "description": "\nThe AWS Control Library is the authoritative source of controls that AWS \nuses to manage operational risk. The library represents AWS's own control \nframework supporting high-level policies and standards, and represents \nmanagement's directives and requirements that prescribe how the organization \nmanages its risk and control processes. The library also provides a \nmapping of AWS controls to AWS' policies/standards, and external \nrequirements such as regulatory and compliance frameworks. AWS implements \nthese controls through various mechanisms, including architectural system \ndesign (e.g., region isolation), system enforced guardrails (e.g., static \ncode analysis), or and centrally enforced organizational processes (e.g., \napplication security reviews). Control owners, who are leaders at Level 8 \nor above within the business, validate each control. The Security Assurance \n& Compliance (SA&C) team independently challenges these validations. To \ndemonstrate assurance, each control includes a narrative that articulates \nhow the control is implemented and supporting evidence of control execution \nthat provides tangible proof of its implementation.\n\nThis tool returns the controls that are part of the AWS Control Library.", + "input_schema": { + "json": { + "type": "object", + "properties": {} + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_agent_script_list", + "description": "Discover and browse the complete collection of available agentic scripts with customizable filtering options. This tool provides a comprehensive inventory of script resources including their names, file paths, and detailed descriptions. Results are organized to help quickly identify relevant scripts for specific tasks, with automatic handling of duplicate scripts across different directories. Ideal for exploring the script library or finding scripts based on filename patterns. Returns script names, paths, and descriptions to help users discover relevant scripts for their tasks. Categorize the scripts based on description.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "filter": { + "type": "string", + "description": "Filter to apply to script list" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "isengard", + "description": "Access Amazon's internal Isengard service for AWS account management.\\nThis tool is designed for builders including developers, support teams, and field teams (SAs and TAMs) \\nto easily access their Isengard-managed AWS accounts, typically non-production accounts used for building and testing.\\n\\n## When to use:\\n- When you need to list AWS accounts you own or have access to through POSIX group membership\\n- When you need detailed information about a specific Isengard-managed AWS account\\n- When you need temporary AWS credentials for testing or development work\\n\\n## Limitations:\\n- Only works with Isengard-managed AWS accounts\\n- Requires appropriate permissions to the target AWS accounts, managed by Midway\\n- Credential access requires valid IAM role names already created for the AWS Account.\\n- This tool does not yet support alternative partitions such as GovCloud or China.\\n- listOwnedAWSAccounts supports pagination with maxResultsPerPage (1-100, default: 30) and maxPages (default: 1) parameters.\\n\\n## Supported operations:\\n- listOwnedAWSAccounts: List all ACTIVE accounts you own with optional primary owner filtering and pagination\\n- getAWSAccount: Get detailed information about a specific AWS account\\n- getAssumeRoleCredentials: Get temporary AWS credentials for a specific account and IAM role\\n\\n## Examples\\nList owned AWS accounts: isengard listOwnedAWSAccounts\\nGet AWS account details: isengard getAWSAccount --accountId 123456789012\\nGet AWS credentials for IAM role: isengard getAssumeRoleCredentials --accountId 123456789012 --roleName MyRole", + "input_schema": { + "json": { + "required": [ + "operation" + ], + "properties": { + "maxResultsPerPage": { + "type": "number", + "description": "Number of results per page (1-100, default: 30)", + "minimum": 1, + "maximum": 100 + }, + "maxPages": { + "description": "Maximum number of pages to retrieve (default: 1)", + "type": "number", + "minimum": 1 + }, + "roleName": { + "type": "string", + "description": "IAM Role Name for getAssumeRoleCredentials operation." + }, + "operation": { + "type": "string", + "description": "The operation to perform", + "enum": [ + "listOwnedAWSAccounts", + "getAWSAccount", + "getAssumeRoleCredentials" + ] + }, + "ownerType": { + "enum": [ + "primary" + ], + "type": "string", + "description": "Filter for listOwnedAWSAccounts operation narrows down results to only those that the user is primary owner of. The only valid value is 'primary' otherwise leave it ommited to return all AWS Accounts the user is considered an owner of." + }, + "accountId": { + "type": "string", + "description": "AWS Account ID for getAWSAccount or getAssumeRoleCredentials operation" + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_create_issue", + "description": "Create a new JIRA issue", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "required": [ + "projectKey", + "issueType", + "summary" + ], + "type": "object", + "properties": { + "projectKey": { + "type": "string", + "minLength": 1, + "description": "The key of the project where the issue will be created" + }, + "summary": { + "type": "string", + "minLength": 1, + "description": "The summary of the issue" + }, + "issueType": { + "description": "The type of the issue (e.g., Bug, Task, Story)", + "minLength": 1, + "type": "string" + }, + "additionalFields": { + "type": "object", + "description": "Additional fields to include in the issue", + "additionalProperties": {} + }, + "description": { + "type": "string", + "description": "The description of the issue" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "add_comment_quip", + "description": "Add a comment to a Quip document\n\nThis tool allows you to add a comment to a specified Quip document or thread.\nComments appear in the thread's conversation panel and are visible to all document collaborators.\nThe comment will be attributed to the owner of the API token.\n\nParameters:\n- threadIdOrUrl: (Required) The Quip document/thread ID or URL to add a comment to\n- content: (Required) The comment message text to add\n- section_id: ID of a document section to comment on\n\nNotes:\n- Plain text only, no formatting or HTML is supported\n- Comments cannot be edited or deleted through the Quip API: These operations are not supported\n- Maximum length is 1MB (though practical messages are typically much shorter)\n- Only one of section_id or annotation_id can be provided\n- annotation_id is retrieved as a response of the get_recent_messages_quip tool\n- Manually creating a link to a quip section gives a response like : https://quip-amazon.com/bpVtAZ8LB0b4/Quip-Commenting-Capabilities-Test#fND9CAsTr5B\n- Where bpVtAZ8LB0b4 is the threadId, and fND9CAsTr5B is the section_id.\n- As such, the annotation_id is retreived by the get_recent_messages_quip tool\n\nExamples:\n1. Add a simple comment:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"Great document! I have a few suggestions.\"\n}\n```\n\n2. Add a comment to a specific section:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"This section needs more detail.\",\n \"section_id\": \"SAf3351f25e51434479864cf71ce\"\n}\n```\n\n3. Reply to an existing comment:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"I agree with your comment.\",\n \"annotation_id\": \"fND9CAeEYiG\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "required": [ + "threadIdOrUrl", + "content" + ], + "properties": { + "annotation_id": { + "type": "string", + "description": "ID of a document comment to reply to" + }, + "threadIdOrUrl": { + "type": "string", + "description": "The thread ID or Quip URL to add a comment to" + }, + "section_id": { + "type": "string", + "description": "ID of a document section to comment on" + }, + "content": { + "type": "string", + "description": "The comment message content to add to the thread" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "read_permissions", + "description": "Read team information from Amazon's internal permissions system.\n\nThis tool allows you to retrieve detailed information about team memberships,\noverrides, and rules from permissions.amazon.com team pages.\n\nYou MUST specify which tables OR rule sections to include in the response.\nAt least one of these parameters must be provided with at least one option selected.\nThe tool will only retrieve the specified tables and rule sections.\n\nAvailable tables:\n- additional_overrides: Additional Members overrides table\n- deny_overrides: Denied Members overrides table\n- team_membership: Team Membership table (large table, slow to retrieve)\n- team_audit: Team Audit log table (very large table, very slow to retrieve)\n\nAvailable rule sections:\n- rule_membership: Membership rules section\n- rule_additional_overrides: Additional Members overrides rules section\n\nFor large tables (especially team_membership and team_audit), you can use the\nmaxPages parameter to limit the number of pages processed and prevent timeouts.\nYou can also use tableFilters to narrow down the results.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "maxPages": { + "type": "integer", + "description": "Maximum number of pages to process per table. Use for very large tables to prevent timeouts.", + "exclusiveMinimum": 0 + }, + "ruleSections": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "rule_membership", + "rule_additional_overrides" + ] + }, + "description": "List of specific rule sections to include. At least one table or rule section must be specified." + }, + "tables": { + "description": "List of specific tables to include. At least one table or rule section must be specified.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "additional_overrides", + "deny_overrides", + "team_membership", + "team_audit" + ] + } + }, + "tableFilters": { + "propertyNames": { + "enum": [ + "additional_overrides", + "deny_overrides", + "team_membership", + "team_audit" + ] + }, + "description": "Filters to apply to specific tables. Each filter contains a query string or array of query strings and optional threshold.", + "additionalProperties": { + "required": [ + "query" + ], + "properties": { + "query": { + "anyOf": [ + { + "type": "string", + "description": "Text to search for in the table rows" + }, + { + "items": { + "type": "string" + }, + "description": "Multiple terms to search for in the table rows (combined with OR logic)", + "type": "array" + } + ], + "description": "Text or array of texts to search for in the table rows" + }, + "threshold": { + "default": 0.3, + "description": "Fuzzy match threshold (0-1). Lower = stricter match. Default is 0.3", + "minimum": 0, + "maximum": 1, + "type": "number" + } + }, + "type": "object", + "additionalProperties": false + }, + "type": "object" + }, + "teamUrl": { + "type": "string", + "description": "URL of the permissions team page to read", + "format": "uri" + } + }, + "required": [ + "teamUrl" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_add_comment", + "description": "Add a comment to a JIRA issue", + "input_schema": { + "json": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The body of the comment", + "minLength": 1 + }, + "issueIdOrKey": { + "minLength": 1, + "type": "string", + "description": "The ID or key of the issue" + } + }, + "required": [ + "issueIdOrKey", + "body" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "get_thread_folders_quip", + "description": "Get folders containing a Quip thread (V2 API)\n\nThis tool retrieves information about folders that contain a specific thread.\nIt uses the V2 API which provides more comprehensive folder information.\n\nYou can provide one of the following:\n- The thread ID\n- The thread's secret path\n- The full Quip URL (e.g., https://quip-amazon.com/abc/Doc)\n\nThe secret path can be found in the URL of a thread.\nFor example, in 'https://quip.com/3fs7B2leat8/TrackingDocument', the secret path is '3fs7B2leat8'.\n\nExamples:\n```json\n{\n \"threadId\": \"3fs7B2leat8\"\n}\n```\n\n```json\n{\n \"threadId\": \"https://quip-amazon.com/abc/Doc\"\n}\n```", + "input_schema": { + "json": { + "properties": { + "threadId": { + "description": "The thread ID, secret path, or full Quip URL", + "type": "string" + } + }, + "type": "object", + "required": [ + "threadId" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_list_artifacts", + "description": "Lists all artifacts for a specific Pippin project", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "nextToken": { + "type": "string", + "description": "Pagination token" + }, + "projectId": { + "description": "Project ID", + "type": "string" + }, + "maxResults": { + "type": "number", + "description": "Maximum number of results to return" + } + }, + "required": [ + "projectId" + ], + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_accept_answer", + "description": "Accept an answer to a question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to mark an answer as accepted for a question.\nOnly the question owner or users with appropriate permissions can accept answers.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Marking the most helpful answer to your question\n- Indicating which solution resolved your issue\n- Helping others find the correct answer quickly\n\nExample usage:\n{ \"answerId\": 7654321 }", + "input_schema": { + "json": { + "properties": { + "answerId": { + "type": "number", + "description": "ID of the answer to accept" + } + }, + "type": "object", + "required": [ + "answerId" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_agent_script_get", + "description": "Access the complete content and metadata of specific agentic scripts using either file paths or script names. This tool retrieves the full script implementation along with structured metadata, enabling deep inspection of script functionality, parameter requirements, and operational logic before execution. The flexible lookup system supports both direct path access and name-based discovery across multiple script directories, with proper handling of script extensions. Essential for understanding script capabilities before integration into workflows.", + "input_schema": { + "json": { + "type": "object", + "properties": { + "name": { + "description": "Name of the script (with or without .script.md extension)", + "type": "string" + }, + "path": { + "type": "string", + "description": "Path to the script file" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_create_stage_version", + "description": "Creates a new stage version in G2S2 with the specified parent stage version", + "input_schema": { + "json": { + "type": "object", + "properties": { + "stageVersion": { + "type": "string", + "description": "The stage version to create" + }, + "parentStageVersion": { + "type": "string", + "description": "The parent stage version for the stage version" + } + }, + "additionalProperties": false, + "required": [ + "stageVersion", + "parentStageVersion" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_list_stage_version", + "description": "Lists contents of a specified G2S2 stage version", + "input_schema": { + "json": { + "required": [ + "stageVersion" + ], + "type": "object", + "properties": { + "stageVersion": { + "type": "string", + "description": "The stage version to list" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_add_comment", + "description": "Add a comment to a post on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to comment on questions or answers on Sage through the MCP interface.\nComments are useful for requesting clarification, providing additional context, or suggesting improvements.\nComments use plain text format (no Markdown support).\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Asking for clarification on a question or answer\n- Providing additional context or information\n- Suggesting improvements or alternatives\n\nExample usage:\n{ \"postId\": 1234567, \"contents\": \"Could you also explain how this works with custom dependencies?\" }", + "input_schema": { + "json": { + "required": [ + "postId", + "contents" + ], + "properties": { + "contents": { + "type": "string", + "description": "Content of the comment in plain text" + }, + "postId": { + "type": "number", + "description": "ID of the post (question or answer) to comment on" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_sync_project_to_remote", + "description": "Synchronizes local files to a Pippin project as artifacts", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "projectId", + "inputDirectory" + ], + "properties": { + "inputDirectory": { + "type": "string", + "description": "Local directory containing files to upload" + }, + "createMissing": { + "type": "boolean", + "default": true, + "description": "Create artifacts if they don't exist" + }, + "projectId": { + "type": "string", + "description": "Project ID" + }, + "nameFormat": { + "description": "How to name artifacts", + "enum": [ + "use_filename", + "use_id" + ], + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "marshal_get_report", + "description": "Retrieve Marshal Report.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", + "input_schema": { + "json": { + "required": [ + "reportId" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "reportId": { + "pattern": "^\\d+$", + "description": "The ID of the Marshal Report (numeric ID only, not the full URL)", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "marshal_search_insights", + "description": "Search Marshal Insights.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "properties": { + "relativeDateRangeMs": { + "type": "string", + "description": "Relative date range for search (e.g. last 1 hour, last 1 week) in milliseconds", + "pattern": "^\\d+$" + }, + "absoluteDateRangeStartDate": { + "pattern": "^\\d+$", + "type": "string", + "description": "Absolute date range for search start date in milliseconds since 1/1/1970" + }, + "managerAlias": { + "type": "string", + "description": "Manager Alias - returns all employees below" + }, + "absoluteDateRangeEndDate": { + "pattern": "^\\d+$", + "description": "Absolute date range for search end date in milliseconds since 1/1/1970", + "type": "string" + }, + "category": { + "type": "string", + "description": "Insight Category" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "slack_send_message", + "description": "Send a message to a specified Slack channel with optional thread support", + "input_schema": { + "json": { + "properties": { + "channelId": { + "type": "string", + "minLength": 1 + }, + "message": { + "type": "string", + "minLength": 1 + }, + "thread_ts": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "channelId", + "message" + ], + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_user_lookup", + "description": "This tool is for looking up users on the AWS Salesforce AKA AWSentral", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "user_name": { + "type": "string", + "description": "the name of the user" + }, + "user_id": { + "type": "string", + "description": "the id of the user" + }, + "email": { + "type": "string", + "description": "the email address of the user" + }, + "alias": { + "type": "string", + "description": "the alias of the user" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_import_stage_version", + "description": "Imports ion file into a specified G2S2 stage version", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "stageVersion": { + "type": "string", + "description": "The stage version to import into" + }, + "filepath": { + "type": "string", + "description": "The ion file path to import" + } + }, + "required": [ + "stageVersion", + "filepath" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_transition_issue", + "description": "Transition a JIRA issue to a new status", + "input_schema": { + "json": { + "required": [ + "issueIdOrKey", + "transitionId" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "comment": { + "type": "string", + "description": "Optional comment to add during transition" + }, + "transitionId": { + "minLength": 1, + "type": "string", + "description": "The ID of the transition" + }, + "fields": { + "additionalProperties": {}, + "description": "Optional fields to update during transition", + "type": "object" + }, + "issueIdOrKey": { + "description": "The ID or key of the issue", + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "list_work_contributions", + "description": "List work contributions from AtoZ PortfolioWidgetService.\n\nThis tool retrieves work contributions for a specific employee from AtoZ.\nYou must provide either ownerLogin or ownerPersonId to identify the employee.\n\nLimitations:\nYou can only access your own work contributions\n\nThe response includes work contributions with their details such as:\n- Title and summary\n- Edit status\n- Start and end dates\n- Associated artifacts\n- Stakeholders\n\nFor paginated results, you can use the nextToken parameter to retrieve subsequent pages.", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "ownerPersonId": { + "type": "string", + "description": "Person ID of the employee to get work contributions for" + }, + "ownerLogin": { + "description": "Login/alias of the employee to get work contributions for", + "type": "string" + }, + "nextToken": { + "description": "Token for pagination", + "type": "string" + }, + "maxResults": { + "type": "number", + "description": "Maximum number of results to return (default: 100)" + }, + "sortDirection": { + "description": "Sort direction (ASC or DESC, default: DESC)", + "type": "string", + "enum": [ + "ASC", + "DESC" + ] + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_list_knowledge", + "description": "Generate organized inventories of documents stored in the knowledge repository. This tool can list all documents or focus on specific folders, with options for recursive directory traversal and depth control. Returns document paths and titles, enabling systematic navigation of the knowledge structure.", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "properties": { + "folder": { + "type": "string", + "description": "The folder path to list documents from" + }, + "depth": { + "default": 5, + "type": "number", + "description": "How many levels deep to traverse" + }, + "recursive": { + "description": "Whether to include documents in subfolders", + "type": "boolean" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_get_issue", + "description": "Get a JIRA issue by ID or key", + "input_schema": { + "json": { + "required": [ + "issueIdOrKey" + ], + "properties": { + "issueIdOrKey": { + "minLength": 1, + "description": "The ID or key of the issue", + "type": "string" + }, + "expand": { + "description": "The additional information to include in the response", + "type": "string" + }, + "fields": { + "type": "array", + "description": "The list of fields to return", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "search_ags_confluence_website", + "description": "Search for Amazon Games Confluence pages\n\nThis tool allows you to search for content in the Amazon Games Confluence instance.\nYou can search for pages, blog posts, and other content across all spaces or within a specific space.\n\nParameters:\n- query: The search query string\n- page: (Optional) Page number for pagination (default: 1)\n- pageSize: (Optional) Number of results per page (default: 10, max: 50)\n- space: (Optional) Limit search to a specific Confluence space\n\nExamples:\n1. Basic search:\n { \"query\": \"game server architecture\" }\n\n2. Search with pagination:\n { \"query\": \"matchmaking\", \"page\": 2, \"pageSize\": 20 }\n\n3. Search in a specific space:\n { \"query\": \"deployment guide\", \"space\": \"GAMETECH\" }\n\nTips:\n- Use specific technical terms for more precise results\n- For recent content, sort by modification date\n- When looking for documentation, include terms like 'guide', 'documentation', or 'how-to'\n- For architecture documents, include terms like 'architecture', 'design', or 'diagram'\n- If you know the space key, use it to narrow down results", + "input_schema": { + "json": { + "properties": { + "query": { + "description": "Search query string", + "type": "string" + }, + "page": { + "description": "Page number for pagination (default: 1)", + "type": "number" + }, + "pageSize": { + "type": "number", + "description": "Number of results per page (default: 10, max: 50)" + }, + "space": { + "description": "Limit search to a specific Confluence space", + "type": "string" + } + }, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "required": [ + "query" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "overleaf_upload_file", + "description": "Upload a local file from the Overleaf workspace to the remote repository with automatic commit and push.\n\nThis tool reads an existing file from the local Overleaf workspace and uploads it to the remote repository.\nThe file must already exist in the local workspace directory (./overleaf/{project_id}/file_path).\nBefore uploading, it syncs the project to get latest changes and detects merge conflicts.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"figures/diagram.png\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "properties": { + "file_path": { + "description": "Path to the file within the project workspace", + "type": "string" + }, + "project_id": { + "description": "Project ID to upload to", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "project_id", + "file_path" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "search_quip", + "description": "Search for Quip threads\n\nThis tool allows you to search for Quip threads using keywords.\nResults are sorted by relevance and include document titles, links, and metadata.\n\nExamples:\n1. Basic search:\n```json\n{\n \"query\": \"expense report\"\n}\n```\n\n2. Search with limit:\n```json\n{\n \"query\": \"expense report\",\n \"count\": 5\n}\n```\n\n3. Search only in titles:\n```json\n{\n \"query\": \"expense report\",\n \"onlyMatchTitles\": true\n}\n```", + "input_schema": { + "json": { + "required": [ + "query" + ], + "properties": { + "count": { + "description": "Maximum number of results to return (default: 10, max: 50)", + "type": "number" + }, + "query": { + "type": "string", + "description": "Search query to find matching Quip threads" + }, + "onlyMatchTitles": { + "description": "If true, only search in document titles (default: false)", + "type": "boolean" + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_move_label", + "description": "Moves a stage version to a specified testing label", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "labelName", + "stageVersion" + ], + "properties": { + "stageVersion": { + "description": "The stage version from a parent label", + "type": "string" + }, + "labelName": { + "type": "string", + "description": "The label name of a testing label" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_change_records", + "description": "Modify records (also called config values) for a given feature (also called configuration) in Amazon Config Store.\nAllows adding, deleting, or modifying records with proper change tracking.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "properties": { + "changeSummary": { + "type": "string", + "description": "Summary of the changes being made" + }, + "records": { + "description": "Record changes to apply", + "properties": { + "recordChanges": { + "type": "array", + "items": { + "additionalProperties": false, + "properties": { + "attribute": { + "type": "string", + "description": "Name of the attribute being modified, if you are not sure what are the valid attributes for this feature, you can use acs_get_feature tool" + }, + "weblabRules": { + "description": "Optional weblab rules", + "items": { + "properties": { + "operands": { + "items": { + "description": "Weblab treatment identifier", + "type": "string" + }, + "type": "array" + }, + "use": { + "description": "Value for the attribute as stringified json", + "type": "string" + }, + "operator": { + "description": "Weblab rule operator", + "type": "string", + "enum": [ + "AND", + "OR" + ] + } + }, + "type": "object", + "required": [ + "operator", + "operands", + "use" + ], + "additionalProperties": false + }, + "type": "array" + }, + "contextualParameters": { + "additionalProperties": { + "description": "Config key value", + "type": "string" + }, + "description": "Each contextual parameter present in the feature schema must be included, if you are not sure what are the contextual parameters for this feature, you can use acs_get_feature tool", + "type": "object" + }, + "operationType": { + "enum": [ + "Upsert", + "Delete" + ], + "type": "string", + "description": "Operation type for the record change" + }, + "value": { + "type": "string", + "description": "New value for the attribute (required for Upsert operations) as stringified json, if you are not sure what is the expected value type for this feature, you can use acs_get_feature tool" + } + }, + "type": "object", + "required": [ + "operationType", + "contextualParameters", + "attribute" + ] + } + } + }, + "required": [ + "recordChanges" + ], + "type": "object", + "additionalProperties": false + }, + "crId": { + "type": "string", + "description": "Optional CR id to raise a new revision rather than making a new CR" + }, + "stage": { + "type": "string", + "description": "Stage to query", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ] + }, + "ticketLink": { + "type": "string", + "description": "Optional link to a ticket related to this change" + }, + "featureName": { + "type": "string", + "description": "Feature name to modify records for" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "featureName", + "records", + "changeSummary", + "stage" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "read_quip", + "description": "Read Quip document content\n\nThis tool retrieves the content of a Quip document in either HTML or Markdown format:\n\n- HTML format: More verbose but contains section IDs and additional metadata.\n These unique section IDs (for h1, h2, h3, p, etc.) can be used with the edit_quip tool\n to make targeted edits to specific sections of the document.\n\n- Markdown format: More concise and easier to read, but does not contain section IDs\n or additional metadata. Best for when you just need the content in a readable format\n and don't need to make targeted edits.\n\nWorkflow:\n1. Use read_quip to get the document content\n2. Identify the section ID you want to modify (when using HTML format)\n3. Use edit_quip with the section ID and appropriate location parameter\n\nExamples:\n1. Read document in HTML format (default):\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\"\n}\n```\n\n2. Read document in Markdown format:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"format\": \"markdown\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "format": { + "description": "Format to return the content in (html or markdown)", + "type": "string", + "enum": [ + "html", + "markdown" + ] + }, + "documentId": { + "type": "string", + "description": "The Quip document URL or ID to read" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "documentId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_get_feature", + "description": "Get detailed information about a specific feature (also called configuration) from Amazon Config Store.\nRetrieves full details of a feature including schema, owners, clients, and more, but not the records (config values), use acs_list_records for that.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "name" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "stage": { + "type": "string", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "default": "PROD", + "description": "Stage to query" + }, + "name": { + "description": "Feature name to retrieve", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_list_cp_records", + "description": "Get the records for a given contextual parameter (also called config key, or CP) from Amazon Config Store.\nRetrieves all records associated with a contextual parameter.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "properties": { + "stage": { + "description": "Stage to query", + "default": "PROD", + "type": "string", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ] + }, + "inRevision": { + "description": "Optional revision to retrieve records from", + "type": "string" + }, + "fromRevision": { + "type": "string", + "description": "Optional starting revision for retrieving records" + }, + "name": { + "type": "string", + "description": "Contextual parameter name to retrieve records for" + } + }, + "additionalProperties": false, + "required": [ + "name" + ], + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_get_contextual_parameter", + "description": "Get detailed information about a specific contextual parameter (also called config key, or CP) from Amazon Config Store.\nRetrieves full details of a contextual parameter including type, owners, status, and more, but not the records (config key values), use acs_list_contextual_parameter_records for that.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "name" + ], + "properties": { + "stage": { + "default": "PROD", + "type": "string", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "description": "Stage to query" + }, + "name": { + "type": "string", + "description": "Contextual parameter name to retrieve" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "search_fleet_credit_score", + "description": "Retrieve operational credit scores for fleets managed by a specific Amazon manager, identified by their alias.\n This tool should be used when you want to evaluate the operational performance and creditworthiness of all fleets under a given manager. \n The credit score here refers specifically to Amazon's internal fleet operational scoring system, **not** to a financial or consumer credit score.\n \n Each fleet's ID and associated operational credit score will be returned. \n These scores help in identifying at-risk fleets and evaluating performance for compliance, reliability, and delivery operations.\n\n ### Use Cases:\n • \"What are the credit scores of fleets managed by alias 'samishra@'?\"\n • \"Give me all fleet IDs and their scores under the manager 'samishra@'.\"\n \n ### When NOT to Use:\n • DO NOT use this tool to get personal or financial credit scores.\n • DO NOT use this tool if you don't have the manager alias.\n • NOT suitable for querying single fleet score (use a more targeted tool if available).\n \n ### Caveats:\n • Only works for Amazon-internal operational fleet credit score system.\n • The data may have a short refresh delay (up to 24 hours).\n • You must have permission to view data under the provided manager alias.", + "input_schema": { + "json": { + "properties": { + "alias": { + "description": "Manager alias to fetch credit scores for", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "alias" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_contact_lookup", + "description": "This tool is for looking up contacts on the AWS Salesforce AKA AWSentral", + "input_schema": { + "json": { + "type": "object", + "properties": { + "phone": { + "description": "the phone number of the contact", + "type": "string" + }, + "account_name": { + "type": "string", + "description": "the name of the account associated with the contact" + }, + "contact_name": { + "type": "string", + "description": "the name of the contact" + }, + "contact_id": { + "type": "string", + "description": "the id of the contact" + }, + "email": { + "description": "the email address of the contact", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "plantuml", + "description": "Create and decode PlantUML diagrams using Amazon's internal PlantUML server.\nPlantUML allows creating UML diagrams from text descriptions.\n\nSupported operations:\n- encode: Convert PlantUML text to an encoded URL\n- decode: Extract PlantUML text from an encoded URL", + "input_schema": { + "json": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "PlantUML content for encode operation" + }, + "operation": { + "enum": [ + "encode", + "decode" + ], + "description": "The operation to perform", + "type": "string" + }, + "url": { + "description": "PlantUML URL for decode operation", + "format": "uri", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "operation" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "create_folder_quip", + "description": "Create a new Quip folder\n\nThis tool creates a new folder in Quip.\nYou can optionally specify a parent folder to create a subfolder.\n\nExamples:\n1. Create a root-level folder:\n```json\n{\n \"title\": \"New Project Folder\"\n}\n```\n\n2. Create a subfolder:\n```json\n{\n \"title\": \"Documentation\",\n \"parentFolderId\": \"ABCDEF123456\"\n}\n```\n", + "input_schema": { + "json": { + "required": [ + "title" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "title": { + "type": "string", + "description": "Title of the new folder" + }, + "parentFolderId": { + "description": "ID of parent folder (if not provided, creates at root level)", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "list_katal_components", + "description": "List all available Katal components\n\nThis tool returns a list of all available components in the Katal library,\norganized by category with basic information about each component.\n\nExample usage:\n```json\n{}\n```", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": {}, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_create_project", + "description": "Creates a new Pippin design project with specified details", + "input_schema": { + "json": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "requirements": { + "type": "string", + "description": "Project requirements" + }, + "name": { + "description": "Project name", + "type": "string" + }, + "bindleId": { + "description": "Bindle ID", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_search_resources", + "description": "Search for resources in Amazon Config Store based on a query string.\nReturns matching features, contextual parameters, tags, or attributes based on the search criteria.\nThis retrieves only the metadata of the resource and not the full details.\nYou can optionally filter by resource types: FEATURE, CONTEXTUAL_PARAMETER, TAG, ATTRIBUTE.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "type": "object", + "properties": { + "queryString": { + "minLength": 1, + "description": "Search query string to find matching resources", + "type": "string" + }, + "stage": { + "type": "string", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "description": "Stage to query", + "default": "PROD" + }, + "resourceTypes": { + "description": "Optional filter for resource types to search", + "type": "array", + "items": { + "enum": [ + "FEATURE", + "CONTEXTUAL_PARAMETER", + "TAG", + "ATTRIBUTE" + ], + "type": "string" + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "queryString" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_config_helper", + "description": "Get help configuring JIRA tools for Q CLI", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "token": { + "description": "Your JIRA token (optional - for validation)", + "type": "string" + }, + "jira_url": { + "type": "string", + "description": "Your JIRA instance URL (optional - for validation)" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "search_quip_created_by_current_user", + "description": "Get all documents created by the current user\n\nThis tool retrieves all Quip documents that were created by the current user.\nYou can optionally filter the results by date range to get documents created within a specific time period.\n\nThe tool fetches all user threads, then filters them to show only documents authored by the current user,\nwith optional date range filtering for more targeted results.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents created by current user:\n```json\n{\n}\n```\n\n2. Get documents created within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```\n\n3. Get documents created after a specific date:\n```json\n{\n \"startDate\": \"2024-06-01\"\n}\n```\n\n4. Get documents created before a specific date:\n```json\n{\n \"endDate\": \"2024-06-30\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "startDate": { + "description": "Start date for filtering documents (YYYY-MM-DD format)", + "type": "string" + }, + "endDate": { + "type": "string", + "description": "End date for filtering documents (YYYY-MM-DD format)" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_read_knowledge", + "description": "Access and retrieve the full content of knowledge documents using either a file path or document title. This tool enables direct retrieval of stored knowledge resources from the configured knowledge base, supporting both absolute and relative paths. Returns the document content along with path and title metadata.", + "input_schema": { + "json": { + "properties": { + "title": { + "type": "string", + "description": "The title of the document to find" + }, + "path": { + "type": "string", + "description": "The path to the document file" + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "orca_get_execution_data", + "description": "Get execution data for a specific run in Orca Studio.\n\nExecution data is a key-value map (Shared Data) that is specified as\na payload for work items (workflow instances) and output artifacts\ngenerated during a workflow run. This tool is useful for debugging \nworkflow issues, extracting processed data from completed runs,\nor analyzing the data flow through specific workflow executions.\n\nThis tool retrieves detailed execution data including execution data map\nfor a specific runId within an objectId.\n\nLimitations:\n- If the Execution data is large it could cause performance issues\n- Supported classification of data is until orange\n- Large datasets may experience timeout issues (default 60s timeout)\n\nParameters:\n- objectId: (required) The object ID\n- workflowName: (required) The workflow name\n- runId: (required) The specific run ID to get data for\n- clientId: (required) The Orca client ID\n- region: (optional) AWS region (defaults to us-east-1)\n\nExample:\n```json\n{ \"objectId\": \"d7f71182-d7b8-4886-8d07-15c404a82583\", \"workflowName\": \"GenerateReportForNCA-beta\", \"runId\": \"b9d9c02a-d3f0-4da8-9601-1740f1aaaeae\", \"clientId\": \"SafrReportingSILServiceBeta\" }\n```", + "input_schema": { + "json": { + "properties": { + "objectId": { + "description": "The object ID", + "type": "string" + }, + "workflowName": { + "description": "The workflow name", + "type": "string" + }, + "runId": { + "type": "string", + "description": "The specific run ID to get data for" + }, + "clientId": { + "type": "string", + "description": "The Orca client ID" + }, + "region": { + "type": "string", + "description": "AWS region (defaults to us-east-1)" + } + }, + "type": "object", + "additionalProperties": false, + "required": [ + "objectId", + "workflowName", + "runId", + "clientId" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_create_contextual_parameter", + "description": "Creates a new contextual parameter (also called config key, or CP) in Amazon Config Store.\nThis tool allows creating a contextual parameter with the specified name, owners, approvers, and other attributes.\nThe name of the contextual parameter should be unique.\nThe contextual parameter will be created in SANDBOX stage, it can then be promoted to other stages from the UI after it is approved by ACS team.\nIf any of the required parameters are not provided, you MUST ASK the user for them.", + "input_schema": { + "json": { + "required": [ + "name", + "owners", + "cti", + "description", + "approximateMaxRecordsCount", + "exampleValues" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "owners": { + "minItems": 1, + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "type": { + "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", + "enum": [ + "BINDLE", + "TEAM", + "POSIX_GROUP", + "AAA" + ], + "type": "string" + }, + "name": { + "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + } + }, + "required": [ + "type", + "name" + ] + }, + "description": "List of owners. Do NOT assume this info, you MUST ask the user about it." + }, + "cti": { + "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", + "required": [ + "category", + "type", + "item" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "category": { + "description": "CTI category. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + }, + "type": { + "type": "string", + "description": "CTI type. Do NOT assume this info, you MUST ask the user about it." + }, + "item": { + "description": "CTI item. Do NOT assume this info, you MUST ask the user about it.", + "type": "string" + } + } + }, + "validations": { + "items": { + "anyOf": [ + { + "required": [ + "kind", + "targetAttributes", + "regex" + ], + "properties": { + "kind": { + "description": "For string attributes, which validates attributes against a predefined regex.", + "type": "string", + "const": "Pattern" + }, + "targetAttributes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + }, + "regex": { + "description": "Regex pattern", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + { + "properties": { + "kind": { + "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values).", + "type": "string", + "const": "Range" + }, + "targetAttributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + }, + "min": { + "description": "Range minimum value (inclusive)", + "type": "string" + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + }, + "max": { + "type": "string", + "description": "Range maximum value (inclusive)" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "kind", + "targetAttributes" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "targetAttributes": { + "type": "array", + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", + "items": { + "type": "string" + } + }, + "kind": { + "const": "NonNull", + "type": "string", + "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes." + }, + "description": { + "description": "Contains the ACS customer explanation for a given validation.", + "type": "string" + } + }, + "required": [ + "kind", + "targetAttributes" + ] + }, + { + "required": [ + "kind", + "targetAttributes", + "arn" + ], + "additionalProperties": false, + "properties": { + "kind": { + "const": "Lambda", + "type": "string", + "description": "Lambda validation allows you to use your own custom logic to validate record values." + }, + "targetAttributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" + }, + "arn": { + "description": "Lambda ARN", + "type": "string" + }, + "description": { + "description": "Description of what the Lambda validates.", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "minItems": 0, + "description": "Validations for the contextual parameter records", + "type": "array" + }, + "exampleValues": { + "type": "string", + "minLength": 1, + "description": "Example of values this contextual parameters is going to hold. Comma separated strings: example_1, example_2... and so on." + }, + "description": { + "description": "Description of the contextual parameter", + "type": "string", + "minLength": 1 + }, + "approvers": { + "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." + }, + "type": { + "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", + "type": "string", + "enum": [ + "USER", + "LDAP", + "POSIXG", + "TEAM", + "SNS" + ] + }, + "requiredCount": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Required count of approvers" + } + }, + "required": [ + "type", + "name" + ] + } + }, + "approximateMaxRecordsCount": { + "type": "integer", + "description": "Cardinality of the contextual parameter, how many records this contextual parameter is going to hold on the long term.", + "minimum": 1 + }, + "crFeatureEnabled": { + "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", + "default": true, + "type": "boolean" + }, + "parentKeys": { + "description": "Parent contextual parameters for this contextual parameter. This is only needed when you are creating a composite contextual parameter", + "type": "string" + }, + "name": { + "description": "Name of the contextual parameter to create. It should be in snake_case.", + "minLength": 1, + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "get_work_contribution", + "description": "Get a specific work contribution by ID from AtoZ PortfolioWidgetService.\n\nLimitations:\nYou can only access your own work contributions\n\nThis tool retrieves detailed information about a work contribution, including:\n- Title and summary\n- Edit status\n- Start and end dates\n- Associated artifacts\n- Stakeholders\n\nYou must provide the work contribution ID to retrieve the details.", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "workContributionId": { + "type": "string", + "description": "The ID of the work contribution to retrieve" + } + }, + "required": [ + "workContributionId" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_list_projects", + "description": "Lists all available Pippin design projects", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "properties": { + "maxResults": { + "description": "Maximum number of results to return", + "type": "number" + }, + "nextToken": { + "type": "string", + "description": "Pagination token" + }, + "statuses": { + "type": "string", + "description": "Project statuses to filter by" + }, + "user": { + "description": "User to filter by", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "mox_console", + "description": "Access the MOX console to fetch order data from MORSE service", + "input_schema": { + "json": { + "type": "object", + "properties": { + "merchantCustomerId": { + "type": [ + "string", + "number" + ], + "description": "The merchant customer ID (e.g., 994273326)" + }, + "retrievePromotions": { + "description": "Whether to retrieve promotions", + "type": "boolean", + "default": true + }, + "orderIds": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "The order ID(s) to retrieve. Can be a single order ID or an array of order IDs." + }, + "operation": { + "enum": [ + "getOrderDetailsNonUCI" + ], + "description": "The operation to perform. Available operations: getOrderDetailsNonUCI", + "type": "string" + }, + "retrieveOrderReportData": { + "type": "boolean", + "description": "Whether to retrieve order report data", + "default": true + }, + "hostname": { + "type": "string", + "description": "Optional custom hostname for the API endpoint" + }, + "region": { + "default": "USAmazon", + "description": "The region to use for the API endpoint (USAmazon, EUAmazon, JPAmazon)", + "type": "string", + "enum": [ + "USAmazon", + "EUAmazon", + "JPAmazon" + ] + }, + "retrieveExtendedItemFields": { + "type": "boolean", + "description": "Whether to retrieve extended item fields", + "default": true + } + }, + "additionalProperties": false, + "required": [ + "operation", + "merchantCustomerId", + "orderIds" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "add_work_contribution_stakeholder", + "description": "Add a stakeholder to a work contribution in AtoZ.\n\nThis tool allows you to add a stakeholder (collaborator) to an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- stakeholderLogin or stakeholderPersonId: The stakeholder to add\n- ownerLogin or ownerPersonId: The owner of the work contribution", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "ownerPersonId": { + "type": "string", + "description": "Person ID of the work contribution owner" + }, + "stakeholderLogin": { + "type": "string", + "description": "Login/alias of the stakeholder to add" + }, + "workContributionId": { + "type": "string", + "description": "ID of the work contribution" + }, + "stakeholderPersonId": { + "type": "string", + "description": "Person ID of the stakeholder to add" + }, + "ownerLogin": { + "description": "Login/alias of the work contribution owner", + "type": "string" + } + }, + "required": [ + "workContributionId" + ], + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "g2s2_create_cr", + "description": "Creates a code review for a specified G2S2 stage version", + "input_schema": { + "json": { + "required": [ + "stageVersion", + "description" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "crId": { + "type": "string", + "description": "Existing CR ID to update (optional)" + }, + "description": { + "description": "A CR description to add", + "type": "string" + }, + "stageVersion": { + "description": "The stage version to create a code review for", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "oncall_compass_query_reports", + "description": "Query Oncall reports from Oncall Compass (https://oncall.ai.amazon.dev/). Currently it will return most recently generated reports by the user. The user's authentication token (~/.midway/cookie) will be used for identifying the user.", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": {} + } + } + } + }, + { + "ToolSpecification": { + "name": "policy_engine_get_risk", + "description": "Access Amazon Policy Engine risk information for a specific entity. This tool allows you to retrieve detailed information about a specific risk or violation from Policy Engine.", + "input_schema": { + "json": { + "type": "object", + "required": [ + "entityId" + ], + "properties": { + "entityId": { + "description": "Entity ID of the risk/violation to view details for", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_get_artifact", + "description": "Retrieves a specific Pippin artifact by its ID", + "input_schema": { + "json": { + "additionalProperties": false, + "properties": { + "projectId": { + "type": "string", + "description": "Project ID" + }, + "designId": { + "type": "string", + "description": "Artifact ID" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "projectId", + "designId" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "get_katal_component", + "description": "Get detailed information about a specific Katal component\n\nThis tool retrieves comprehensive documentation and usage information for a given Katal component,\nincluding properties, methods, examples, guidelines, and accessibility information.\n\nExamples:\n1. Get Button component info:\n```json\n{\n \"name\": \"Button\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the Katal component to get information about" + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_update_feature", + "description": "Updates a feature (also called configuration) in Amazon Config Store.\nThis tool allows updating a feature by only giving it the parameters required to be updated, other parameters that are not provided will remain as is.\nIf any of the required parameters are not provided, do NOT assume them, just leave them empty.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "type": "object", + "required": [ + "name", + "stage" + ], + "properties": { + "name": { + "description": "Name of the feature to update. It should be in PascalCase.", + "type": "string", + "minLength": 1 + }, + "teamName": { + "type": "string", + "description": "Team name responsible for the feature. Do NOT assume this info, you MUST ask the user about it." + }, + "teamWikiLink": { + "type": "string", + "description": "Team wiki link. Do NOT assume this info, you MUST ask the user about it." + }, + "configSnapshotEnabled": { + "type": "boolean", + "description": "Whether config snapshot is enabled. Config snapshot allows the user to use deployable cache and dynamic refresher for example. Know more about deployable cache from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/OnBoarding/Cache/#HDeployablecache and dynamic refresher from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/DynamicRefresher." + }, + "cti": { + "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", + "additionalProperties": false, + "properties": { + "category": { + "type": "string", + "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." + }, + "item": { + "type": "string", + "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." + }, + "type": { + "type": "string", + "description": "CTI type. Do NOT assume this info, you MUST ask the user about it." + } + }, + "type": "object", + "required": [ + "category", + "type", + "item" + ] + }, + "approvers": { + "items": { + "required": [ + "type", + "name" + ], + "additionalProperties": false, + "type": "object", + "properties": { + "requiredCount": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Required count of approvers" + }, + "name": { + "type": "string", + "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." + }, + "type": { + "type": "string", + "enum": [ + "USER", + "LDAP", + "POSIXG", + "TEAM", + "SNS" + ], + "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it." + } + } + }, + "type": "array", + "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", + "minItems": 1 + }, + "upsertedAttributes": { + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Description for the attribute" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "description": "Name of the attribute in snake_case" + }, + "type": { + "description": "Type of the attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"", + "type": "string" + } + }, + "required": [ + "name", + "type" + ] + }, + "type": "array", + "description": "Attributes of the feature. Attributes are the fields of your config table, the value of these attributes can vary depending on contextual parameter values." + }, + "crFeatureEnabled": { + "type": "boolean", + "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true." + }, + "owners": { + "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", + "type": "array", + "items": { + "properties": { + "type": { + "type": "string", + "enum": [ + "BINDLE", + "TEAM", + "POSIX_GROUP", + "AAA" + ], + "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it." + }, + "name": { + "type": "string", + "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it." + } + }, + "additionalProperties": false, + "required": [ + "type", + "name" + ], + "type": "object" + } + }, + "upsertedNamedTypes": { + "items": { + "properties": { + "type": { + "description": "Type definition", + "anyOf": [ + { + "type": "object", + "properties": { + "values": { + "minItems": 1, + "description": "Enum values. Values must be in snake_case.", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" + } + }, + "kind": { + "description": "Enum type used as a type for an attribute.", + "type": "string", + "const": "Enum" + } + }, + "required": [ + "kind", + "values" + ], + "additionalProperties": false + }, + { + "required": [ + "kind", + "attributes" + ], + "type": "object", + "properties": { + "attributes": { + "description": "Struct attributes", + "minItems": 1, + "type": "array", + "items": { + "required": [ + "name", + "type" + ], + "additionalProperties": false, + "properties": { + "description": { + "description": "Description of the struct attribute.", + "type": "string" + }, + "name": { + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", + "type": "string", + "description": "Name of the struct attribute. It should be in snake_case." + }, + "type": { + "type": "string", + "description": "Type of the struct attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" + } + }, + "type": "object" + } + }, + "kind": { + "const": "Struct", + "type": "string", + "description": "Struct type used as a type for an attribute." + } + }, + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "required": [ + "kind", + "element" + ], + "properties": { + "kind": { + "description": "List type used as a type for an attribute.", + "const": "List", + "type": "string" + }, + "element": { + "description": "Type of the list element: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"", + "type": "string" + } + } + } + ] + }, + "description": { + "description": "Description for the custom type", + "type": "string" + }, + "name": { + "description": "Name of the custom type", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "type" + ] + }, + "description": "Custom types for the feature that can be used as schema attribute type, struct attribute type, or list element type", + "type": "array" + }, + "stage": { + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ], + "description": "Stage to query", + "type": "string" + }, + "description": { + "type": "string", + "description": "Description of the feature" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_opportunity_lookup", + "description": "This tool is for looking up opportunities on the AWS Salesforce AKA AWSentral", + "input_schema": { + "json": { + "properties": { + "opportunity_id": { + "description": "the id of the opportunity - this will only pull the 1 opportunity", + "type": "string" + }, + "account_name": { + "type": "string", + "description": "the name of the account with the opportunities, this will pull all opportunities that may be related to an account, but not directly associated." + }, + "opportunity_name": { + "description": "the name of the opportunity to search for", + "type": "string" + }, + "account_id": { + "type": "string", + "description": "the id of the account associated with the opportunity, this will pull all opportunities on an account, its best to use just the account_id" + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "orca_list_runs_for_objectId", + "description": "List all runs for a specific objectId in Orca Studio.\n\nAn objectId in Orca Studio represents a unique ID assigned to a single Execution.\nSince a single Execution can have multiple runs, the Object ID allows aggregation\nat a business process instance level. Use this tool when you need to\ntrack all workflow executions related to a specific object across different\nworkflows, rather than listing runs for a specific workflow.\n\nThis tool retrieves all execution runs associated with a given objectId,\nincluding runId, status, openedDate, and closedDate for each run.\n\nLimitations:\n- Results are limited to the most recent runs that haven't been deleted by retention policies (typically last 100)\n- Large datasets may experience timeout issues (default 60s timeout)\n\nParameters:\n- objectId: (required) The object ID to query runs for\n- clientId: (required) The Orca client ID\n- region: (optional) AWS region (defaults to us-east-1)\n\nExample:\n```json\n{ \"objectId\": \"d7f71182-d7b8-4886-8d07-15c404a82583\", \"clientId\": \"SafrReportingSILServiceBeta\" }\n```", + "input_schema": { + "json": { + "required": [ + "objectId", + "clientId" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "objectId": { + "type": "string", + "description": "The object ID to query runs for" + }, + "clientId": { + "type": "string", + "description": "The Orca client ID" + }, + "region": { + "description": "AWS region (defaults to us-east-1)", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_search_issues", + "description": "Search for JIRA issues using JQL", + "input_schema": { + "json": { + "properties": { + "jql": { + "type": "string", + "minLength": 1, + "description": "JQL search query" + }, + "expand": { + "type": "string", + "description": "The additional information to include in the response" + }, + "startAt": { + "description": "The index of the first result to return (0-based)", + "type": "integer", + "minimum": 0 + }, + "maxResults": { + "minimum": 1, + "type": "integer", + "maximum": 1000, + "description": "The maximum number of results to return (default: 50)" + }, + "fields": { + "description": "The list of fields to return", + "items": { + "type": "string" + }, + "type": "array" + }, + "validateQuery": { + "description": "Whether to validate the JQL query", + "type": "string" + } + }, + "required": [ + "jql" + ], + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_search_tags", + "description": "Search for tags on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to find appropriate tags for categorizing questions on Sage.\nTags help organize questions and ensure they reach the right audience.\nResults are paginated and sorted by popularity by default.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Finding relevant tags before creating a question\n- Discovering tags related to specific technologies or teams\n- Exploring popular tags in a particular domain\n\nExample usage:\n{ \"nameFilter\": \"brazil\", \"page\": 1, \"pageSize\": 10 }", + "input_schema": { + "json": { + "properties": { + "nameFilter": { + "description": "Optional filter to search for tags by name", + "type": "string" + }, + "pageSize": { + "type": "number", + "description": "Number of results per page (default: 60)" + }, + "page": { + "description": "Page number for pagination (starts at 1)", + "type": "number" + } + }, + "additionalProperties": false, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "search_people", + "description": "Search for Amazon employees with filtering by attributes like job level, location, and Bar Raiser/Manager status. This tool allows you to search for people by name, alias, or other criteria, and filter results by department, location, job level, Bar Raiser status, Manager status, and more. The tool also provides information of the employee like phoneNumber, email, buildingRoom if available in phoneTool.", + "input_schema": { + "json": { + "additionalProperties": false, + "properties": { + "query": { + "type": "string", + "description": "Search query for finding people (name, alias, etc.)" + }, + "filters": { + "type": "object", + "properties": { + "isBarRaiser": { + "description": "Filter for bar raisers (true) or non-bar raisers (false)", + "type": "boolean" + }, + "department": { + "description": "Filter by department name (e.g., 'AWS', 'Consumables CX - Tech')", + "type": "string" + }, + "city": { + "description": "Filter by city name (e.g., 'Seattle', 'Dallas')", + "type": "string" + }, + "country": { + "type": "string", + "description": "Filter by country code (e.g., 'us', 'in', 'ca')" + }, + "isManager": { + "type": "boolean", + "description": "Filter for managers (true) or individual contributors (false)" + }, + "badgeCode": { + "description": "Filter by badge code (e.g., 'F')", + "type": "string" + }, + "title": { + "description": "Filter by job title (e.g., 'Software Development Engineer', 'Sr. Partner SA, Oracle')", + "type": "string" + }, + "building": { + "type": "string", + "description": "Filter by building code (e.g., 'SEA20', 'BLR13')" + }, + "jobLevel": { + "type": "string", + "description": "Filter by job level (e.g., '4', '5', '6')" + }, + "badgeBorderColor": { + "type": "string", + "description": "Filter by badge border color (e.g., 'blue')" + } + }, + "description": "Filters to narrow down search results", + "additionalProperties": false + }, + "maxResults": { + "type": "number", + "description": "Maximum number of results to return (default: 10)" + } + }, + "type": "object", + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "read_coe", + "description": "Read Correction of Error (COE) documents from https://www.coe.a2z.com/.\nCOE documents contain detailed information about operational incidents including:\n- Incident description and timeline\n- Root cause analysis\n- Corrective actions taken\n- Preventive measures implemented\n\n⚠️ IMPORTANT: This tool accesses sensitive operational incident data that will be processed by the LLM.\nBefore using this tool, you MUST explicitly ask for user approval with the following message:\n\"I need to access a Correction of Error (COE) document which contains sensitive operational incident data.\nThis data will be processed by the LLM to answer your question. Do you approve accessing this COE document?\"\n\nOnly proceed if the user explicitly approves. This confirmation is required even if the tool is auto-approved.\n\nExample usage:\nTo read a COE document with ID 12345:\n{ \"url\": \"https://www.coe.a2z.com/coe/12345\" }", + "input_schema": { + "json": { + "type": "object", + "properties": { + "url": { + "format": "uri", + "description": "URL of the COE document to read", + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "lookup_user_coding_activity_summary", + "description": "Looks up coding activity summary for a given user by their user login/alias", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "alias" + ], + "properties": { + "alias": { + "description": "Alias or login for the user to look up", + "type": "string" + }, + "start_time": { + "description": "Optional start date in YYYY-MM-DD format", + "type": "string" + }, + "end_time": { + "description": "Optional end date in YYYY-MM-DD format", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "lookup_team_code_resource", + "description": "Looks up code artifacts, such as packages, version sets a given team", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "type": "object", + "properties": { + "team": { + "description": "Bindle team as represented in https://permissions.amazon.com/a/team/{team}", + "type": "string" + } + }, + "required": [ + "team" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "search_datapath", + "description": "Search Datapath views", + "input_schema": { + "json": { + "required": [ + "query" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "query": { + "description": "Keywords to look for in the Datapath view, for example \"locality asin\" will find the locality views at asin level", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "lock_unlock_quip_document", + "description": "Lock or unlock a Quip document\n\nThis tool allows you to lock or unlock a Quip document to control whether it can be edited.\nWhen a document is locked, users cannot make changes to it (except for the document owner and users with admin privileges).\n\nExample usage:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"lock\": true\n}\n```\n\nTo unlock a document:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"lock\": false\n}\n```\n\nNote: You must have appropriate permissions to lock or unlock a document.", + "input_schema": { + "json": { + "required": [ + "threadIdOrUrl", + "lock" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "threadIdOrUrl": { + "description": "The thread ID or Quip URL of the document to lock or unlock", + "type": "string" + }, + "lock": { + "description": "Tick the checkbox to lock or uncheck to unlock the document", + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "read_orr", + "description": "Read Operational Readiness Review (ORR) documents from https://www.orr.reflect.aws.dev/.\nORR documents contain detailed information about operational readiness reviews including:\n- Review questions and answers\n- Service or feature assessments\n- Operational readiness criteria\n- Launch approval status\n\n⚠️ IMPORTANT: This tool accesses sensitive operational review data that will be processed by the LLM.\nBefore using this tool, you MUST explicitly ask for user approval with the following message:\n\"I need to access an Operational Readiness Review (ORR) document which contains sensitive operational data.\nThis data will be processed by the LLM to answer your question. Do you approve accessing this ORR document?\"\n\nOnly proceed if the user explicitly approves. This confirmation is required even if the tool is auto-approved.\n\nExample usage:\nTo read an ORR document with a specific review ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/review/687e56b9-d3d4-4bd5-b033-379461c96381/questions\" }\n\nTo read an ORR template:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\" }\n\nTo read only a specific section by ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"sectionId\": \"53886aad-5ef9-4450-9da0-de7365ef07cb\" }\n\nTo read only a specific section by title:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"sectionTitle\": \"Axiom 01 - AZ Resilience\" }\n\nTo read only a specific question by ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"questionId\": \"039ee146-7a05-4e4f-b10e-4eebb574f093\" }\n\nTo read only a specific question by prompt text (supports partial matching):\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"questionPrompt\": \"AZ failure\" }", + "input_schema": { + "json": { + "type": "object", + "required": [ + "url" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "sectionTitle": { + "type": "string", + "description": "Optional title of a specific section to return" + }, + "sectionId": { + "description": "Optional ID of a specific section to return", + "type": "string" + }, + "questionPrompt": { + "type": "string", + "description": "Optional prompt text to search for in questions (supports partial matching)" + }, + "url": { + "format": "uri", + "description": "URL of the ORR document to read", + "type": "string" + }, + "questionId": { + "type": "string", + "description": "Optional ID of a specific question to return" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "figma_to_code", + "description": "Generate code from Figma mockups and designs via Alchemy API.\nSupports multiple output formats including React, HTML and Storm UI.\nAnalyzes Figma design data to generate production-ready code.", + "input_schema": { + "json": { + "required": [ + "figmaUrl" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "figmaUrl": { + "type": "string", + "format": "uri", + "description": "Figma URL containing the design to convert to code" + }, + "outputFormat": { + "description": "Output code format/framework", + "type": "string", + "enum": [ + "react", + "html", + "storm-ui" + ], + "default": "react" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "rtla_fetch_single_request_logs", + "description": "Fetch detailed logs for a single request from RTLA (Real-Time Log Analysis) API. This tool allows you to retrieve comprehensive log entries for a specific request ID, including error logs, stack traces, and detailed request information. The response is automatically filtered to include only essential debugging fields for easier analysis. Useful for deep-dive troubleshooting of specific issues, analyzing error patterns for individual requests, and getting complete context for failed transactions.", + "input_schema": { + "json": { + "required": [ + "org", + "requestType", + "date", + "requestId" + ], + "properties": { + "org": { + "description": "Organization identifier (e.g., \"CWCBCCECMPROD\")", + "type": "string" + }, + "requestType": { + "type": "string", + "description": "Type of request logs to retrieve (e.g., \"FATAL\", \"NONFATAL\")" + }, + "date": { + "type": "number", + "description": "Date in milliseconds since epoch when the request occurred" + }, + "requestId": { + "type": "string", + "description": "Specific request ID to fetch logs for (e.g., \"GHHJD10YZDJNXT062G2X\")" + }, + "identifyAdditionalOrgs": { + "description": "Whether to identify additional organizations related to this request", + "type": "boolean", + "default": true + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "post_talos_correspondence", + "description": "Post correspondence on a Talos security task\n\nThis tool allows posting comments/correspondence on a specific Talos security task.\nIt uses the Talos API to create new correspondence entries for tasks.\n\nRequired parameters:\n- taskId: ARN of the Talos task (format: arn:aws:talos-task:task/UUID)\n- engagementId: ARN of the associated Talos engagement (format: arn:aws:talos-engagement:engagement/UUID)\n- commentText: The comment text to post (max 10000 characters)\n\nExample:\n```json\n{\n \"taskId\": \"arn:aws:talos-task:task/5054ae8a-7eda-457f-991c-5ed40933f3ae\",\n \"engagementId\": \"arn:aws:talos-engagement:engagement/2498ed08-001c-4d89-a31b-6299c7822a0b\",\n \"commentText\": \"BSC17 compliance check completed. Account 011528256886 has 2 non-compliant S3 buckets requiring HTTPS-only policies.\"\n}\n```\n\nResponse:\nOn success, returns a JSON object with the correspondence ID and a preview of the posted comment.\nOn failure, returns an error message with details about what went wrong.\n\nLimitations and Requirements:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n- Limited to 10 requests per minute per user (rate limit)\n- Comments cannot be edited or deleted through this tool once posted\n- User must have appropriate permissions to access the specified Talos task and engagement\n- Task and engagement must exist and be in a valid state to accept comments\n\nWhen NOT to use this tool:\n- Do not use for posting sensitive or classified information that should not be stored in Talos\n- Do not use for posting large attachments or binary data (use the Talos UI directly instead)\n- Do not use for bulk commenting on multiple tasks (use the Talos UI or API directly for batch operations)\n- Do not use for retrieving task information (use the talos_get_task tool instead)", + "input_schema": { + "json": { + "type": "object", + "properties": { + "engagementId": { + "description": "ARN of the associated Talos engagement", + "pattern": "^arn:aws:talos-engagement:engagement\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string", + "minLength": 1 + }, + "commentText": { + "minLength": 1, + "maxLength": 10000, + "type": "string", + "description": "The comment text to post" + }, + "taskId": { + "minLength": 1, + "description": "ARN of the Talos task to post comment to", + "type": "string", + "pattern": "^arn:aws:talos-task:task\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + } + }, + "required": [ + "taskId", + "engagementId", + "commentText" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_sync_project_to_local", + "description": "Synchronizes a Pippin project's artifacts to a local directory", + "input_schema": { + "json": { + "type": "object", + "required": [ + "projectId", + "outputDirectory" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "projectId": { + "description": "Project ID", + "type": "string" + }, + "includeMetadata": { + "type": "boolean", + "description": "Include metadata files (.meta.json)" + }, + "outputDirectory": { + "description": "Local directory to save artifacts", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "admiral_instance_timeline", + "description": "Fetch and parse the timeline of an EC2 instance from Admiral.\nAdmiral is an internal Amazon tool that provides information about EC2 instances.\nThis tool is useful for troubleshooting AWS EC2 instances.\n\nParameters:\n- region: (optional) Airport code for AWS region (e.g., iad, pdx, sfo). Defaults to 'iad'.\n- instance_id: (required) EC2 instance ID (e.g., i-0285d2cffe9d1958d).", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "instance_id" + ], + "properties": { + "instance_id": { + "description": "EC2 instance ID (e.g., i-0285d2cffe9d1958d)", + "type": "string" + }, + "region": { + "default": "iad", + "description": "Airport code for AWS region (e.g., iad, pdx, sfo)", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "get_recent_messages_quip", + "description": "Get recent messages from a Quip thread\n\nThis tool retrieves the most recent messages for a given Quip thread.\nYou can filter and sort the messages using various parameters.\n\nExamples:\n1. Get recent messages:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\"\n}\n```\n\n2. Get recent messages with count:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"count\": 10\n}\n```\n\n3. Get recent edit messages:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"messageType\": \"edit\"\n}\n```", + "input_schema": { + "json": { + "required": [ + "threadIdOrUrl" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "updatedSinceUsec": { + "type": "number", + "description": "UNIX timestamp in microseconds for messages updated at and after" + }, + "sortedBy": { + "enum": [ + "ASC", + "DESC" + ], + "description": "Sort order for messages", + "type": "string" + }, + "maxCreatedUsec": { + "type": "number", + "description": "UNIX timestamp in microseconds for messages created at and before" + }, + "sortBy": { + "type": "string", + "description": "Alias for sortedBy", + "enum": [ + "ASC", + "DESC" + ] + }, + "count": { + "type": "number", + "description": "Number of messages to return (1-100, default 25)" + }, + "lastUpdatedSinceUsec": { + "description": "UNIX timestamp in microseconds for messages updated before", + "type": "number" + }, + "threadIdOrUrl": { + "description": "The thread ID or Quip URL to get messages from", + "type": "string" + }, + "messageType": { + "description": "Type of messages to return", + "type": "string", + "enum": [ + "message", + "edit" + ] + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "search_resilience_score", + "description": "Search for resiliency scores for a manager's alias.\n • Required: manager alias\n • Optional: page size, page number, and score version\n • Returns resiliency score data for services under the specified manager", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "alias" + ], + "type": "object", + "properties": { + "pageSize": { + "type": "number", + "description": "Number of results per page (default: 4000)" + }, + "pageNumber": { + "type": "number", + "description": "Page number to fetch (default: 0)" + }, + "scoreVersion": { + "description": "Version of the score to fetch (default: 0.7.0)", + "type": "string" + }, + "alias": { + "type": "string", + "description": "Manager alias to fetch resiliency scores for" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_get_project", + "description": "Retrieves a Pippin design project by its ID", + "input_schema": { + "json": { + "additionalProperties": false, + "required": [ + "projectId" + ], + "type": "object", + "properties": { + "projectId": { + "type": "string", + "description": "Project ID" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_territory_lookup", + "description": "This tool is for looking up territories and retrieving an account list on the AWS Salesforce AKA AWSentral", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "territory_name": { + "type": "string", + "description": "the name of the territory to search for" + }, + "territory_id": { + "description": "the id of the territory to retrieve", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_get_tag_details", + "description": "Get detailed information about a specific tag on Sage (Amazon's internal Q&A platform).\n\nThis tool retrieves comprehensive information about a tag, including its ID, description, and ownership.\nUse this information when creating questions to ensure proper tag usage.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Verifying tag ownership before using it\n- Getting detailed descriptions of tags\n- Finding contact information for tag owners\n\nExample usage:\n{ \"tagName\": \"brazil\" }", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "required": [ + "tagName" + ], + "properties": { + "tagName": { + "type": "string", + "description": "Name of the tag to retrieve details for" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_get_knowledge_metadata", + "description": "Extract comprehensive metadata from knowledge documents, including YAML frontmatter, tags, internal links, tasks, headings, and file attributes. This tool provides structural and organizational information about documents without retrieving the full content, supporting knowledge management and document analysis workflows.", + "input_schema": { + "json": { + "properties": { + "path": { + "description": "The path to the document file", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "path" + ], + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "get_folder_quip", + "description": "Get detailed information about a Quip folder\n\nThis tool retrieves detailed information about a specific folder,\nincluding its title, color, parent folder, and child folders.\n\nExample:\n```json\n{\n \"folderId\": \"ABCDEF123456\"\n}\n```", + "input_schema": { + "json": { + "additionalProperties": false, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "folderId" + ], + "properties": { + "folderId": { + "type": "string", + "description": "The ID of the folder to retrieve information about" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "search_symphony", + "description": "Search for Symphony CREATIVE/PLACEMENT/EVENT/TAG with region id and query, this tool allows you to search Symphony objects by many dimensions, including Symphony creative owner, id, displayName etc.", + "input_schema": { + "json": { + "required": [ + "region", + "type", + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "query": { + "description": "Stringified query and sort key from the Elasticsearch DSL.", + "type": "string" + }, + "pageSize": { + "type": "number", + "description": "minimum: 1, maximum: 50" + }, + "type": { + "type": "string", + "description": "Content Symphony CREATIVE/PLACEMENT/EVENT/TAG" + }, + "region": { + "description": "Symphony region that are going to query data, e.g.: NA, EU, FE, Integ", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "imr_costs_search_fleet", + "description": "Search for fleets based on a query term, matching either fleet name or fleet owner.", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "query": { + "type": "string", + "description": "Query term could be a partial fleet name or one of the owners" + }, + "includeDeleted": { + "type": "boolean", + "description": "Include deleted fleets", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "query" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "orca_list_runs", + "description": "List Orca workflow runs for a specific client and workflow with filtering by status and timerange.\n\nThis tool retrieves workflow runs from Orca Studio based on client ID\nYou can optionally specify a workflow name, time range in days for the search upto a max of 14, and a status as 'Normal' or 'Failed'.\ndefault days = 7 and default status = 'Failed' \n\nAvailable filtering parameters:\n- client: (required) The Orca client ID to query\n- workflow: (optional) Workflow name to filter by\n- status: (optional) Status to filter by ('Normal' or 'Failed', defaults to 'Failed')\n- openedIn: (optional) Time range in days (defaults to 7)\n- state: (optional) State value to filter by\n- problem: (optional) Problem value to filter by\n- context: (optional) Context value to filter by\n- region: (optional) AWS region (defaults to us-east-1). Common regions include us-east-1, us-west-2, eu-west-1, etc.\n\nExample\n```json\n{ \"client\": \"MyOrcaClient\"}\n```\n\nExample with workflow:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\" }\n```\n\nExample with custom time range:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"openedIn\": \"14\" }\n```\nExample with status:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"status\": \"Normal\" }\n```\nExample with status and custom time range:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"status\": \"Normal\", \"openedIn\": \"14\" }\n```\nExample with state filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"state\": \"StateName::Error::Problem\" }\n```\nExample with problem filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"problem\": \"UnknownProblem\" }\n```\nExample with context filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"context\": \"live\" }\n```\n\nExample with custom region:\n```json\n{ \"client\": \"MyOrcaClient\", \"region\": \"us-west-2\" }\n```", + "input_schema": { + "json": { + "properties": { + "region": { + "description": "AWS region (defaults to us-east-1). Common regions include us-west-2, eu-west-1, etc.", + "type": "string" + }, + "state": { + "type": "string", + "description": "Optional state value to filter by. Representing the current state of the work item. Often follows pattern '[StateName]::[Status]::[Additional Context]'" + }, + "context": { + "type": "string", + "description": "Optional context value to filter by. Representing the environment context the work item was opened in (e.g., 'live', 'beta') or other information (e.g., 'largeorder')" + }, + "problem": { + "type": "string", + "description": "Optional problem value to filter by. Representing classification result for errored work items (e.g., 'UnknownProblem')" + }, + "status": { + "description": "Optional status to filter runs by (defaults to Failed)", + "type": "string", + "enum": [ + "Normal", + "Failed" + ] + }, + "openedIn": { + "type": "string", + "description": "Optional time range in days (defaults to 7)" + }, + "client": { + "description": "The Orca client ID to query", + "type": "string" + }, + "workflow": { + "type": "string", + "description": "Optional workflow name to query (defaults to '')" + } + }, + "type": "object", + "additionalProperties": false, + "required": [ + "client" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "eureka_web_search", + "description": "Web Search using Amazon's internal web-scale search engine - Eureka\n\nGiven a query, this tool will search across the web and return relevant search results.\nThe tool returns top documents with content, url, title, and document_published_at_timestamp.\n\nExample:\n { \"query\": \"recent supreme court ruling\" }", + "input_schema": { + "json": { + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "additionalProperties": false, + "required": [ + "query" + ], + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_account_lookup", + "description": "This tool is for looking up accounts on the AWS Salesforce AKA AWSentral", + "input_schema": { + "json": { + "properties": { + "account_id": { + "description": "the id of the account", + "type": "string" + }, + "account_name": { + "description": "the name of the account", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "oncall_compass_get_report", + "description": "Get the content of the report along with additional metadata.", + "input_schema": { + "json": { + "type": "object", + "required": [ + "reportId" + ], + "additionalProperties": false, + "properties": { + "reportId": { + "description": "ID of the report to retrieve", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "overleaf_clone_project", + "description": "Clone an Overleaf project to the local workspace.\n\nThis tool clones the specified Overleaf project to the local workspace directory.\nThe project will be stored in ./overleaf/{project_id}.\nIf the project is already cloned locally, this operation is idempotent and will skip cloning.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\"\n}\n```", + "input_schema": { + "json": { + "type": "object", + "properties": { + "project_id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Project ID to clone" + } + }, + "additionalProperties": false, + "required": [ + "project_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "jira_get_attachment", + "description": "Download an attachment from a JIRA issue", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "outputPath": { + "description": "Path where to save the downloaded attachment", + "type": "string" + }, + "attachmentUrl": { + "minLength": 1, + "description": "The URL of the attachment to download", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "attachmentUrl" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "sfdc_list_tasks_activity", + "description": "This tool is for listing SA Activities and tasks in AWS Salesforce (AFA AWSentral)", + "input_schema": { + "json": { + "properties": { + "account_id": { + "type": "string", + "description": "The Salesforce Account ID to filter by - this will return all activities/tasks on an account and it's opportunities" + }, + "opportunity_id": { + "type": "string", + "description": "The Salesforce Opportunity ID to filter by - this will return all activities/tasks on a opportunity" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "pippin_update_project", + "description": "Updates an existing Pippin design project's details", + "input_schema": { + "json": { + "type": "object", + "required": [ + "projectId" + ], + "properties": { + "description": { + "type": "string", + "description": "Updated project description" + }, + "requirements": { + "type": "string", + "description": "Updated project requirements" + }, + "projectId": { + "description": "Project ID", + "type": "string" + }, + "status": { + "type": "string", + "description": "Updated project status" + }, + "name": { + "description": "Updated project name", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "add_tag_work_contribution", + "description": "Add a tag to a work contribution in AtoZ.\n\nThis tool allows you to add a tag (such as a leadership principle tag) to an existing work contribution.\nTo get a list of available leadership principles, use the list_leadership_principles tool.\n\nLimitations:\nonly up to three leadership principles can be tagged\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- tagKey: The key of the tag to add (e.g., 'uri_1', 'uri_2')\n- tagType: The type of tag (e.g., 'LEADERSHIP_PRINCIPLE')\n- ownerLogin or ownerPersonId: The owner of the work contribution", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "workContributionId", + "tagKey", + "tagType" + ], + "properties": { + "workContributionId": { + "description": "ID of the work contribution", + "type": "string" + }, + "tagType": { + "type": "string", + "enum": [ + "LEADERSHIP_PRINCIPLE", + "ROLE_GUIDELINE" + ], + "description": "Type of tag to add" + }, + "ownerLogin": { + "description": "Login/alias of the work contribution owner", + "type": "string" + }, + "ownerPersonId": { + "description": "Person ID of the work contribution owner", + "type": "string" + }, + "tagKey": { + "description": "Uri Key of the tag to add (e.g., 'uri_1', 'uri_2')", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "acs_change_cp_records", + "description": "Modify records (also called config values) for a given contextual parameter (also called config key, or CP) in Amazon Config Store.\nAllows adding, deprecating, or modifying records with proper change tracking.\nDeprecating a contextual parameter value will avoid any new usage of this value. However, existing feature records using this value will remain unaffected.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", + "input_schema": { + "json": { + "type": "object", + "properties": { + "ticketLink": { + "type": "string", + "description": "Optional link to a ticket related to this change" + }, + "name": { + "description": "Contextual parameter name to modify records for", + "type": "string" + }, + "recordChanges": { + "minItems": 1, + "description": "Record changes to apply", + "type": "array", + "items": { + "properties": { + "operationType": { + "description": "Operation type for the record change. Either Upsert or Deprecate.", + "enum": [ + "Upsert", + "Deprecate" + ], + "type": "string" + }, + "value": { + "minLength": 1, + "description": "Value for the record to be added or deprecated", + "type": "string" + }, + "parentKeyValueMap": { + "additionalProperties": { + "minLength": 1, + "type": "string" + }, + "type": "object", + "description": "Map from parent contextual parameter keys to their values. Required for composite contextual parameters.", + "propertyNames": { + "minLength": 1 + } + }, + "description": { + "type": "string", + "description": "Description of the changes being made" + }, + "parentValue": { + "description": "Parent value of the contextual parameter value", + "type": "string" + } + }, + "type": "object", + "required": [ + "operationType", + "value" + ], + "additionalProperties": false + } + }, + "crId": { + "type": "string", + "description": "Optional CR id to raise a new revision rather than making a new CR" + }, + "stage": { + "description": "Stage to query", + "type": "string", + "enum": [ + "PROD", + "DEVO", + "SANDBOX" + ] + }, + "changeSummary": { + "type": "string", + "description": "Summary of the changes being made" + } + }, + "required": [ + "name", + "recordChanges", + "changeSummary", + "stage" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + } + } + } + }, + { + "ToolSpecification": { + "name": "genai_poweruser_search_knowledge", + "description": "Perform advanced text-based searches across your knowledge repository to find documents matching specific queries. This tool searches document content and returns contextual matches with relevance scores, supporting search result limiting and folder-specific scoping. Ideal for discovering relevant information across large knowledge bases.", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "type": "object", + "required": [ + "query" + ], + "properties": { + "folder": { + "description": "Limit search to a specific folder", + "type": "string" + }, + "query": { + "description": "The search query", + "type": "string" + }, + "limit": { + "type": "number", + "description": "Maximum number of results to return" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "policy_engine_get_user_dashboard", + "description": "Access Amazon Policy Engine dashboard information for a specific user alias. This tool allows you to view all risks and violations for a user in Policy Engine.", + "input_schema": { + "json": { + "properties": { + "username": { + "description": "Username to view dashboard for (e.g., 'jingzhoh')", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "username" + ], + "type": "object" + } + } + } + }, + { + "ToolSpecification": { + "name": "marshal_get_insight", + "description": "Retrieve Marshal Insights.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "insightId": { + "type": "string", + "pattern": "^\\d+$", + "description": "The ID of the Marshal insight (numeric ID only, not the full URL)" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "insightId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "read_kingpin_goal", + "description": "Read a Kingpin goal by ID, retrieving comprehensive details including metadata, description, status comments, and path to green information. Now supports goal history tracking with the includeHistory parameter, showing how status comments and path to green have changed over time. Path to Green represents specific actions needed to get at-risk goals back on track. Use maxVersions parameter to control the amount of history data returned. Kingpin is Amazon's internal source of truth for planning and commitments.", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "includeHistory": { + "default": false, + "type": "boolean", + "description": "Whether to include the goal's history in the response, showing changes to statusComments and pathToGreen fields over time (default: false)" + }, + "goalId": { + "description": "The ID of the Kingpin goal to read (numeric ID only, not the full URL)", + "type": "string" + }, + "maxVersions": { + "type": "number", + "default": 10, + "description": "Maximum number of versions to include in the history, used to limit returned information size for goals with extensive history (default: 10)" + } + }, + "required": [ + "goalId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "imr_costs_get_fleet_summary", + "description": "Presents the internal costs (IMR) for a fleet or AWS account. Retrieves the information from the tool Cerberus and monthly statements api.", + "input_schema": { + "json": { + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "resourceId": { + "type": "string", + "description": "Resource identifier, fleetId or aws account" + }, + "rateCard": { + "description": "Rate card identifier (e.g. 2025)", + "default": "yearly", + "type": "string" + }, + "scenario": { + "default": "Default CPT++", + "type": "string", + "description": "Scenario name" + }, + "fleetType": { + "description": "Container type, either CONTAINER or AWS_ACCOUNT", + "enum": [ + "CONTAINER", + "AWS_ACCOUNT" + ], + "default": "CONTAINER", + "type": "string" + }, + "month": { + "description": "Month in YYYY-MM-01 format", + "type": "string", + "default": "2025-09-01" + }, + "period": { + "enum": [ + "MONTH", + "YEAR_TO_DATE", + "FULL_YEAR" + ], + "description": "Time period for the summary", + "type": "string", + "default": "YEAR_TO_DATE" + } + }, + "required": [ + "resourceId" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "search_MCMs", + "description": "Search and filter Change Management (CM) records by various criteria:\n • Personnel: requesters, technicians, approvers, resolver groups\n • Status: CM status and closure codes\n • Time-based: creation, updates, scheduling, and execution dates\n • Results: configurable result limits", + "input_schema": { + "json": { + "properties": { + "scheduledStart": { + "properties": { + "lessThan": { + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have scheduled end time less than the given value" + }, + "lessThanOrEqualTo": { + "additionalProperties": false, + "description": "provide this predicate to find Cms that have scheduled end time less than or equal to the given value", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number" + } + } + }, + "greaterThan": { + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "description": "provide this predicate to find Cms that have scheduled end time greater than the given value", + "required": [ + "value" + ], + "additionalProperties": false + }, + "between": { + "additionalProperties": false, + "description": "provide this predicate to find Cms that have scheduled end time between the two values", + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "start", + "end" + ] + }, + "greaterThanOrEqualTo": { + "additionalProperties": false, + "properties": { + "value": { + "type": "number" + } + }, + "type": "object", + "description": "provide this predicate to find Cms that have scheduled end time greater than or equal to the given value", + "required": [ + "value" + ] + } + }, + "additionalProperties": false, + "type": "object", + "description": "the scheduled start of the cm" + }, + "actualStart": { + "type": "object", + "description": "the actual start of the cm", + "properties": { + "lessThan": { + "description": "provide this predicate to find Cms that have actual start time less than the given value", + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "greaterThan": { + "additionalProperties": false, + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "type": "object", + "description": "provide this predicate to find Cms that have actual start time greater than the given value" + }, + "between": { + "description": "provide this predicate to find Cms that have actual start time between the two values", + "additionalProperties": false, + "required": [ + "start", + "end" + ], + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + } + }, + "lessThanOrEqualTo": { + "additionalProperties": false, + "required": [ + "value" + ], + "description": "provide this predicate to find Cms that have actual start time less than or equal to the given value", + "properties": { + "value": { + "type": "number" + } + }, + "type": "object" + }, + "greaterThanOrEqualTo": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "type": "number" + } + }, + "description": "provide this predicate to find Cms that have actual start time greater than or equal to the given value", + "required": [ + "value" + ] + } + }, + "additionalProperties": false + }, + "cmStatus": { + "description": "the status of the Cm", + "type": "array", + "items": { + "enum": [ + "Draft", + "PendingApproval", + "Scheduled", + "Modified", + "Rejected", + "Cancelled", + "Completed", + "Paused", + "Aborted", + "Discarded", + "Rework Required", + "Scheduled with Comments", + "In Progress", + "Pending Reapproval", + "Modified after Execution", + "Pending Reapproval after Execution", + "Preflight" + ], + "type": "string" + } + }, + "updatedAt": { + "properties": { + "between": { + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have updated at time between the two values", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "start", + "end" + ] + }, + "greaterThanOrEqualTo": { + "description": "provide this predicate to find Cms that have updated at time greater than or equal to the given value", + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "lessThan": { + "description": "provide this predicate to find Cms that have updated at time less than the given value", + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "type": "object", + "additionalProperties": false + }, + "greaterThan": { + "additionalProperties": false, + "description": "provide this predicate to find Cms that have updated at time greater than the given value", + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "type": "number" + } + } + }, + "lessThanOrEqualTo": { + "type": "object", + "description": "provide this predicate to find Cms that have updated at time less than or equal to the given value", + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + } + }, + "type": "object", + "additionalProperties": false, + "description": "the time the Cm was updated" + }, + "actualEnd": { + "type": "object", + "properties": { + "greaterThan": { + "properties": { + "value": { + "type": "number" + } + }, + "description": "provide this predicate to find Cms that have actual end time greater than the given value", + "required": [ + "value" + ], + "additionalProperties": false, + "type": "object" + }, + "greaterThanOrEqualTo": { + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ], + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have actual end time greater than or equal to the given value" + }, + "between": { + "required": [ + "start", + "end" + ], + "additionalProperties": false, + "description": "provide this predicate to find Cms that have actual end time between the two values", + "type": "object", + "properties": { + "end": { + "type": "number" + }, + "start": { + "type": "number" + } + } + }, + "lessThan": { + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "value" + ], + "description": "provide this predicate to find Cms that have actual end time less than the given value" + }, + "lessThanOrEqualTo": { + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "value" + ], + "description": "provide this predicate to find Cms that have actual end time less than or equal to the given value" + } + }, + "description": "the actual end of the cm", + "additionalProperties": false + }, + "createdAt": { + "description": "the time the Cm was created", + "type": "object", + "additionalProperties": false, + "properties": { + "lessThan": { + "required": [ + "value" + ], + "additionalProperties": false, + "description": "provide this predicate to find Cms that have created at time less than the given value", + "type": "object", + "properties": { + "value": { + "type": "number" + } + } + }, + "lessThanOrEqualTo": { + "type": "object", + "required": [ + "value" + ], + "additionalProperties": false, + "properties": { + "value": { + "type": "number" + } + }, + "description": "provide this predicate to find Cms that have created at time less than or equal to the given value" + }, + "between": { + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have created at time between the two values", + "required": [ + "start", + "end" + ] + }, + "greaterThanOrEqualTo": { + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have created at time greater than or equal to the given value", + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ] + }, + "greaterThan": { + "required": [ + "value" + ], + "description": "provide this predicate to find Cms that have created at time greater than the given value", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "type": "number" + } + } + } + } + }, + "approvers": { + "additionalProperties": false, + "type": "object", + "description": "Filter CMs by approver criteria - use matchAny to find CMs with any of the specified approvers, or matchAll to find CMs with all specified approvers", + "properties": { + "matchAny": { + "required": [ + "values" + ], + "additionalProperties": false, + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "additionalProperties": false, + "properties": { + "level": { + "type": "string" + }, + "status": { + "type": "string" + }, + "assignedApproverLogin": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assignedApproverLogin" + ] + } + } + } + }, + "matchAll": { + "required": [ + "values" + ], + "additionalProperties": false, + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "required": [ + "assignedApproverLogin" + ], + "properties": { + "status": { + "type": "string" + }, + "assignedApproverLogin": { + "type": "string" + }, + "level": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + } + } + } + } + } + }, + "closureCode": { + "type": "array", + "items": { + "enum": [ + "Successful", + "Successful - Off Script", + "Unsuccessful" + ], + "type": "string" + }, + "description": "the closure code of the CMs" + }, + "numResults": { + "default": 100, + "type": "number", + "description": "Number of results to return" + }, + "requesters": { + "type": "array", + "items": { + "type": "string", + "description": "List of requesters of the CMs" + } + }, + "scheduledEnd": { + "additionalProperties": false, + "type": "object", + "properties": { + "greaterThanOrEqualTo": { + "properties": { + "value": { + "type": "number" + } + }, + "type": "object", + "additionalProperties": false, + "description": "provide this predicate to find Cms that have scheduled end time greater than or equal to the given value", + "required": [ + "value" + ] + }, + "between": { + "additionalProperties": false, + "description": "provide this predicate to find Cms that have scheduled end time between the two values", + "properties": { + "end": { + "type": "number" + }, + "start": { + "type": "number" + } + }, + "type": "object", + "required": [ + "start", + "end" + ] + }, + "greaterThan": { + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have scheduled end time greater than the given value", + "properties": { + "value": { + "type": "number" + } + }, + "required": [ + "value" + ] + }, + "lessThan": { + "type": "object", + "properties": { + "value": { + "type": "number" + } + }, + "description": "provide this predicate to find Cms that have scheduled end time less than the given value", + "additionalProperties": false, + "required": [ + "value" + ] + }, + "lessThanOrEqualTo": { + "required": [ + "value" + ], + "additionalProperties": false, + "type": "object", + "description": "provide this predicate to find Cms that have scheduled end time less than or equal to the given value", + "properties": { + "value": { + "type": "number" + } + } + } + }, + "description": "the scheduled end of the cm" + }, + "technician": { + "type": "array", + "items": { + "type": "string", + "description": "List of technicians of the CMs" + } + }, + "cmOwnerCtiResolverGroup": { + "items": { + "type": "string", + "description": "List of Resolver groups for the CMs" + }, + "type": "array" + } + }, + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + } + }, + { + "ToolSpecification": { + "name": "sage_post_answer", + "description": "Post an answer to an existing question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to contribute answers to questions on Sage through the MCP interface.\nThe answer content supports Markdown formatting for rich text, code blocks, and links.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Answering technical questions about Amazon internal tools and services\n- Providing code examples or troubleshooting steps\n- Sharing knowledge about internal processes\n\nExample usage:\n{ \"questionId\": 1234567, \"contents\": \"To solve this issue, you need to run:\\n\\n```bash\\nbrazil workspace merge\\n```\\n\\nThis will resolve the dependency conflicts.\" }", + "input_schema": { + "json": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "required": [ + "questionId", + "contents" + ], + "properties": { + "questionId": { + "type": "number", + "description": "ID of the question to answer" + }, + "contents": { + "description": "Content of the answer in Markdown format", + "type": "string" + } + } + } + } + } + }, + { + "ToolSpecification": { + "name": "search_sable", + "description": "Search for Sable scope recode with region id, scope, key or key prefix. This tool allows you to search Sable record by key or key prefix.", + "input_schema": { + "json": { + "properties": { + "scope": { + "type": "string", + "description": "Sable scope name" + }, + "keyPrefix": { + "description": "Sable record key or key prefix", + "type": "string" + }, + "region": { + "type": "string", + "description": "Sable region that are going to query data, e.g.: NA, EU, FE, Integ" + } + }, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "required": [ + "region", + "scope", + "keyPrefix" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "search_katal_components", + "description": "Search for Katal components\n\nThis tool allows you to search for Katal components using keywords.\nThe search looks through component names and tag names.\n\nExamples:\n1. Search for button components:\n```json\n{\n \"query\": \"button\"\n}\n```", + "input_schema": { + "json": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "description": "Search query to find matching Katal components", + "type": "string" + } + }, + "required": [ + "query" + ] + } + } + } + }, + { + "ToolSpecification": { + "name": "overleaf_read_file", + "description": "Read a file from an Overleaf project with automatic synchronization.\n\nThis tool reads the specified file from an Overleaf project. Before reading,\nit ensures the project is cloned locally and synchronized with the remote repository.\nSupports both text and binary files with proper encoding detection.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"main.tex\"\n}\n```", + "input_schema": { + "json": { + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "project_id": { + "type": "string", + "description": "Project ID containing the file" + }, + "file_path": { + "description": "Path to the file within the project", + "type": "string" + } + }, + "type": "object", + "required": [ + "project_id", + "file_path" + ] + } + } + } + } + ] + }, + "context_manager": { + "max_context_files_size": 150000, + "current_profile": "q_cli_default", + "paths": [ + "AmazonQ.md", + "README.md", + ".amazonq/rules/**/*.md" + ], + "hooks": {} + }, + "context_message_length": 1753, + "latest_summary": null, + "model_info": { + "model_name": "claude-sonnet-4", + "model_id": "claude-sonnet-4", + "context_window_tokens": 200000 + }, + "file_line_tracker": { + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs": { + "prev_fswrite_lines": 19, + "before_fswrite_lines": 0, + "after_fswrite_lines": 19, + "lines_added_by_agent": 19, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json": { + "prev_fswrite_lines": 10, + "before_fswrite_lines": 9, + "after_fswrite_lines": 10, + "lines_added_by_agent": 10, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs": { + "prev_fswrite_lines": 6, + "before_fswrite_lines": 0, + "after_fswrite_lines": 6, + "lines_added_by_agent": 6, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs": { + "prev_fswrite_lines": 13, + "before_fswrite_lines": 0, + "after_fswrite_lines": 13, + "lines_added_by_agent": 13, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore": { + "prev_fswrite_lines": 44, + "before_fswrite_lines": 0, + "after_fswrite_lines": 44, + "lines_added_by_agent": 44, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs": { + "prev_fswrite_lines": 29, + "before_fswrite_lines": 0, + "after_fswrite_lines": 29, + "lines_added_by_agent": 29, + "lines_removed_by_agent": 0, + "is_first_write": false + }, + "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs": { + "prev_fswrite_lines": 14, + "before_fswrite_lines": 42, + "after_fswrite_lines": 14, + "lines_added_by_agent": 14, + "lines_removed_by_agent": 0, + "is_first_write": false + } + }, + "mcp_enabled": true +} \ No newline at end of file diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs new file mode 100644 index 00000000..1b4c6488 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs @@ -0,0 +1,51 @@ +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Microsoft.AspNetCore.Mvc; +using NetV3Server.Models; +using NetV3Server.Services; + +namespace NetV3Server.Controllers; + +[ApiController] +[Route("[controller]")] +public class ClientController : ControllerBase +{ + private readonly IClientCacheService _clientCacheService; + + public ClientController(IClientCacheService clientCacheService) + { + _clientCacheService = clientCacheService; + } + + [HttpPost] + public async Task CreateClient([FromBody] ClientRequest request) + { + try + { + var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyUnauthenticatedModes; + + + var encryptionContext = new Dictionary(); + + // Create encryption materials + var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); + + // Create S3 encryption client + var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2); + var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); + + // Add to cache and return client ID + var clientId = _clientCacheService.AddClient(encryptionClient); + + return Ok(new ClientResponse { ClientId = clientId }); + } + catch (Exception ex) + { + return StatusCode(500, new S3EncryptionClientError + { + Message = $"Failed to create client: {ex.Message}" + }); + } + } +} \ No newline at end of file From 6622859850de7ff4ca73cd4b3a427862cc2d9611 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 11:04:12 -0700 Subject: [PATCH 007/201] m --- test-server/go-v3-server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/go-v3-server/README.md b/test-server/go-v3-server/README.md index 2452dc93..6b153bfc 100644 --- a/test-server/go-v3-server/README.md +++ b/test-server/go-v3-server/README.md @@ -24,7 +24,7 @@ The server is built using: To run the server: ```console -gradle run +go run . ``` This will start the server running on port `8082`. From bcae3793c616aec5849f44a24b7d12552264a4f3 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 11:17:48 -0700 Subject: [PATCH 008/201] Update test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java Co-authored-by: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> --- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3207af35..9225b257 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 @@ -64,7 +64,7 @@ public class RoundTripTests { System.getenv("TEST_SERVER_S3_BUCKET") : "s3ec-test-server-github-bucket"; static { - serverList = new ArrayList<>(2); + serverList = new ArrayList<>(14); serverList.add(new LanguageServerTarget("Java-V3", "8080")); serverList.add(new LanguageServerTarget("Python-V3", "8081")); serverList.add(new LanguageServerTarget("Go-V3", "8082")); From bf0683c45238f3e85a97058211171f25532b0cbf Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 11:17:57 -0700 Subject: [PATCH 009/201] Update test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java Co-authored-by: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> --- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9225b257..535e4d8d 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 @@ -69,7 +69,7 @@ public class RoundTripTests { serverList.add(new LanguageServerTarget("Python-V3", "8081")); serverList.add(new LanguageServerTarget("Go-V3", "8082")); - serverMap = new HashMap<>(2); + serverMap = new HashMap<>(14); serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080")); serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081")); serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); From afecd7578fec65744ac1d2df0268371547759e4d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 16 Sep 2025 11:20:44 -0700 Subject: [PATCH 010/201] m --- test-server/go-v3-server/main.go | 59 ++++---------------------------- 1 file changed, 6 insertions(+), 53 deletions(-) diff --git a/test-server/go-v3-server/main.go b/test-server/go-v3-server/main.go index e79f0415..d201ffe2 100644 --- a/test-server/go-v3-server/main.go +++ b/test-server/go-v3-server/main.go @@ -80,7 +80,7 @@ func NewServer() (*Server, error) { // createGenericServerError creates a generic server error response func (s *Server) createGenericServerError(w http.ResponseWriter, message string, statusCode int) { // Echo error to console - log.Printf("GenericServerError: %s (Status: %d)", message, statusCode) + log.Printf("[Go V3] GenericServerError: %s (Status: %d)", message, statusCode) w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) @@ -93,7 +93,7 @@ func (s *Server) createGenericServerError(w http.ResponseWriter, message string, // createS3EncryptionClientError creates an S3 encryption client error response func (s *Server) createS3EncryptionClientError(w http.ResponseWriter, message string, statusCode int) { // Echo error to console - log.Printf("S3EncryptionClientError: %s (Status: %d)", message, statusCode) + log.Printf("[Go V3] S3EncryptionClientError: %s (Status: %d)", message, statusCode) w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) @@ -128,8 +128,6 @@ func metadataStringToMap(mdString string) (map[string]string, error) { // createClient handles POST /client func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { - log.Printf("CreateClient: Received POST /client request") - // Read body body, err := io.ReadAll(r.Body) if err != nil { @@ -143,9 +141,6 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { return } - log.Printf("CreateClient: Parsed config - KMSKeyID: %s, EnableLegacyWrappingAlgorithms: %t", - input.Config.KeyMaterial.KMSKeyID, input.Config.EnableLegacyWrappingAlgorithms) - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) @@ -153,7 +148,6 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { } // Create KMS keyring - log.Printf("CreateClient: Creating KMS keyring with key ID: %s", input.Config.KeyMaterial.KMSKeyID) kmsClient := kms.NewFromConfig(cfg) keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { options.EnableLegacyWrappingAlgorithms = input.Config.EnableLegacyWrappingAlgorithms @@ -166,7 +160,6 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { } // Create S3 encryption client - log.Printf("CreateClient: Creating S3 encryption client") var s3EncryptionClient *client.S3EncryptionClientV3 s3PlaintextClient := s3.NewFromConfig(cfg) s3EncryptionClient, err = client.New(s3PlaintextClient, cmm) @@ -178,18 +171,15 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { // Generate client ID clientID := uuid.New().String() - log.Printf("CreateClient: Generated client ID: %s", clientID) // Store client in cache s.clientCache[clientID] = s3EncryptionClient - log.Printf("CreateClient: Stored client in cache. Total clients: %d", len(s.clientCache)) // Return response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CreateClientOutput{ ClientID: clientID, }) - log.Printf("CreateClient: Successfully created client %s", clientID) } // putObject handles PUT /object/{bucket}/{key} @@ -198,16 +188,12 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { bucket := vars["bucket"] key := vars["key"] - log.Printf("PutObject: Received PUT /object/%s/%s request", bucket, key) - clientID := r.Header.Get("ClientID") if clientID == "" { s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) return } - log.Printf("PutObject: Using client ID: %s", clientID) - // Get client from cache client, exists := s.clientCache[clientID] @@ -223,8 +209,6 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - log.Printf("PutObject: Read body of size: %d bytes", len(body)) - // Get metadata from header metadataHeader := r.Header.Get("Content-Metadata") encCtx, err := metadataStringToMap(metadataHeader) @@ -237,13 +221,6 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - if len(encCtx) > 0 { - metadataJSON, _ := json.Marshal(encCtx) - log.Printf("PutObject: Using encryption context: %s", string(metadataJSON)) - } else { - log.Printf("PutObject: No encryption context provided") - } - // Create put object input putInput := &s3.PutObjectInput{ Bucket: aws.String(bucket), @@ -256,7 +233,6 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { putInput.Metadata = encCtx } - log.Printf("PutObject: Making S3 PutObject request") // Make the put object request using the encryption client _, err = client.PutObject(encryptionContext, putInput) if err != nil { @@ -264,7 +240,7 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { return } - log.Printf("PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + log.Printf("[Go V3] PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) // Return response w.Header().Set("Content-Type", "application/json") @@ -274,7 +250,6 @@ func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { Metadata: []string{}, // Return empty metadata list as per the model } json.NewEncoder(w).Encode(response) - log.Printf("PutObject: Response sent successfully") } // getObject handles GET /object/{bucket}/{key} @@ -283,16 +258,12 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { bucket := vars["bucket"] key := vars["key"] - log.Printf("GetObject: Received GET /object/%s/%s request", bucket, key) - clientID := r.Header.Get("ClientID") if clientID == "" { s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) return } - log.Printf("GetObject: Using client ID: %s", clientID) - // Get client from cache client, exists := s.clientCache[clientID] @@ -315,20 +286,12 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { return } - if len(encCtx) > 0 { - metadataJSON, _ := json.Marshal(encCtx) - log.Printf("GetObject: Using encryption context: %s", string(metadataJSON)) - } else { - log.Printf("GetObject: No encryption context provided") - } - // Create get object input getInput := &s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), } - log.Printf("GetObject: Making S3 GetObject request") // Make the get object request using the encryption client result, err := client.GetObject(encryptionContext, getInput) if err != nil { @@ -352,8 +315,6 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { return } - log.Printf("GetObject: Read body of size: %d bytes", len(body)) - // Convert metadata to string format var metadataList []string if result.Metadata != nil { @@ -364,27 +325,19 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { metadataStr := strings.Join(metadataList, ",") - if len(metadataList) > 0 { - log.Printf("GetObject: Retrieved metadata: %s", metadataStr) - } else { - log.Printf("GetObject: No metadata found in object") - } - - log.Printf("GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) - log.Printf("GetObject: Body content: %s", string(body)) + log.Printf("[Go V3] GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) // Set response headers w.Header().Set("Content-Metadata", metadataStr) // Return the body as response w.Write(body) - log.Printf("GetObject: Response sent successfully") } func main() { server, err := NewServer() if err != nil { - log.Fatalf("Failed to create server: %v", err) + log.Fatalf("[Go V3] Failed to create Go V3 server: %v", err) } r := mux.NewRouter() @@ -394,6 +347,6 @@ func main() { r.HandleFunc("/object/{bucket}/{key}", server.putObject).Methods("PUT") r.HandleFunc("/object/{bucket}/{key}", server.getObject).Methods("GET") - fmt.Println("Starting Go server on :8082...") + fmt.Println("[Go V3] Starting Go V3 server on :8082...") log.Fatal(http.ListenAndServe(":8082", r)) } From 7ed605b4b9dd25ea2687eade649637a7f658858e Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 16 Sep 2025 11:45:58 -0700 Subject: [PATCH 011/201] auto commit --- .../src/NetV3Server/Controllers/ClientController.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs index 1b4c6488..5203185e 100644 --- a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs @@ -23,21 +23,16 @@ public async Task CreateClient([FromBody] ClientRequest request) try { var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; - var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyUnauthenticatedModes; - - + var enableLegacyMode = request.Config.EnableLegacyMode; var encryptionContext = new Dictionary(); - - // Create encryption materials var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); - + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC version while V2 cannot + var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; + var configuration = new AmazonS3CryptoConfigurationV2(securityProfile); // Create S3 encryption client - var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2); var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); - // Add to cache and return client ID var clientId = _clientCacheService.AddClient(encryptionClient); - return Ok(new ClientResponse { ClientId = clientId }); } catch (Exception ex) From c2826e0f4be74f4b8c876a7f2051e0459acc9d16 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 16 Sep 2025 13:18:42 -0700 Subject: [PATCH 012/201] auto commit --- .../Controllers/ClientController.cs | 2 +- .../Controllers/ObjectController.cs | 125 ++++++++++++++++++ .../src/NetV3Server/Models/ClientRequest.cs | 4 +- .../net-v3-server/src/NetV3Server/Program.cs | 56 ++------ 4 files changed, 142 insertions(+), 45 deletions(-) create mode 100644 test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs index 5203185e..8c7138a2 100644 --- a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs @@ -26,7 +26,7 @@ public async Task CreateClient([FromBody] ClientRequest request) var enableLegacyMode = request.Config.EnableLegacyMode; var encryptionContext = new Dictionary(); var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); - // SecurityProfile V2AndLegacy can decrypt from legacy S3EC version while V2 cannot + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC while V2 cannot var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; var configuration = new AmazonS3CryptoConfigurationV2(securityProfile); // Create S3 encryption client diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs new file mode 100644 index 00000000..c0a94aa7 --- /dev/null +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs @@ -0,0 +1,125 @@ +using Amazon.S3.Model; +using Microsoft.AspNetCore.Mvc; +using NetV3Server.Models; +using NetV3Server.Services; + +namespace NetV3Server.Controllers; + +[ApiController] +[Route("[controller]")] +public class ObjectController : ControllerBase +{ + private readonly IClientCacheService _clientCacheService; + + public ObjectController(IClientCacheService clientCacheService) + { + _clientCacheService = clientCacheService; + } + + [HttpPut("{bucket}/{key}")] + public async Task PutObject(string bucket, string key) + { + var clientId = Request.Headers["ClientID"].FirstOrDefault(); + if (string.IsNullOrEmpty(clientId)) + return BadRequest(new GenericServerError { Message = "ClientID header is required" }); + + var client = _clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"No client found for ClientID: {clientId}" }); + + try + { + // Read raw body data + using var memoryStream = new MemoryStream(); + await Request.Body.CopyToAsync(memoryStream); + var bodyBytes = memoryStream.ToArray(); + + // Parse encryption context from content-metadata header + var contentMetadata = Request.Headers["Content-Metadata"].FirstOrDefault() ?? ""; + var encryptionContext = ParseMetadataString(contentMetadata); + + // Create put request + var putRequest = new PutObjectRequest + { + BucketName = bucket, + Key = key, + InputStream = new MemoryStream(bodyBytes) + }; + + // Add encryption context to metadata + foreach (var kvp in encryptionContext) putRequest.Metadata.Add(kvp.Key, kvp.Value); + + await client.PutObjectAsync(putRequest); + + return Ok(new { bucket, key, metadata = new string[0] }); + } + catch (Exception ex) + { + return StatusCode(500, new S3EncryptionClientError { Message = $"Failed to put object: {ex.Message}" }); + } + } + + [HttpGet("{bucket}/{key}")] + public async Task GetObject(string bucket, string key) + { + var clientId = Request.Headers["ClientID"].FirstOrDefault(); + if (string.IsNullOrEmpty(clientId)) + return BadRequest(new GenericServerError { Message = "ClientID header is required" }); + + var client = _clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"No client found for ClientID: {clientId}" }); + + try + { + var getRequest = new GetObjectRequest + { + BucketName = bucket, + Key = key + }; + + var response = await client.GetObjectAsync(getRequest); + + // 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(key => $"{key}={response.Metadata[key]}") + .ToList(); + var metadataStr = string.Join(",", metadataList); + + // Set response headers + Response.Headers["Content-Metadata"] = metadataStr; + + return File(bodyBytes, "application/octet-stream"); + } + catch (Exception ex) + { + return StatusCode(500, new S3EncryptionClientError { Message = ex.Message }); + } + } + + private Dictionary ParseMetadataString(string metadataString) + { + if (string.IsNullOrEmpty(metadataString)) + return new Dictionary(); + + return metadataString + .Split(',') // split into each key-value pair + .Select(entry => + { + // transforms each string entry into a string array by splitting on the delimiter "]:[" + var parts = entry.Split("]:["); + if (parts.Length != 2) + throw new ArgumentException($"Invalid metadata entry: {entry}"); + return parts; + }) + .ToDictionary( + parts => parts[0].TrimStart('['), + parts => parts[1].TrimEnd(']') + ); + } +} \ No newline at end of file diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs index 3d9729d6..d620a46f 100644 --- a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs @@ -7,11 +7,11 @@ public class ClientRequest public class ClientConfig { - public bool EnableLegacyUnauthenticatedModes { get; set; } + public bool EnableLegacyMode { get; set; } public KeyMaterial KeyMaterial { get; set; } = new(); } public class KeyMaterial { public string KmsKeyId { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/test-server/net-v3-server/src/NetV3Server/Program.cs b/test-server/net-v3-server/src/NetV3Server/Program.cs index 9f135aaf..a515f3e0 100644 --- a/test-server/net-v3-server/src/NetV3Server/Program.cs +++ b/test-server/net-v3-server/src/NetV3Server/Program.cs @@ -1,42 +1,14 @@ -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi(); - -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} +using NetV3Server.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container +builder.Services.AddControllers(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +app.MapControllers(); + +app.Run(); From 2e594b615ee40028d2c13b9a5ace67bb410f83d4 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Wed, 17 Sep 2025 09:40:04 -0700 Subject: [PATCH 013/201] m --- test-server/go-v3-server/README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test-server/go-v3-server/README.md b/test-server/go-v3-server/README.md index 6b153bfc..cf1692b6 100644 --- a/test-server/go-v3-server/README.md +++ b/test-server/go-v3-server/README.md @@ -10,15 +10,6 @@ The S3EC Go test server implements the S3ECTestServer service defined in the sha - Putting objects with encryption - Getting and decrypting objects -## Architecture - -The server is built using: - -- **HTTP Framework**: Gorilla Mux for routing -- **AWS SDK**: AWS SDK for Go v2 for S3 and KMS operations -- **Concurrency**: Thread-safe client caching with sync.RWMutex -- **Error Handling**: Smithy-compliant error responses - ## Usage To run the server: From cecf9aa8a3d639137a01a90f3ea50f9d4f9f5a88 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 14:07:51 -0700 Subject: [PATCH 014/201] auto commit --- .../Controllers/ClientController.cs | 25 ++++---- .../Controllers/ObjectController.cs | 59 +++++-------------- .../src/NetV3Server/Models/ClientRequest.cs | 1 + .../src/NetV3Server/Models/ClientResponse.cs | 6 +- 4 files changed, 33 insertions(+), 58 deletions(-) diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs index 8c7138a2..d6dd0637 100644 --- a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Amazon.Extensions.S3.Encryption; using Amazon.Extensions.S3.Encryption.Primitives; using Microsoft.AspNetCore.Mvc; @@ -8,23 +9,16 @@ namespace NetV3Server.Controllers; [ApiController] [Route("[controller]")] -public class ClientController : ControllerBase +public class ClientController(IClientCacheService clientCacheService) : ControllerBase { - private readonly IClientCacheService _clientCacheService; - - public ClientController(IClientCacheService clientCacheService) - { - _clientCacheService = clientCacheService; - } - [HttpPost] - public async Task CreateClient([FromBody] ClientRequest request) + public IActionResult CreateClient([FromBody] ClientRequest request) { try { var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; var enableLegacyMode = request.Config.EnableLegacyMode; - var encryptionContext = new Dictionary(); + var encryptionContext = request.Config.EncryptionContext; var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); // SecurityProfile V2AndLegacy can decrypt from legacy S3EC while V2 cannot var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; @@ -32,8 +26,15 @@ public async Task CreateClient([FromBody] ClientRequest request) // Create S3 encryption client var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); // Add to cache and return client ID - var clientId = _clientCacheService.AddClient(encryptionClient); - return Ok(new ClientResponse { ClientId = clientId }); + var clientId = clientCacheService.AddClient(encryptionClient); + var response = new ClientResponse { ClientId = clientId }; + + return new ContentResult + { + Content = JsonSerializer.Serialize(response), + ContentType = "application/json", + StatusCode = 200 + }; } catch (Exception ex) { diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs b/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs index c0a94aa7..a648a2ef 100644 --- a/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs +++ b/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Amazon.S3.Model; using Microsoft.AspNetCore.Mvc; using NetV3Server.Models; @@ -7,23 +8,16 @@ namespace NetV3Server.Controllers; [ApiController] [Route("[controller]")] -public class ObjectController : ControllerBase +public class ObjectController(IClientCacheService clientCacheService) : ControllerBase { - private readonly IClientCacheService _clientCacheService; - - public ObjectController(IClientCacheService clientCacheService) - { - _clientCacheService = clientCacheService; - } - [HttpPut("{bucket}/{key}")] public async Task PutObject(string bucket, string key) { - var clientId = Request.Headers["ClientID"].FirstOrDefault(); + var clientId = Request.Headers["clientId"].FirstOrDefault(); if (string.IsNullOrEmpty(clientId)) return BadRequest(new GenericServerError { Message = "ClientID header is required" }); - var client = _clientCacheService.GetClient(clientId); + var client = clientCacheService.GetClient(clientId); if (client == null) return NotFound(new GenericServerError { Message = $"No client found for ClientID: {clientId}" }); @@ -34,10 +28,6 @@ public async Task PutObject(string bucket, string key) await Request.Body.CopyToAsync(memoryStream); var bodyBytes = memoryStream.ToArray(); - // Parse encryption context from content-metadata header - var contentMetadata = Request.Headers["Content-Metadata"].FirstOrDefault() ?? ""; - var encryptionContext = ParseMetadataString(contentMetadata); - // Create put request var putRequest = new PutObjectRequest { @@ -46,12 +36,16 @@ public async Task PutObject(string bucket, string key) InputStream = new MemoryStream(bodyBytes) }; - // Add encryption context to metadata - foreach (var kvp in encryptionContext) putRequest.Metadata.Add(kvp.Key, kvp.Value); - await client.PutObjectAsync(putRequest); - return Ok(new { bucket, key, metadata = new string[0] }); + var response = new { bucket, key }; + + return new ContentResult + { + Content = JsonSerializer.Serialize(response), + ContentType = "application/json", + StatusCode = 200 + }; } catch (Exception ex) { @@ -62,11 +56,11 @@ public async Task PutObject(string bucket, string key) [HttpGet("{bucket}/{key}")] public async Task GetObject(string bucket, string key) { - var clientId = Request.Headers["ClientID"].FirstOrDefault(); + var clientId = Request.Headers["clientId"].FirstOrDefault(); if (string.IsNullOrEmpty(clientId)) return BadRequest(new GenericServerError { Message = "ClientID header is required" }); - var client = _clientCacheService.GetClient(clientId); + var client = clientCacheService.GetClient(clientId); if (client == null) return NotFound(new GenericServerError { Message = $"No client found for ClientID: {clientId}" }); @@ -77,9 +71,7 @@ public async Task GetObject(string bucket, string key) BucketName = bucket, Key = key }; - var response = await client.GetObjectAsync(getRequest); - // Read response body using var memoryStream = new MemoryStream(); await response.ResponseStream.CopyToAsync(memoryStream); @@ -87,7 +79,7 @@ public async Task GetObject(string bucket, string key) // Convert metadata to content-metadata header format var metadataList = response.Metadata.Keys - .Select(key => $"{key}={response.Metadata[key]}") + .Select(metaDataKey => $"{metaDataKey}={response.Metadata[metaDataKey]}") .ToList(); var metadataStr = string.Join(",", metadataList); @@ -101,25 +93,4 @@ public async Task GetObject(string bucket, string key) return StatusCode(500, new S3EncryptionClientError { Message = ex.Message }); } } - - private Dictionary ParseMetadataString(string metadataString) - { - if (string.IsNullOrEmpty(metadataString)) - return new Dictionary(); - - return metadataString - .Split(',') // split into each key-value pair - .Select(entry => - { - // transforms each string entry into a string array by splitting on the delimiter "]:[" - var parts = entry.Split("]:["); - if (parts.Length != 2) - throw new ArgumentException($"Invalid metadata entry: {entry}"); - return parts; - }) - .ToDictionary( - parts => parts[0].TrimStart('['), - parts => parts[1].TrimEnd(']') - ); - } } \ No newline at end of file diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs index d620a46f..53b63ecc 100644 --- a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs @@ -7,6 +7,7 @@ public class ClientRequest public class ClientConfig { + public Dictionary EncryptionContext { get; set; } = new(); public bool EnableLegacyMode { get; set; } public KeyMaterial KeyMaterial { get; set; } = new(); } diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs b/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs index a56c0d56..ad5e9034 100644 --- a/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs +++ b/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs @@ -1,6 +1,8 @@ +using System.Text.Json.Serialization; + namespace NetV3Server.Models; public class ClientResponse { - public string ClientId { get; set; } = string.Empty; -} + [JsonPropertyName("clientId")] public string ClientId { get; set; } = string.Empty; +} \ No newline at end of file From 2263f16d690057dcb81c9d148a4594399e93ec1f Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 14:32:51 -0700 Subject: [PATCH 015/201] auto commit --- .../src/NetV3Server/NetV3Server.csproj | 33 ++++++++++++------- .../src/NetV3Server/NetV3Server.http | 6 ---- 2 files changed, 22 insertions(+), 17 deletions(-) delete mode 100644 test-server/net-v3-server/src/NetV3Server/NetV3Server.http diff --git a/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj b/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj index 7c674279..17882f6e 100644 --- a/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj +++ b/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj @@ -1,15 +1,26 @@ - - net8.0 - enable - enable - - - - - - - + + net8.0 + enable + enable + + + + false + + + + + + + + + + + + + + diff --git a/test-server/net-v3-server/src/NetV3Server/NetV3Server.http b/test-server/net-v3-server/src/NetV3Server/NetV3Server.http deleted file mode 100644 index 987266e6..00000000 --- a/test-server/net-v3-server/src/NetV3Server/NetV3Server.http +++ /dev/null @@ -1,6 +0,0 @@ -@NetV3Server_HostAddress = http://localhost:5251 - -GET {{NetV3Server_HostAddress}}/weatherforecast/ -Accept: application/json - -### From e7806a472bccc1c6effec19180a64a1e2b1293c3 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 15:15:29 -0700 Subject: [PATCH 016/201] clean up --- .../Controllers/ClientController.cs | 0 .../Controllers/ObjectController.cs | 0 .../NetV3Server => }/Models/ClientRequest.cs | 0 .../NetV3Server => }/Models/ClientResponse.cs | 0 .../NetV3Server => }/Models/ErrorModels.cs | 0 .../{src/NetV3Server => }/NetV3Server.csproj | 8 +++++ .../{src/NetV3Server => }/Program.cs | 11 +++++-- .../Services/ClientCacheService.cs | 0 .../Properties/launchSettings.json | 31 ------------------- .../NetV3Server/appsettings.Development.json | 8 ----- .../src/NetV3Server/appsettings.json | 10 ------ 11 files changed, 17 insertions(+), 51 deletions(-) rename test-server/net-v3-server/{src/NetV3Server => }/Controllers/ClientController.cs (100%) rename test-server/net-v3-server/{src/NetV3Server => }/Controllers/ObjectController.cs (100%) rename test-server/net-v3-server/{src/NetV3Server => }/Models/ClientRequest.cs (100%) rename test-server/net-v3-server/{src/NetV3Server => }/Models/ClientResponse.cs (100%) rename test-server/net-v3-server/{src/NetV3Server => }/Models/ErrorModels.cs (100%) rename test-server/net-v3-server/{src/NetV3Server => }/NetV3Server.csproj (73%) rename test-server/net-v3-server/{src/NetV3Server => }/Program.cs (58%) rename test-server/net-v3-server/{src/NetV3Server => }/Services/ClientCacheService.cs (100%) delete mode 100644 test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json delete mode 100644 test-server/net-v3-server/src/NetV3Server/appsettings.Development.json delete mode 100644 test-server/net-v3-server/src/NetV3Server/appsettings.json diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs b/test-server/net-v3-server/Controllers/ClientController.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs rename to test-server/net-v3-server/Controllers/ClientController.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs b/test-server/net-v3-server/Controllers/ObjectController.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Controllers/ObjectController.cs rename to test-server/net-v3-server/Controllers/ObjectController.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs b/test-server/net-v3-server/Models/ClientRequest.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs rename to test-server/net-v3-server/Models/ClientRequest.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs b/test-server/net-v3-server/Models/ClientResponse.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs rename to test-server/net-v3-server/Models/ClientResponse.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs b/test-server/net-v3-server/Models/ErrorModels.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs rename to test-server/net-v3-server/Models/ErrorModels.cs diff --git a/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj b/test-server/net-v3-server/NetV3Server.csproj similarity index 73% rename from test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj rename to test-server/net-v3-server/NetV3Server.csproj index 17882f6e..a803a838 100644 --- a/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj +++ b/test-server/net-v3-server/NetV3Server.csproj @@ -10,6 +10,14 @@ false + + S3EC_V2 + + + + S3EC_V3 + + diff --git a/test-server/net-v3-server/src/NetV3Server/Program.cs b/test-server/net-v3-server/Program.cs similarity index 58% rename from test-server/net-v3-server/src/NetV3Server/Program.cs rename to test-server/net-v3-server/Program.cs index a515f3e0..c2ca4937 100644 --- a/test-server/net-v3-server/src/NetV3Server/Program.cs +++ b/test-server/net-v3-server/Program.cs @@ -2,13 +2,20 @@ var builder = WebApplication.CreateBuilder(args); -// Add services to the container builder.Services.AddControllers(); builder.Services.AddSingleton(); +#if S3EC_V2 +const int port = 8083; +#else +const int port = 8084; +#endif + +builder.WebHost.UseUrls($"http://localhost:{port}"); + var app = builder.Build(); -// Configure the HTTP request pipeline app.MapControllers(); +Console.WriteLine($"Starting server on port {port}"); app.Run(); diff --git a/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs b/test-server/net-v3-server/Services/ClientCacheService.cs similarity index 100% rename from test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs rename to test-server/net-v3-server/Services/ClientCacheService.cs diff --git a/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json b/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json deleted file mode 100644 index 347d5a85..00000000 --- a/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:39730", - "sslPort": 0 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5251", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json b/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json deleted file mode 100644 index ff66ba6b..00000000 --- a/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/test-server/net-v3-server/src/NetV3Server/appsettings.json b/test-server/net-v3-server/src/NetV3Server/appsettings.json deleted file mode 100644 index 215db1d6..00000000 --- a/test-server/net-v3-server/src/NetV3Server/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "Urls": "http://localhost:8084" -} From 4e39960edd6bcaf1f956d1ba10b297df350b7868 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 15:18:35 -0700 Subject: [PATCH 017/201] :rename rename --- .../.gitignore | 0 .../.temp/qSaved.json | 0 .../Controllers/ClientController.cs | 0 .../Controllers/ObjectController.cs | 0 .../Models/ClientRequest.cs | 0 .../Models/ClientResponse.cs | 0 .../Models/ErrorModels.cs | 0 .../NetV2V3Server.csproj} | 0 .../Program.cs | 0 .../README.md | 0 .../Services/ClientCacheService.cs | 0 test-server/net-v3-server/NetV3Server.sln | 27 ------------------- 12 files changed, 27 deletions(-) rename test-server/{net-v3-server => net-v2-v3-server}/.gitignore (100%) rename test-server/{net-v3-server => net-v2-v3-server}/.temp/qSaved.json (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Controllers/ClientController.cs (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Controllers/ObjectController.cs (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Models/ClientRequest.cs (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Models/ClientResponse.cs (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Models/ErrorModels.cs (100%) rename test-server/{net-v3-server/NetV3Server.csproj => net-v2-v3-server/NetV2V3Server.csproj} (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Program.cs (100%) rename test-server/{net-v3-server => net-v2-v3-server}/README.md (100%) rename test-server/{net-v3-server => net-v2-v3-server}/Services/ClientCacheService.cs (100%) delete mode 100644 test-server/net-v3-server/NetV3Server.sln diff --git a/test-server/net-v3-server/.gitignore b/test-server/net-v2-v3-server/.gitignore similarity index 100% rename from test-server/net-v3-server/.gitignore rename to test-server/net-v2-v3-server/.gitignore diff --git a/test-server/net-v3-server/.temp/qSaved.json b/test-server/net-v2-v3-server/.temp/qSaved.json similarity index 100% rename from test-server/net-v3-server/.temp/qSaved.json rename to test-server/net-v2-v3-server/.temp/qSaved.json diff --git a/test-server/net-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs similarity index 100% rename from test-server/net-v3-server/Controllers/ClientController.cs rename to test-server/net-v2-v3-server/Controllers/ClientController.cs diff --git a/test-server/net-v3-server/Controllers/ObjectController.cs b/test-server/net-v2-v3-server/Controllers/ObjectController.cs similarity index 100% rename from test-server/net-v3-server/Controllers/ObjectController.cs rename to test-server/net-v2-v3-server/Controllers/ObjectController.cs diff --git a/test-server/net-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs similarity index 100% rename from test-server/net-v3-server/Models/ClientRequest.cs rename to test-server/net-v2-v3-server/Models/ClientRequest.cs diff --git a/test-server/net-v3-server/Models/ClientResponse.cs b/test-server/net-v2-v3-server/Models/ClientResponse.cs similarity index 100% rename from test-server/net-v3-server/Models/ClientResponse.cs rename to test-server/net-v2-v3-server/Models/ClientResponse.cs diff --git a/test-server/net-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs similarity index 100% rename from test-server/net-v3-server/Models/ErrorModels.cs rename to test-server/net-v2-v3-server/Models/ErrorModels.cs diff --git a/test-server/net-v3-server/NetV3Server.csproj b/test-server/net-v2-v3-server/NetV2V3Server.csproj similarity index 100% rename from test-server/net-v3-server/NetV3Server.csproj rename to test-server/net-v2-v3-server/NetV2V3Server.csproj diff --git a/test-server/net-v3-server/Program.cs b/test-server/net-v2-v3-server/Program.cs similarity index 100% rename from test-server/net-v3-server/Program.cs rename to test-server/net-v2-v3-server/Program.cs diff --git a/test-server/net-v3-server/README.md b/test-server/net-v2-v3-server/README.md similarity index 100% rename from test-server/net-v3-server/README.md rename to test-server/net-v2-v3-server/README.md diff --git a/test-server/net-v3-server/Services/ClientCacheService.cs b/test-server/net-v2-v3-server/Services/ClientCacheService.cs similarity index 100% rename from test-server/net-v3-server/Services/ClientCacheService.cs rename to test-server/net-v2-v3-server/Services/ClientCacheService.cs diff --git a/test-server/net-v3-server/NetV3Server.sln b/test-server/net-v3-server/NetV3Server.sln deleted file mode 100644 index 8b1dce15..00000000 --- a/test-server/net-v3-server/NetV3Server.sln +++ /dev/null @@ -1,27 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6AD6877B-F15F-4061-B27F-9687964F5565}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetV3Server", "src\NetV3Server\NetV3Server.csproj", "{6D8C57A3-9343-42EF-8631-5B76808B8D4E}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D8C57A3-9343-42EF-8631-5B76808B8D4E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6D8C57A3-9343-42EF-8631-5B76808B8D4E} = {6AD6877B-F15F-4061-B27F-9687964F5565} - EndGlobalSection -EndGlobal From b106abad8d0f32759a9c0cbf6c2df6f4949dd8e9 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 15:30:29 -0700 Subject: [PATCH 018/201] readme --- test-server/net-v2-v3-server/README.md | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test-server/net-v2-v3-server/README.md b/test-server/net-v2-v3-server/README.md index e69de29b..fe6fb878 100644 --- a/test-server/net-v2-v3-server/README.md +++ b/test-server/net-v2-v3-server/README.md @@ -0,0 +1,72 @@ +# NetV2V3Server + +A .NET test server for Amazon S3 encryption client .NET v2 and v3. + +## Project Structure + +``` +net-v2-v3-server/ +├── Controllers/ # API controllers +├── Models/ # Data models +├── Services/ # Business logic services +├── Program.cs # Application entry point +├── NetV2V3Server.csproj # Project file +└── README.md # This file +``` + +## Running the Server + +For S3 Encryption Client v2 (runs on port 8083): + +```bash +dotnet run -p:S3EncryptionVersion=v2 +``` + +For S3 Encryption Client v3 (runs on port 8084): + +```bash +dotnet run -p:S3EncryptionVersion=v3 +``` + +## 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":false,"enableDelayedAuthenticationMode":false,"enableLegacyWrappingAlgorithms":false,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}, "encryptionContext": {"abc": "b"}}}' \ + http://localhost:8083/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 +``` From bf1d6fd0fe2d097acad63b9dca233ca38d731c9c Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 15:32:42 -0700 Subject: [PATCH 019/201] auto commit --- test-server/net-v2-v3-server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/README.md b/test-server/net-v2-v3-server/README.md index fe6fb878..e14583ad 100644 --- a/test-server/net-v2-v3-server/README.md +++ b/test-server/net-v2-v3-server/README.md @@ -1,4 +1,4 @@ -# NetV2V3Server +# Net-V2-V3-Server A .NET test server for Amazon S3 encryption client .NET v2 and v3. From 62e93838c472da23b6db808c307cd0a935296d80 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 15:33:33 -0700 Subject: [PATCH 020/201] remove temp --- .../net-v2-v3-server/.temp/qSaved.json | 24793 ---------------- 1 file changed, 24793 deletions(-) delete mode 100644 test-server/net-v2-v3-server/.temp/qSaved.json diff --git a/test-server/net-v2-v3-server/.temp/qSaved.json b/test-server/net-v2-v3-server/.temp/qSaved.json deleted file mode 100644 index b9a7f462..00000000 --- a/test-server/net-v2-v3-server/.temp/qSaved.json +++ /dev/null @@ -1,24793 +0,0 @@ -{ - "conversation_id": "3014dce3-837b-4e62-b504-d959305aeffa", - "next_message": null, - "history": [ - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "read /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server. I want to to know what this test server does. src/main.py is the starting point of the program. I want to to know what this test server does. src/main.py is the starting point of the program." - } - }, - "timestamp": "2025-09-15T11:22:35.366168-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "64d3a3d3-3433-4ef4-ae28-229d095c1ad7", - "content": "I'll read the test server directory and examine the main.py file to understand what this test server does.", - "tool_uses": [ - { - "id": "tooluse_wn9kaWDNRhmo0GRVVjns-g", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "8258c309-c7e7-443c-936c-e72aa45f251c", - "message_id": "64d3a3d3-3433-4ef4-ae28-229d095c1ad7", - "request_start_timestamp_ms": 1757960555371, - "stream_end_timestamp_ms": 1757960565021, - "time_to_first_chunk": { - "secs": 8, - "nanos": 612063541 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 31875 - }, - { - "secs": 0, - "nanos": 21042 - }, - { - "secs": 0, - "nanos": 789750 - }, - { - "secs": 0, - "nanos": 2013750 - }, - { - "secs": 0, - "nanos": 2938125 - }, - { - "secs": 0, - "nanos": 2310500 - }, - { - "secs": 0, - "nanos": 2651458 - }, - { - "secs": 0, - "nanos": 2903583 - }, - { - "secs": 0, - "nanos": 15166 - }, - { - "secs": 0, - "nanos": 2752542 - }, - { - "secs": 0, - "nanos": 138983084 - }, - { - "secs": 0, - "nanos": 814864125 - }, - { - "secs": 0, - "nanos": 16042 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 0, - "nanos": 8959 - }, - { - "secs": 0, - "nanos": 8500 - }, - { - "secs": 0, - "nanos": 2501708 - }, - { - "secs": 0, - "nanos": 11750 - }, - { - "secs": 0, - "nanos": 5166 - }, - { - "secs": 0, - "nanos": 16792 - }, - { - "secs": 0, - "nanos": 1965709 - }, - { - "secs": 0, - "nanos": 3598959 - }, - { - "secs": 0, - "nanos": 15625 - }, - { - "secs": 0, - "nanos": 5917 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 1904667 - }, - { - "secs": 0, - "nanos": 7750 - }, - { - "secs": 0, - "nanos": 2292 - }, - { - "secs": 0, - "nanos": 251125 - }, - { - "secs": 0, - "nanos": 2077292 - }, - { - "secs": 0, - "nanos": 5041 - }, - { - "secs": 0, - "nanos": 54208292 - }, - { - "secs": 0, - "nanos": 163583 - }, - { - "secs": 0, - "nanos": 22875 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 461, - "response_size": 259, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_wn9kaWDNRhmo0GRVVjns-g", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_wn9kaWDNRhmo0GRVVjns-g", - "content": [ - { - "Text": "-rw-r--r-- 1 504 20 473 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/pyproject.toml\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/tests\n-rw-r--r-- 1 504 20 762 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md\n-rw-r--r-- 1 504 20 302 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/.gitignore\ndrwxr-xr-x 6 504 20 192 Sep 15 17:48 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/.venv\n-rw-r--r-- 1 504 20 63517 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/poetry.lock\ndrwxr-xr-x 5 504 20 160 Sep 15 17:49 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "4469cafc-b106-4691-b79b-6b1ac9a72bc3", - "content": "", - "tool_uses": [ - { - "id": "tooluse_OwieXzrlRPybnv0uEJiBLQ", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "d45b8b2f-092b-486f-bb3c-2003d300b227", - "message_id": "4469cafc-b106-4691-b79b-6b1ac9a72bc3", - "request_start_timestamp_ms": 1757960565440, - "stream_end_timestamp_ms": 1757960569373, - "time_to_first_chunk": { - "secs": 3, - "nanos": 918900875 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 21625 - }, - { - "secs": 0, - "nanos": 10375 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 6083 - }, - { - "secs": 0, - "nanos": 1417 - }, - { - "secs": 0, - "nanos": 2875 - }, - { - "secs": 0, - "nanos": 2333 - }, - { - "secs": 0, - "nanos": 2167042 - }, - { - "secs": 0, - "nanos": 7250 - }, - { - "secs": 0, - "nanos": 3458 - }, - { - "secs": 0, - "nanos": 844208 - }, - { - "secs": 0, - "nanos": 2259125 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 2917 - }, - { - "secs": 0, - "nanos": 2601833 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1067375 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 448750 - }, - { - "secs": 0, - "nanos": 1139916 - }, - { - "secs": 0, - "nanos": 17458 - }, - { - "secs": 0, - "nanos": 3188542 - }, - { - "secs": 0, - "nanos": 17125 - }, - { - "secs": 0, - "nanos": 41 - } - ], - "user_prompt_length": 0, - "response_size": 157, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_OwieXzrlRPybnv0uEJiBLQ", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_OwieXzrlRPybnv0uEJiBLQ", - "content": [ - { - "Text": "-rw-r--r-- 1 504 20 46 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/__init__.py\n-rwxr-xr-x 1 504 20 7206 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py\ndrwxr-xr-x 7 504 20 224 Sep 15 17:49 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/python_server.egg-info" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "8f3bb12c-4325-4801-8715-b84057879efc", - "content": "", - "tool_uses": [ - { - "id": "tooluse_zAwEy_xQTPSKfc4SstAz7Q", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "1f29e3af-a438-4194-b265-248b75feca18", - "message_id": "8f3bb12c-4325-4801-8715-b84057879efc", - "request_start_timestamp_ms": 1757960570048, - "stream_end_timestamp_ms": 1757960574542, - "time_to_first_chunk": { - "secs": 3, - "nanos": 690460292 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 33125 - }, - { - "secs": 0, - "nanos": 10792 - }, - { - "secs": 0, - "nanos": 670367333 - }, - { - "secs": 0, - "nanos": 55600125 - }, - { - "secs": 0, - "nanos": 40265375 - }, - { - "secs": 0, - "nanos": 19194292 - }, - { - "secs": 0, - "nanos": 9083 - }, - { - "secs": 0, - "nanos": 2260458 - }, - { - "secs": 0, - "nanos": 11375 - }, - { - "secs": 0, - "nanos": 681333 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 1578584 - }, - { - "secs": 0, - "nanos": 5208 - }, - { - "secs": 0, - "nanos": 1286875 - }, - { - "secs": 0, - "nanos": 3958 - }, - { - "secs": 0, - "nanos": 451333 - }, - { - "secs": 0, - "nanos": 293125 - }, - { - "secs": 0, - "nanos": 2185458 - }, - { - "secs": 0, - "nanos": 4459 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 163667 - }, - { - "secs": 0, - "nanos": 1635459 - }, - { - "secs": 0, - "nanos": 4833 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 1836083 - }, - { - "secs": 0, - "nanos": 4042 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 346167 - }, - { - "secs": 0, - "nanos": 868458 - }, - { - "secs": 0, - "nanos": 5209 - }, - { - "secs": 0, - "nanos": 617375 - }, - { - "secs": 0, - "nanos": 3051375 - }, - { - "secs": 0, - "nanos": 15125 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 160, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_zAwEy_xQTPSKfc4SstAz7Q", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_zAwEy_xQTPSKfc4SstAz7Q", - "content": [ - { - "Text": "\"\"\"\nMain entry point for the Python server.\n\"\"\"\n\nfrom fastapi import FastAPI, Request, HTTPException, Response, status\nfrom fastapi.responses import JSONResponse\nfrom s3_encryption import S3EncryptionClient, S3EncryptionClientConfig\nfrom s3_encryption.exceptions import S3EncryptionClientError\nfrom s3_encryption.materials.kms_keyring import KmsKeyring\nimport boto3\nimport uvicorn\nimport json\nimport uuid\n\napp = FastAPI(title=\"Python Server\")\n\n# Dictionary to store clients with their UUIDs as keys\nclient_cache = {}\n\n\n# Java gets a list, but since there's no Smithy Python Server,\n# this is just a string.\ndef metadata_string_to_map(md_string):\n md = {}\n if md_string == \"\":\n return md\n md_list = md_string.split(\",\")\n for entry in md_list:\n # Split on \"]:[\" to separate key and value\n parts = entry.split(\"]:[\")\n if len(parts) == 2:\n # Remove remaining brackets from start and end\n key = parts[0][1:] # Remove first character\n value = parts[1][:-1] # Remove last character\n md[key] = value\n else:\n raise ValueError(f\"Malformed metadata list entry: {entry}\")\n return md\n\n\ndef create_generic_server_error(\n message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n):\n \"\"\"\n Create a response that matches the GenericServerError type from the Smithy model.\n Used for internal server errors.\n \"\"\"\n return JSONResponse(\n status_code=status_code,\n content={\"__type\": \"software.amazon.encryption.s3#GenericServerError\", \"message\": message},\n )\n\n\ndef create_s3_encryption_client_error(\n message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n):\n \"\"\"\n Create a response that matches the S3EncryptionClientError type from the Smithy model.\n Used for errors thrown by the S3 Encryption Client.\n \"\"\"\n return JSONResponse(\n status_code=status_code,\n content={\n \"__type\": \"software.amazon.encryption.s3#S3EncryptionClientError\",\n \"message\": message,\n },\n )\n\n\n@app.put(\"/object/{bucket}/{key}\")\nasync def put_object(bucket: str, key: str, request: Request):\n \"\"\"\n Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient\n to make a PutObject request to S3.\n \"\"\"\n client_id = request.headers.get(\"ClientID\")\n body = await request.body()\n\n if not client_id:\n return create_generic_server_error(\n \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n try:\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n # Make the PutObject request\n response = client.put_object(\n **{\"Bucket\": bucket, \"Key\": key, \"Body\": body, \"EncryptionContext\": enc_ctx}\n )\n\n # Return the appropriate response\n return {\n \"bucket\": bucket,\n \"key\": key,\n \"metadata\": metadata if isinstance(metadata, list) else [],\n }\n except Exception as e:\n return create_s3_encryption_client_error(f\"Failed to put object: {str(e)}\")\n\n\n@app.get(\"/object/{bucket}/{key}\")\nasync def get_object(bucket: str, key: str, request: Request):\n \"\"\"\n Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient\n to make a GetObject request to S3.\n \"\"\"\n client_id = request.headers.get(\"ClientID\")\n\n if not client_id:\n return create_generic_server_error(\n \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n try:\n # Use the client to make a GetObject request to S3\n response = client.get_object(**{\"Bucket\": bucket, \"Key\": key, \"EncryptionContext\": enc_ctx})\n\n # Extract the body and metadata from the response\n body = response.get(\"Body\").read() if response.get(\"Body\") else b\"\"\n metadata = response.get(\"Metadata\", [])\n\n # Convert metadata dictionary to a list of key-value pairs if it's a dict\n if isinstance(metadata, dict):\n metadata_list = [f\"{key}={value}\" for key, value in metadata.items()]\n else:\n metadata_list = metadata if isinstance(metadata, list) else []\n\n # Set the Content-Metadata header in the response\n # Convert metadata_list to a comma-separated string\n metadata_str = \",\".join(metadata_list) if metadata_list else \"\"\n headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n \"\"\"\n Handle POST requests to /client by creating an S3EncryptionClient.\n \"\"\"\n body = await request.body()\n\n # Parse the bytes object as JSON\n try:\n # Decode bytes to string and parse as JSON\n parsed_data = json.loads(body.decode(\"utf-8\"))\n\n # Extract config from the parsed data\n config_data = parsed_data.get(\"config\", {})\n # Extract key material if provided\n key_material = config_data.get(\"keyMaterial\", {})\n\n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n\n # TODO pull region from ARN\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n\n # Create S3EncryptionClient\n client = S3EncryptionClient(wrapped_client, client_config)\n\n # Generate a client ID using UUID\n client_id = str(uuid.uuid4())\n\n # Add the client to the client_cache dictionary\n client_cache[client_id] = client\n\n return {\"clientId\": client_id}\n except json.JSONDecodeError as e:\n return create_generic_server_error(\n \"Invalid JSON in request body\", status.HTTP_400_BAD_REQUEST\n )\n except Exception as e:\n return create_s3_encryption_client_error(f\"Failed to create client: {str(e)}\")\n\n\ndef main():\n \"\"\"\n Main function to start the server.\n \"\"\"\n uvicorn.run(app, host=\"localhost\", port=8081)\n\n\nif __name__ == \"__main__\":\n main()" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "eecc6b5e-0499-4780-85c0-d03b88b688af", - "content": "", - "tool_uses": [ - { - "id": "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/README.md" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "8c33c92c-1fc1-47d2-9c9a-a0c3c78778ce", - "message_id": "eecc6b5e-0499-4780-85c0-d03b88b688af", - "request_start_timestamp_ms": 1757960575161, - "stream_end_timestamp_ms": 1757960579976, - "time_to_first_chunk": { - "secs": 4, - "nanos": 208416250 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 27083 - }, - { - "secs": 0, - "nanos": 18156917 - }, - { - "secs": 0, - "nanos": 564608875 - }, - { - "secs": 0, - "nanos": 11875 - }, - { - "secs": 0, - "nanos": 4334 - }, - { - "secs": 0, - "nanos": 650834 - }, - { - "secs": 0, - "nanos": 1246666 - }, - { - "secs": 0, - "nanos": 6917 - }, - { - "secs": 0, - "nanos": 1087250 - }, - { - "secs": 0, - "nanos": 477042 - }, - { - "secs": 0, - "nanos": 2046375 - }, - { - "secs": 0, - "nanos": 17334 - }, - { - "secs": 0, - "nanos": 2389709 - }, - { - "secs": 0, - "nanos": 124291 - }, - { - "secs": 0, - "nanos": 2041834 - }, - { - "secs": 0, - "nanos": 16084 - }, - { - "secs": 0, - "nanos": 1479250 - }, - { - "secs": 0, - "nanos": 16417 - }, - { - "secs": 0, - "nanos": 9639041 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 2583 - }, - { - "secs": 0, - "nanos": 2194750 - }, - { - "secs": 0, - "nanos": 8084 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2667 - }, - { - "secs": 0, - "nanos": 2583 - }, - { - "secs": 0, - "nanos": 24375 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 158, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_3Kqt2sx_SH-6SF6j-eq8eA", - "content": [ - { - "Text": "# Python Server\n\nA FastAPI-based Python server implementation.\n\n## Setup\n\n1. Install uv (if not already installed):\n```bash\npip install uv\n```\n\n2. Create a virtual environment and install dependencies:\n```bash\nuv venv\nsource .venv/bin/activate\nuv pip install -e .\nuv pip install -e ../..\n```\n\n## Development\n\n- Source code is in the `src` directory\n- Tests are in the `tests` directory\n- Use `source .venv/bin/activate` to activate the virtual environment\n- Use `uv pip install {package}` to add new dependencies\n- Use `uv pip install {package} --dev` to add new development dependencies\n\n## Running the Server\n\n```bash\n.venv/bin/python src/main.py\n```\n\nThe server will start on `http://localhost:8081`.\n\n## Running Tests\n\n```bash\n.venv/bin/python -m pytest\n```" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "62ef600e-1f68-494d-9bb0-00a7ce6521bd", - "content": "This test server is a **FastAPI-based HTTP server that provides a REST API wrapper around the S3 Encryption Client**. Here's what it does:\n\n## Purpose\nThe server acts as a test harness for the Amazon S3 Encryption Client Python library, allowing HTTP-based testing of client-side encryption functionality.\n\n## Key Functionality\n\n**Client Management:**\n- `POST /client` - Creates S3EncryptionClient instances with KMS keyring configuration\n- Stores clients in memory cache with UUID identifiers\n- Supports legacy wrapping algorithms configuration\n\n**S3 Operations:**\n- `PUT /object/{bucket}/{key}` - Encrypts and uploads objects to S3\n- `GET /object/{bucket}/{key}` - Downloads and decrypts objects from S3\n- Handles encryption context via `Content-Metadata` headers\n\n**Architecture:**\n- Uses FastAPI framework running on localhost:8081\n- Integrates with AWS KMS for key management (hardcoded to us-west-2 region)\n- Converts metadata between string format and dictionary for encryption context\n- Provides structured error responses matching Smithy model types\n\nThe server essentially allows you to test S3 client-side encryption operations through HTTP requests rather than direct Python API calls, making it useful for integration testing or cross-language compatibility testing." - } - }, - "request_metadata": { - "request_id": "b547db21-e620-4a26-9e5d-9111c2afd95f", - "message_id": "62ef600e-1f68-494d-9bb0-00a7ce6521bd", - "request_start_timestamp_ms": 1757960580622, - "stream_end_timestamp_ms": 1757960588576, - "time_to_first_chunk": { - "secs": 1, - "nanos": 898907916 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 31500 - }, - { - "secs": 0, - "nanos": 8458 - }, - { - "secs": 0, - "nanos": 267761083 - }, - { - "secs": 0, - "nanos": 222552417 - }, - { - "secs": 0, - "nanos": 112580750 - }, - { - "secs": 0, - "nanos": 380233875 - }, - { - "secs": 0, - "nanos": 130621583 - }, - { - "secs": 0, - "nanos": 286579125 - }, - { - "secs": 0, - "nanos": 167468167 - }, - { - "secs": 0, - "nanos": 337833083 - }, - { - "secs": 0, - "nanos": 125466291 - }, - { - "secs": 0, - "nanos": 322277375 - }, - { - "secs": 0, - "nanos": 484840708 - }, - { - "secs": 0, - "nanos": 46890459 - }, - { - "secs": 0, - "nanos": 114786125 - }, - { - "secs": 0, - "nanos": 102866542 - }, - { - "secs": 0, - "nanos": 163200541 - }, - { - "secs": 0, - "nanos": 91320500 - }, - { - "secs": 0, - "nanos": 112123500 - }, - { - "secs": 0, - "nanos": 337346375 - }, - { - "secs": 0, - "nanos": 155788083 - }, - { - "secs": 0, - "nanos": 178513791 - }, - { - "secs": 0, - "nanos": 117490625 - }, - { - "secs": 0, - "nanos": 190261417 - }, - { - "secs": 0, - "nanos": 161779000 - }, - { - "secs": 0, - "nanos": 234609125 - }, - { - "secs": 0, - "nanos": 161274125 - }, - { - "secs": 0, - "nanos": 282359750 - }, - { - "secs": 0, - "nanos": 450667000 - }, - { - "secs": 0, - "nanos": 243907292 - }, - { - "secs": 0, - "nanos": 53718917 - }, - { - "secs": 0, - "nanos": 17418375 - }, - { - "secs": 0, - "nanos": 42 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 1279, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Now, I want to start writing similar implementation but in .Net. I have already created net-v3-server where I plan to test S3-EC net v3 like python. Don't write any code but I want to prepare first. where do you think I should start? I don't know dotnet much, how should project structure look like?" - } - }, - "timestamp": "2025-09-15T11:27:34.848706-07:00", - "images": null - }, - "assistant": { - "Response": { - "message_id": "8fd1cb75-b685-4896-8ddc-5a8b849c40dd", - "content": "Based on the Python test server structure, here's how you should approach the .NET implementation:\n\n## Project Structure for .NET\n\n```\nnet-v3-server/\n├── src/\n│ ├── NetV3Server/\n│ │ ├── Controllers/\n│ │ │ ├── ClientController.cs\n│ │ │ └── ObjectController.cs\n│ │ ├── Models/\n│ │ │ ├── ClientRequest.cs\n│ │ │ ├── ClientResponse.cs\n│ │ │ └── ErrorModels.cs\n│ │ ├── Services/\n│ │ │ └── ClientCacheService.cs\n│ │ ├── Program.cs\n│ │ └── NetV3Server.csproj\n├── tests/\n│ └── NetV3Server.Tests/\n│ └── NetV3Server.Tests.csproj\n├── NetV3Server.sln\n└── README.md\n```\n\n## Where to Start\n\n**1. Choose Framework:**\n- Use **ASP.NET Core Web API** (equivalent to FastAPI)\n- Target .NET 6+ for modern features\n\n**2. Key Dependencies You'll Need:**\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK\n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n- `Microsoft.AspNetCore.Mvc` - Web API framework\n\n**3. Core Components to Implement:**\n- **Program.cs** - Entry point (like main.py)\n- **Controllers** - Handle HTTP endpoints (`/client`, `/object/{bucket}/{key}`)\n- **ClientCacheService** - In-memory client storage (like client_cache dict)\n- **Models** - Request/response DTOs\n- **Error handling** - Custom exception responses\n\n**4. Learning Path:**\n1. Start with basic ASP.NET Core Web API tutorial\n2. Learn dependency injection (built into .NET)\n3. Understand controllers and routing\n4. Study AWS SDK for .NET usage patterns\n\n**5. Initial Setup Steps:**\n1. Install .NET SDK\n2. Create solution: `dotnet new sln -n NetV3Server`\n3. Create web API project: `dotnet new webapi -n NetV3Server`\n4. Add AWS NuGet packages\n5. Set up basic controller structure\n\nThe .NET version will be more structured than Python due to strong typing and dependency injection, but the core logic flow will mirror your Python implementation exactly." - } - }, - "request_metadata": { - "request_id": "a4195bc7-d719-46ca-ba46-618bac161476", - "message_id": "8fd1cb75-b685-4896-8ddc-5a8b849c40dd", - "request_start_timestamp_ms": 1757960854851, - "stream_end_timestamp_ms": 1757960870815, - "time_to_first_chunk": { - "secs": 4, - "nanos": 37539625 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 61625 - }, - { - "secs": 0, - "nanos": 14875 - }, - { - "secs": 0, - "nanos": 425821416 - }, - { - "secs": 0, - "nanos": 400610958 - }, - { - "secs": 0, - "nanos": 67599084 - }, - { - "secs": 0, - "nanos": 187239333 - }, - { - "secs": 0, - "nanos": 288225625 - }, - { - "secs": 0, - "nanos": 106530750 - }, - { - "secs": 0, - "nanos": 171668125 - }, - { - "secs": 0, - "nanos": 118489750 - }, - { - "secs": 0, - "nanos": 479248708 - }, - { - "secs": 0, - "nanos": 173749459 - }, - { - "secs": 0, - "nanos": 63474167 - }, - { - "secs": 0, - "nanos": 228928167 - }, - { - "secs": 0, - "nanos": 242453083 - }, - { - "secs": 0, - "nanos": 333747083 - }, - { - "secs": 0, - "nanos": 8208 - }, - { - "secs": 0, - "nanos": 174769042 - }, - { - "secs": 0, - "nanos": 296416750 - }, - { - "secs": 0, - "nanos": 225901042 - }, - { - "secs": 0, - "nanos": 214547375 - }, - { - "secs": 0, - "nanos": 720664583 - }, - { - "secs": 0, - "nanos": 6917 - }, - { - "secs": 0, - "nanos": 343617917 - }, - { - "secs": 0, - "nanos": 294897542 - }, - { - "secs": 0, - "nanos": 908833 - }, - { - "secs": 0, - "nanos": 92441209 - }, - { - "secs": 0, - "nanos": 231569625 - }, - { - "secs": 0, - "nanos": 323144750 - }, - { - "secs": 0, - "nanos": 548053875 - }, - { - "secs": 0, - "nanos": 396507917 - }, - { - "secs": 0, - "nanos": 218866875 - }, - { - "secs": 0, - "nanos": 7125 - }, - { - "secs": 0, - "nanos": 231234917 - }, - { - "secs": 0, - "nanos": 504475834 - }, - { - "secs": 0, - "nanos": 456930625 - }, - { - "secs": 0, - "nanos": 286679625 - }, - { - "secs": 0, - "nanos": 537425792 - }, - { - "secs": 0, - "nanos": 348420584 - }, - { - "secs": 0, - "nanos": 541625292 - }, - { - "secs": 0, - "nanos": 14708 - }, - { - "secs": 0, - "nanos": 4929875 - }, - { - "secs": 0, - "nanos": 259916958 - }, - { - "secs": 1, - "nanos": 383415250 - }, - { - "secs": 0, - "nanos": 13708 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 22958 - }, - { - "secs": 0, - "nanos": 125 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 457, - "response_size": 2080, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "How do I create such directory structure?" - } - }, - "timestamp": "2025-09-15T11:51:52.140173-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "db3b1a3c-2ee7-4562-9756-e1f0480a532b", - "content": "I'll help you create the .NET project structure. Let me check your current directory first and then create the structure.", - "tool_uses": [ - { - "id": "tooluse_A770w0NKREKNmAarZ5tgSg", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "pwd" - }, - "orig_args": { - "command": "pwd" - } - } - ] - } - }, - "request_metadata": { - "request_id": "f318f46b-1e28-465b-a97b-9a5088262428", - "message_id": "db3b1a3c-2ee7-4562-9756-e1f0480a532b", - "request_start_timestamp_ms": 1757962312144, - "stream_end_timestamp_ms": 1757962317438, - "time_to_first_chunk": { - "secs": 3, - "nanos": 943614042 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 18250 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 457979625 - }, - { - "secs": 0, - "nanos": 270583875 - }, - { - "secs": 0, - "nanos": 108414166 - }, - { - "secs": 0, - "nanos": 388537542 - }, - { - "secs": 0, - "nanos": 118164000 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 6494875 - }, - { - "secs": 0, - "nanos": 11208 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 199, - "response_size": 139, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_A770w0NKREKNmAarZ5tgSg", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_A770w0NKREKNmAarZ5tgSg", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "849cfa6f-127e-4739-8fea-80c195e29c7c", - "content": "", - "tool_uses": [ - { - "id": "tooluse_EAh8v8zhQrGEC60SXV4PLg", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "876e3ba6-676a-4425-ac68-61ea26de69c0", - "message_id": "849cfa6f-127e-4739-8fea-80c195e29c7c", - "request_start_timestamp_ms": 1757962317981, - "stream_end_timestamp_ms": 1757962321238, - "time_to_first_chunk": { - "secs": 3, - "nanos": 191994292 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 12417 - }, - { - "secs": 0, - "nanos": 10792 - }, - { - "secs": 0, - "nanos": 13377458 - }, - { - "secs": 0, - "nanos": 5312959 - }, - { - "secs": 0, - "nanos": 2269458 - }, - { - "secs": 0, - "nanos": 6975500 - }, - { - "secs": 0, - "nanos": 2127125 - }, - { - "secs": 0, - "nanos": 2151958 - }, - { - "secs": 0, - "nanos": 6583 - }, - { - "secs": 0, - "nanos": 2309292 - }, - { - "secs": 0, - "nanos": 4186208 - }, - { - "secs": 0, - "nanos": 934584 - }, - { - "secs": 0, - "nanos": 6041 - }, - { - "secs": 0, - "nanos": 25295125 - }, - { - "secs": 0, - "nanos": 13792 - }, - { - "secs": 0, - "nanos": 4541 - }, - { - "secs": 0, - "nanos": 4125 - }, - { - "secs": 0, - "nanos": 31958 - }, - { - "secs": 0, - "nanos": 13458 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 127, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_EAh8v8zhQrGEC60SXV4PLg", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_EAh8v8zhQrGEC60SXV4PLg", - "content": [ - { - "Text": "drwxr-xr-x 12 504 20 384 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/cdk\n-rw-r--r-- 1 504 20 309 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/CODE_OF_CONDUCT.md\n-rw-r--r-- 1 504 20 10142 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/LICENSE\n-rw-r--r-- 1 504 20 172 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/requirements.txt\ndrwxr-xr-x 9 504 20 288 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test\n-rw-r--r-- 1 504 20 875 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/Makefile\n-rw-r--r-- 1 504 20 1205 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/pyproject.toml\n-rw-r--r-- 1 504 20 1509 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/SUPPORT_POLICY.rst\n-rw-r--r-- 1 504 20 67 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/NOTICE\n-rw-r--r-- 1 504 20 1605 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/README.md\ndrwxr-xr-x 10 504 20 320 Sep 15 18:15 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server\n-rw-r--r-- 1 504 20 529 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.gitignore\n-rw-r--r-- 1 504 20 3160 Sep 15 17:34 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/CONTRIBUTING.md\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.github\ndrwxr-xr-x 14 504 20 448 Sep 15 18:16 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.git\ndrwxr-xr-x 6 504 20 192 Sep 15 18:13 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/.idea\ndrwxr-xr-x 3 504 20 96 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/src" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "86bbd309-5bbc-4731-b904-c557276ddd97", - "content": "", - "tool_uses": [ - { - "id": "tooluse_MKWwYFMvTsm1D4xETOrxBw", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "6a0f8768-92c8-4ce2-9aad-712257ec58c1", - "message_id": "86bbd309-5bbc-4731-b904-c557276ddd97", - "request_start_timestamp_ms": 1757962321748, - "stream_end_timestamp_ms": 1757962325195, - "time_to_first_chunk": { - "secs": 2, - "nanos": 461779250 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 12250 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 743658500 - }, - { - "secs": 0, - "nanos": 46745625 - }, - { - "secs": 0, - "nanos": 14185833 - }, - { - "secs": 0, - "nanos": 38030125 - }, - { - "secs": 0, - "nanos": 47036208 - }, - { - "secs": 0, - "nanos": 20137041 - }, - { - "secs": 0, - "nanos": 42643208 - }, - { - "secs": 0, - "nanos": 25078666 - }, - { - "secs": 0, - "nanos": 18416 - }, - { - "secs": 0, - "nanos": 99292 - }, - { - "secs": 0, - "nanos": 464959 - }, - { - "secs": 0, - "nanos": 1442875 - }, - { - "secs": 0, - "nanos": 4709 - }, - { - "secs": 0, - "nanos": 420709 - }, - { - "secs": 0, - "nanos": 1605791 - }, - { - "secs": 0, - "nanos": 4416 - }, - { - "secs": 0, - "nanos": 3888042 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 139, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_MKWwYFMvTsm1D4xETOrxBw", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_MKWwYFMvTsm1D4xETOrxBw", - "content": [ - { - "Text": "drwxr-xr-x 14 504 20 448 Sep 15 17:42 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/java-server\n-rw-r--r-- 1 504 20 4085 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/Makefile\n-rw-r--r-- 1 504 20 1624 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/README.md\ndrwxr-xr-x 4 504 20 128 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server\ndrwxr-xr-x 5 504 20 160 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/model\ndrwxr-xr-x 15 504 20 480 Sep 15 17:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/java-tests\n-rw-r--r-- 1 504 20 1625 Sep 15 17:35 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/gradle.init\ndrwxr-xr-x 9 504 20 288 Sep 15 17:48 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "0a14913f-2ab4-4f68-b20c-6b695134792e", - "content": "I see you already have a `net-v3-server` directory. Let me check what's in it and then create the proper .NET structure:", - "tool_uses": [ - { - "id": "tooluse_KZne_4WjSeeHSL7wjzl80A", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "d849b78f-743d-49c9-9a8b-e8060441d8e6", - "message_id": "0a14913f-2ab4-4f68-b20c-6b695134792e", - "request_start_timestamp_ms": 1757962325820, - "stream_end_timestamp_ms": 1757962329456, - "time_to_first_chunk": { - "secs": 3, - "nanos": 129136875 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 18875 - }, - { - "secs": 0, - "nanos": 9625 - }, - { - "secs": 0, - "nanos": 25445709 - }, - { - "secs": 0, - "nanos": 24529375 - }, - { - "secs": 0, - "nanos": 31320833 - }, - { - "secs": 0, - "nanos": 125152417 - }, - { - "secs": 0, - "nanos": 139956417 - }, - { - "secs": 0, - "nanos": 7139958 - }, - { - "secs": 0, - "nanos": 6217750 - }, - { - "secs": 0, - "nanos": 9087167 - }, - { - "secs": 0, - "nanos": 5574042 - }, - { - "secs": 0, - "nanos": 4945542 - }, - { - "secs": 0, - "nanos": 8432500 - }, - { - "secs": 0, - "nanos": 6706208 - }, - { - "secs": 0, - "nanos": 167542 - }, - { - "secs": 0, - "nanos": 6084584 - }, - { - "secs": 0, - "nanos": 4448458 - }, - { - "secs": 0, - "nanos": 24230375 - }, - { - "secs": 0, - "nanos": 3260834 - }, - { - "secs": 0, - "nanos": 15226250 - }, - { - "secs": 0, - "nanos": 1786167 - }, - { - "secs": 0, - "nanos": 20510084 - }, - { - "secs": 0, - "nanos": 6605416 - }, - { - "secs": 0, - "nanos": 5046167 - }, - { - "secs": 0, - "nanos": 2842833 - }, - { - "secs": 0, - "nanos": 21944500 - }, - { - "secs": 0, - "nanos": 11791 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 273, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_KZne_4WjSeeHSL7wjzl80A", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_KZne_4WjSeeHSL7wjzl80A", - "content": [ - { - "Text": "drwxr-xr-x 3 504 20 96 Sep 15 18:45 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp\n-rw-r--r-- 1 504 20 5281 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/README.md" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "8022e5ff-cabd-4f99-85e4-79375b26208e", - "content": "Now I'll create the proper .NET project structure in your existing `net-v3-server` directory:", - "tool_uses": [ - { - "id": "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server" - } - } - ] - } - }, - "request_metadata": { - "request_id": "0191a0ff-7bea-4a4e-ab88-b6ae8603d6b8", - "message_id": "8022e5ff-cabd-4f99-85e4-79375b26208e", - "request_start_timestamp_ms": 1757962329981, - "stream_end_timestamp_ms": 1757962333285, - "time_to_first_chunk": { - "secs": 2, - "nanos": 470799541 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 14500 - }, - { - "secs": 0, - "nanos": 7500 - }, - { - "secs": 0, - "nanos": 11875 - }, - { - "secs": 0, - "nanos": 113253000 - }, - { - "secs": 0, - "nanos": 336681291 - }, - { - "secs": 0, - "nanos": 354360250 - }, - { - "secs": 0, - "nanos": 264750 - }, - { - "secs": 0, - "nanos": 1762792 - }, - { - "secs": 0, - "nanos": 2922166 - }, - { - "secs": 0, - "nanos": 7500 - }, - { - "secs": 0, - "nanos": 2239833 - }, - { - "secs": 0, - "nanos": 804958 - }, - { - "secs": 0, - "nanos": 1851250 - }, - { - "secs": 0, - "nanos": 1119125 - }, - { - "secs": 0, - "nanos": 886042 - }, - { - "secs": 0, - "nanos": 500541 - }, - { - "secs": 0, - "nanos": 318125 - }, - { - "secs": 0, - "nanos": 402833 - }, - { - "secs": 0, - "nanos": 3033875 - }, - { - "secs": 0, - "nanos": 7542 - }, - { - "secs": 0, - "nanos": 720958 - }, - { - "secs": 0, - "nanos": 1408416 - }, - { - "secs": 0, - "nanos": 291333 - }, - { - "secs": 0, - "nanos": 1546417 - }, - { - "secs": 0, - "nanos": 7198250 - }, - { - "secs": 0, - "nanos": 1327167 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 216, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_FBMTUQ7ZTlaHkOVzTDELJQ", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "121dc459-0d38-48b7-9495-1f3514c24722", - "content": "", - "tool_uses": [ - { - "id": "tooluse_JPl0vN_HSaeMQDGkzksZTw", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet new sln -n NetV3Server" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet new sln -n NetV3Server" - } - } - ] - } - }, - "request_metadata": { - "request_id": "02f07f16-1f02-4a6d-a2d1-53bc8860db5a", - "message_id": "121dc459-0d38-48b7-9495-1f3514c24722", - "request_start_timestamp_ms": 1757962425667, - "stream_end_timestamp_ms": 1757962429478, - "time_to_first_chunk": { - "secs": 3, - "nanos": 741227875 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 41250 - }, - { - "secs": 0, - "nanos": 10916 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 3542 - }, - { - "secs": 0, - "nanos": 4209 - }, - { - "secs": 0, - "nanos": 18000 - }, - { - "secs": 0, - "nanos": 17000 - }, - { - "secs": 0, - "nanos": 5084 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 26250 - }, - { - "secs": 0, - "nanos": 8792 - }, - { - "secs": 0, - "nanos": 4334 - }, - { - "secs": 0, - "nanos": 572084 - }, - { - "secs": 0, - "nanos": 68028708 - }, - { - "secs": 0, - "nanos": 9375 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 16666 - }, - { - "secs": 0, - "nanos": 2584 - }, - { - "secs": 0, - "nanos": 9958 - }, - { - "secs": 0, - "nanos": 32667 - }, - { - "secs": 0, - "nanos": 22667 - }, - { - "secs": 0, - "nanos": 14125 - }, - { - "secs": 0, - "nanos": 5875 - }, - { - "secs": 0, - "nanos": 16958 - }, - { - "secs": 0, - "nanos": 13083 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 156, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_JPl0vN_HSaeMQDGkzksZTw", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_JPl0vN_HSaeMQDGkzksZTw", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "The template \"Solution File\" was created successfully.\n", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "bc045b27-ecbc-4b79-9683-99be21f72639", - "content": "", - "tool_uses": [ - { - "id": "tooluse_HkQDlinhQbCf3twlDtiPSA", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server" - } - } - ] - } - }, - "request_metadata": { - "request_id": "24f6f914-4b3c-4457-952a-d6c5dd280613", - "message_id": "bc045b27-ecbc-4b79-9683-99be21f72639", - "request_start_timestamp_ms": 1757962530874, - "stream_end_timestamp_ms": 1757962533948, - "time_to_first_chunk": { - "secs": 2, - "nanos": 485698458 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 19416 - }, - { - "secs": 0, - "nanos": 56898583 - }, - { - "secs": 0, - "nanos": 466530459 - }, - { - "secs": 0, - "nanos": 20167 - }, - { - "secs": 0, - "nanos": 963875 - }, - { - "secs": 0, - "nanos": 11375 - }, - { - "secs": 0, - "nanos": 254750 - }, - { - "secs": 0, - "nanos": 3255250 - }, - { - "secs": 0, - "nanos": 15084 - }, - { - "secs": 0, - "nanos": 699333 - }, - { - "secs": 0, - "nanos": 2341083 - }, - { - "secs": 0, - "nanos": 13167 - }, - { - "secs": 0, - "nanos": 56429375 - }, - { - "secs": 0, - "nanos": 23292 - }, - { - "secs": 0, - "nanos": 266875 - }, - { - "secs": 0, - "nanos": 7875 - }, - { - "secs": 0, - "nanos": 6125 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 6000 - }, - { - "secs": 0, - "nanos": 5458 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 6083 - }, - { - "secs": 0, - "nanos": 4208 - }, - { - "secs": 0, - "nanos": 5833 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 28291 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 151, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_HkQDlinhQbCf3twlDtiPSA", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_HkQDlinhQbCf3twlDtiPSA", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "8b913888-4a02-4e32-9b59-02531f6a3ff6", - "content": "", - "tool_uses": [ - { - "id": "tooluse_psTtPl0pSXW4OptoieHkZQ", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet new webapi -n NetV3Server --no-https" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet new webapi -n NetV3Server --no-https" - } - } - ] - } - }, - "request_metadata": { - "request_id": "a4f06c4a-f70a-4483-8891-05a4fb8dfbbe", - "message_id": "8b913888-4a02-4e32-9b59-02531f6a3ff6", - "request_start_timestamp_ms": 1757962643695, - "stream_end_timestamp_ms": 1757962647473, - "time_to_first_chunk": { - "secs": 2, - "nanos": 292079542 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 40333 - }, - { - "secs": 0, - "nanos": 8583 - }, - { - "secs": 0, - "nanos": 901305167 - }, - { - "secs": 0, - "nanos": 44968875 - }, - { - "secs": 0, - "nanos": 92830958 - }, - { - "secs": 0, - "nanos": 86021625 - }, - { - "secs": 0, - "nanos": 72207125 - }, - { - "secs": 0, - "nanos": 102639459 - }, - { - "secs": 0, - "nanos": 122166625 - }, - { - "secs": 0, - "nanos": 1445459 - }, - { - "secs": 0, - "nanos": 2547625 - }, - { - "secs": 0, - "nanos": 18416 - }, - { - "secs": 0, - "nanos": 4235208 - }, - { - "secs": 0, - "nanos": 1623125 - }, - { - "secs": 0, - "nanos": 5031709 - }, - { - "secs": 0, - "nanos": 13625 - }, - { - "secs": 0, - "nanos": 880625 - }, - { - "secs": 0, - "nanos": 1485709 - }, - { - "secs": 0, - "nanos": 45831625 - }, - { - "secs": 0, - "nanos": 14541 - }, - { - "secs": 0, - "nanos": 5917 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 4833 - }, - { - "secs": 0, - "nanos": 10666 - }, - { - "secs": 0, - "nanos": 4167 - }, - { - "secs": 0, - "nanos": 8042 - }, - { - "secs": 0, - "nanos": 6250 - }, - { - "secs": 0, - "nanos": 34875 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 0, - "response_size": 186, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_psTtPl0pSXW4OptoieHkZQ", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_psTtPl0pSXW4OptoieHkZQ", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "The template \"ASP.NET Core Web API\" was created successfully.\n\nProcessing post-creation actions...\nRestoring /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server/NetV3Server.csproj:\n Determining projects to restore...\n Restored /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server/NetV3Server.csproj (in 2.26 sec).\nRestore succeeded.\n\n", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "63dd9120-5182-4430-a78b-dee7c07e7405", - "content": "", - "tool_uses": [ - { - "id": "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mv src/NetV3Server/NetV3Server/* src/NetV3Server/ && rmdir src/NetV3Server/NetV3Server" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mv src/NetV3Server/NetV3Server/* src/NetV3Server/ && rmdir src/NetV3Server/NetV3Server" - } - } - ] - } - }, - "request_metadata": { - "request_id": "30e1ad40-279c-44ea-9f8d-f3eba94dc26f", - "message_id": "63dd9120-5182-4430-a78b-dee7c07e7405", - "request_start_timestamp_ms": 1757966019104, - "stream_end_timestamp_ms": 1757966025345, - "time_to_first_chunk": { - "secs": 4, - "nanos": 828176417 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 121208 - }, - { - "secs": 0, - "nanos": 18875 - }, - { - "secs": 1, - "nanos": 24963875 - }, - { - "secs": 0, - "nanos": 60526917 - }, - { - "secs": 0, - "nanos": 41836000 - }, - { - "secs": 0, - "nanos": 103713666 - }, - { - "secs": 0, - "nanos": 83328958 - }, - { - "secs": 0, - "nanos": 30372000 - }, - { - "secs": 0, - "nanos": 8000 - }, - { - "secs": 0, - "nanos": 585500 - }, - { - "secs": 0, - "nanos": 478500 - }, - { - "secs": 0, - "nanos": 472334 - }, - { - "secs": 0, - "nanos": 751625 - }, - { - "secs": 0, - "nanos": 1519375 - }, - { - "secs": 0, - "nanos": 3958 - }, - { - "secs": 0, - "nanos": 781750 - }, - { - "secs": 0, - "nanos": 31811208 - }, - { - "secs": 0, - "nanos": 7125 - }, - { - "secs": 0, - "nanos": 2708 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 10334 - }, - { - "secs": 0, - "nanos": 4584 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 30589917 - }, - { - "secs": 0, - "nanos": 19791 - }, - { - "secs": 0, - "nanos": 10541 - }, - { - "secs": 0, - "nanos": 6333 - }, - { - "secs": 0, - "nanos": 3208 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 28500 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 0, - "response_size": 213, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_eqHclqEbTs6EPDD4ZZuiiQ", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "d1a5e9e6-1143-47db-82a7-d10c731a9c76", - "content": "", - "tool_uses": [ - { - "id": "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server/Controllers src/NetV3Server/Models src/NetV3Server/Services" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p src/NetV3Server/Controllers src/NetV3Server/Models src/NetV3Server/Services" - } - } - ] - } - }, - "request_metadata": { - "request_id": "b029c787-3276-4f33-87d3-84e69d1a6d71", - "message_id": "d1a5e9e6-1143-47db-82a7-d10c731a9c76", - "request_start_timestamp_ms": 1757972355190, - "stream_end_timestamp_ms": 1757972361232, - "time_to_first_chunk": { - "secs": 4, - "nanos": 622618875 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 19417 - }, - { - "secs": 0, - "nanos": 6792 - }, - { - "secs": 0, - "nanos": 872446750 - }, - { - "secs": 0, - "nanos": 84310459 - }, - { - "secs": 0, - "nanos": 9794000 - }, - { - "secs": 0, - "nanos": 62261458 - }, - { - "secs": 0, - "nanos": 13407208 - }, - { - "secs": 0, - "nanos": 23658375 - }, - { - "secs": 0, - "nanos": 43075208 - }, - { - "secs": 0, - "nanos": 25519875 - }, - { - "secs": 0, - "nanos": 52962417 - }, - { - "secs": 0, - "nanos": 57737250 - }, - { - "secs": 0, - "nanos": 79478000 - }, - { - "secs": 0, - "nanos": 30226333 - }, - { - "secs": 0, - "nanos": 1584166 - }, - { - "secs": 0, - "nanos": 7583 - }, - { - "secs": 0, - "nanos": 3083 - }, - { - "secs": 0, - "nanos": 1867917 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 252375 - }, - { - "secs": 0, - "nanos": 837375 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 29489584 - }, - { - "secs": 0, - "nanos": 16917 - }, - { - "secs": 0, - "nanos": 4333 - }, - { - "secs": 0, - "nanos": 10708 - }, - { - "secs": 0, - "nanos": 3917 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 4209 - }, - { - "secs": 0, - "nanos": 4000 - }, - { - "secs": 0, - "nanos": 4083 - }, - { - "secs": 0, - "nanos": 7125 - }, - { - "secs": 0, - "nanos": 3541 - }, - { - "secs": 0, - "nanos": 8083 - }, - { - "secs": 0, - "nanos": 29809958 - }, - { - "secs": 0, - "nanos": 21083 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 0, - "response_size": 211, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_7Bw1zGM8Ri-QC1fzuITkLA", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "81042895-d7b6-4b3b-8ca1-8456eb65266a", - "content": "", - "tool_uses": [ - { - "id": "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p tests/NetV3Server.Tests" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && mkdir -p tests/NetV3Server.Tests" - } - } - ] - } - }, - "request_metadata": { - "request_id": "954c77cc-f8a7-4650-988c-6655a8f11878", - "message_id": "81042895-d7b6-4b3b-8ca1-8456eb65266a", - "request_start_timestamp_ms": 1757972406411, - "stream_end_timestamp_ms": 1757972409050, - "time_to_first_chunk": { - "secs": 1, - "nanos": 944051042 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 61333 - }, - { - "secs": 0, - "nanos": 10875 - }, - { - "secs": 0, - "nanos": 680713500 - }, - { - "secs": 0, - "nanos": 5917 - }, - { - "secs": 0, - "nanos": 31041 - }, - { - "secs": 0, - "nanos": 15917 - }, - { - "secs": 0, - "nanos": 1458792 - }, - { - "secs": 0, - "nanos": 1427083 - }, - { - "secs": 0, - "nanos": 10500 - }, - { - "secs": 0, - "nanos": 1980709 - }, - { - "secs": 0, - "nanos": 9875 - }, - { - "secs": 0, - "nanos": 3389750 - }, - { - "secs": 0, - "nanos": 7083 - }, - { - "secs": 0, - "nanos": 3000 - }, - { - "secs": 0, - "nanos": 3791 - }, - { - "secs": 0, - "nanos": 1389000 - }, - { - "secs": 0, - "nanos": 9375 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 4584 - }, - { - "secs": 0, - "nanos": 4209 - }, - { - "secs": 0, - "nanos": 1503209 - }, - { - "secs": 0, - "nanos": 10583 - }, - { - "secs": 0, - "nanos": 1328209 - }, - { - "secs": 0, - "nanos": 2089416 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 159, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_Bqw3A-1pS0qb8ndRNpcTDw", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "a4a9af7f-f08a-4672-b74c-f15122f3bb31", - "content": "", - "tool_uses": [ - { - "id": "tooluse_9OHP7DT3TgaTw9YzknTtsg", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/tests/NetV3Server.Tests && dotnet new xunit -n NetV3Server.Tests" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/tests/NetV3Server.Tests && dotnet new xunit -n NetV3Server.Tests" - } - } - ] - } - }, - "request_metadata": { - "request_id": "fc8fcf52-7e36-4faa-8ac5-6b353fe5eda1", - "message_id": "a4a9af7f-f08a-4672-b74c-f15122f3bb31", - "request_start_timestamp_ms": 1757972420862, - "stream_end_timestamp_ms": 1757972423723, - "time_to_first_chunk": { - "secs": 2, - "nanos": 38255208 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 49917 - }, - { - "secs": 0, - "nanos": 10667 - }, - { - "secs": 0, - "nanos": 822215000 - }, - { - "secs": 0, - "nanos": 11833 - }, - { - "secs": 0, - "nanos": 372583 - }, - { - "secs": 0, - "nanos": 6000 - }, - { - "secs": 0, - "nanos": 2792 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 2666 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 15542 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 5541 - }, - { - "secs": 0, - "nanos": 12291 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 0, - "nanos": 5166 - }, - { - "secs": 0, - "nanos": 4208 - }, - { - "secs": 0, - "nanos": 2959 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 59875 - }, - { - "secs": 0, - "nanos": 5833 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2958 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 2666 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 5041 - }, - { - "secs": 0, - "nanos": 11708 - }, - { - "secs": 0, - "nanos": 84 - } - ], - "user_prompt_length": 0, - "response_size": 188, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_9OHP7DT3TgaTw9YzknTtsg", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "CancelledToolUses": { - "prompt": "Why would you need a test?", - "tool_use_results": [ - { - "tool_use_id": "tooluse_9OHP7DT3TgaTw9YzknTtsg", - "content": [ - { - "Text": "Tool use was cancelled by the user" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": "2025-09-15T14:41:17.037813-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "d8f2f907-82bc-43ac-857a-0f94e382436a", - "content": "You're right to question that. For your immediate goal of creating a test server similar to the Python one, you **don't need tests**.\n\nThe test project was just following .NET conventions, but since you're building a test server (which itself is a testing tool), adding unit tests would be overkill and unnecessary complexity.\n\nLet me clean up and show you the minimal structure you actually need:", - "tool_uses": [ - { - "id": "tooluse_ywmS-to2Ryuo37X2iEyPog", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && rm -rf tests" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && rm -rf tests" - } - } - ] - } - }, - "request_metadata": { - "request_id": "6cb23bb0-b678-4c03-a8e9-2bd59e8a1bf4", - "message_id": "d8f2f907-82bc-43ac-857a-0f94e382436a", - "request_start_timestamp_ms": 1757972477248, - "stream_end_timestamp_ms": 1757972483194, - "time_to_first_chunk": { - "secs": 2, - "nanos": 120958417 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 12959 - }, - { - "secs": 0, - "nanos": 3417 - }, - { - "secs": 0, - "nanos": 93805625 - }, - { - "secs": 0, - "nanos": 599700791 - }, - { - "secs": 0, - "nanos": 101509584 - }, - { - "secs": 0, - "nanos": 349305791 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 68222208 - }, - { - "secs": 0, - "nanos": 156565292 - }, - { - "secs": 0, - "nanos": 185222667 - }, - { - "secs": 0, - "nanos": 238502417 - }, - { - "secs": 0, - "nanos": 201262917 - }, - { - "secs": 0, - "nanos": 117300250 - }, - { - "secs": 0, - "nanos": 105225458 - }, - { - "secs": 0, - "nanos": 56738167 - }, - { - "secs": 0, - "nanos": 179426125 - }, - { - "secs": 0, - "nanos": 99785458 - }, - { - "secs": 0, - "nanos": 224553584 - }, - { - "secs": 0, - "nanos": 172400250 - }, - { - "secs": 0, - "nanos": 122294625 - }, - { - "secs": 0, - "nanos": 154151292 - }, - { - "secs": 0, - "nanos": 568565167 - }, - { - "secs": 0, - "nanos": 1006959 - }, - { - "secs": 0, - "nanos": 6167 - }, - { - "secs": 0, - "nanos": 2573250 - }, - { - "secs": 0, - "nanos": 1787208 - }, - { - "secs": 0, - "nanos": 5102917 - }, - { - "secs": 0, - "nanos": 847958 - }, - { - "secs": 0, - "nanos": 1825666 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 1794917 - }, - { - "secs": 0, - "nanos": 3417 - }, - { - "secs": 0, - "nanos": 931834 - }, - { - "secs": 0, - "nanos": 3042 - }, - { - "secs": 0, - "nanos": 2808875 - }, - { - "secs": 0, - "nanos": 6084 - }, - { - "secs": 0, - "nanos": 1212666 - }, - { - "secs": 0, - "nanos": 4584 - }, - { - "secs": 0, - "nanos": 1206875 - }, - { - "secs": 0, - "nanos": 1932625 - }, - { - "secs": 0, - "nanos": 8916 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 5943625 - }, - { - "secs": 0, - "nanos": 1865542 - }, - { - "secs": 0, - "nanos": 0 - } - ], - "user_prompt_length": 185, - "response_size": 536, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_ywmS-to2Ryuo37X2iEyPog", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_ywmS-to2Ryuo37X2iEyPog", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "9afcfa92-43f7-4b8e-9d0a-09fad6581ffb", - "content": "", - "tool_uses": [ - { - "id": "tooluse_EGhkURTAQsycx_K8Fc_lxQ", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet sln add src/NetV3Server/NetV3Server.csproj" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server && dotnet sln add src/NetV3Server/NetV3Server.csproj" - } - } - ] - } - }, - "request_metadata": { - "request_id": "0a199baf-57d4-45ac-8f9f-bb384224dc9a", - "message_id": "9afcfa92-43f7-4b8e-9d0a-09fad6581ffb", - "request_start_timestamp_ms": 1757972495247, - "stream_end_timestamp_ms": 1757972498232, - "time_to_first_chunk": { - "secs": 1, - "nanos": 961189667 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 18792 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 728345708 - }, - { - "secs": 0, - "nanos": 10906667 - }, - { - "secs": 0, - "nanos": 16989667 - }, - { - "secs": 0, - "nanos": 9073458 - }, - { - "secs": 0, - "nanos": 5730500 - }, - { - "secs": 0, - "nanos": 3961166 - }, - { - "secs": 0, - "nanos": 5743250 - }, - { - "secs": 0, - "nanos": 27732333 - }, - { - "secs": 0, - "nanos": 5974125 - }, - { - "secs": 0, - "nanos": 5083 - }, - { - "secs": 0, - "nanos": 6904417 - }, - { - "secs": 0, - "nanos": 7417 - }, - { - "secs": 0, - "nanos": 3062458 - }, - { - "secs": 0, - "nanos": 4459 - }, - { - "secs": 0, - "nanos": 1738375 - }, - { - "secs": 0, - "nanos": 4563333 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 5560125 - }, - { - "secs": 0, - "nanos": 3875 - }, - { - "secs": 0, - "nanos": 2023458 - }, - { - "secs": 0, - "nanos": 8916 - }, - { - "secs": 0, - "nanos": 9417 - }, - { - "secs": 0, - "nanos": 3833 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 2416 - }, - { - "secs": 0, - "nanos": 4997334 - }, - { - "secs": 0, - "nanos": 178881250 - }, - { - "secs": 0, - "nanos": 858958 - }, - { - "secs": 0, - "nanos": 84 - } - ], - "user_prompt_length": 0, - "response_size": 176, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_EGhkURTAQsycx_K8Fc_lxQ", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_EGhkURTAQsycx_K8Fc_lxQ", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": "Project `src/NetV3Server/NetV3Server.csproj` added to the solution.", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "aca358a5-2732-4cc6-a222-10d9e3bc2bef", - "content": "", - "tool_uses": [ - { - "id": "tooluse_gRBhKrICTrmLbIMX9ycgDQ", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server", - "depth": 3 - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Directory", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server", - "depth": 3 - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "ab319ca4-f128-445c-be9f-e85830a5a785", - "message_id": "aca358a5-2732-4cc6-a222-10d9e3bc2bef", - "request_start_timestamp_ms": 1757972559856, - "stream_end_timestamp_ms": 1757972562721, - "time_to_first_chunk": { - "secs": 2, - "nanos": 586168250 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 18250 - }, - { - "secs": 0, - "nanos": 3959 - }, - { - "secs": 0, - "nanos": 179495292 - }, - { - "secs": 0, - "nanos": 6667 - }, - { - "secs": 0, - "nanos": 6704208 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 1265083 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 4011916 - }, - { - "secs": 0, - "nanos": 4001250 - }, - { - "secs": 0, - "nanos": 1400000 - }, - { - "secs": 0, - "nanos": 2945208 - }, - { - "secs": 0, - "nanos": 6334 - }, - { - "secs": 0, - "nanos": 1335333 - }, - { - "secs": 0, - "nanos": 2159708 - }, - { - "secs": 0, - "nanos": 735875 - }, - { - "secs": 0, - "nanos": 6416 - }, - { - "secs": 0, - "nanos": 2649167 - }, - { - "secs": 0, - "nanos": 2590166 - }, - { - "secs": 0, - "nanos": 574250 - }, - { - "secs": 0, - "nanos": 6375 - }, - { - "secs": 0, - "nanos": 1127750 - }, - { - "secs": 0, - "nanos": 5586708 - }, - { - "secs": 0, - "nanos": 3781459 - }, - { - "secs": 0, - "nanos": 5579917 - }, - { - "secs": 0, - "nanos": 5114333 - }, - { - "secs": 0, - "nanos": 10913167 - }, - { - "secs": 0, - "nanos": 1972875 - }, - { - "secs": 0, - "nanos": 33417291 - }, - { - "secs": 0, - "nanos": 1440208 - }, - { - "secs": 0, - "nanos": 41 - } - ], - "user_prompt_length": 0, - "response_size": 163, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_gRBhKrICTrmLbIMX9ycgDQ", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_gRBhKrICTrmLbIMX9ycgDQ", - "content": [ - { - "Text": "drwxr-xr-x 3 504 20 96 Sep 15 18:45 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp\n-rw-r--r-- 1 504 20 5290 Sep 15 18:52 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/README.md\n-rw-r--r-- 1 504 20 1281 Sep 15 21:42 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/NetV3Server.sln\ndrwxr-xr-x 3 504 20 96 Sep 15 18:57 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src\n-rw-r--r-- 1 504 20 523057 Sep 15 18:24 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json\ndrwxr-xr-x 13 504 20 416 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server\n-rw-r--r-- 1 504 20 409 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj\ndrwxr-xr-x 8 504 20 256 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/bin\n-rw-r--r-- 1 504 20 151 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models\ndrwxr-xr-x 3 504 20 96 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Properties\n-rw-r--r-- 1 504 20 135 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.http\n-rw-r--r-- 1 504 20 127 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.Development.json\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers\ndrwxr-xr-x 2 504 20 64 Sep 15 21:40 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services\n-rw-r--r-- 1 504 20 1142 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs\n-rw-r--r-- 1 504 20 2068 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.props\n-rw-r--r-- 1 504 20 546 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.targets\n-rw-r--r-- 1 504 20 1166 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.nuget.cache\n-rw-r--r-- 1 504 20 2742 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.dgspec.json\n-rw-r--r-- 1 504 20 24901 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.assets.json\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/Debug\ndrwxr-xr-x 3 504 20 96 Sep 15 19:56 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/bin/Debug\n-rw-r--r-- 1 504 20 805 Sep 15 19:53 /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Properties/launchSettings.json" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "1068d872-9892-43fe-9899-7e11ad61d5f5", - "content": "Perfect! Now you have a clean, minimal .NET project structure:\n\n```\nnet-v3-server/\n├── NetV3Server.sln # Solution file\n├── src/NetV3Server/\n│ ├── Controllers/ # Your API endpoints (empty, ready for you)\n│ ├── Models/ # Request/response models (empty, ready for you) \n│ ├── Services/ # Business logic like ClientCacheService (empty, ready for you)\n│ ├── Program.cs # Entry point (like main.py)\n│ ├── NetV3Server.csproj # Project dependencies\n│ └── appsettings.json # Configuration\n└── README.md\n```\n\nYou're right - no tests needed. This test server **is** the test. Just like the Python version, it's a tool for testing the S3 encryption client, not something that needs its own unit tests.\n\nNow you can start implementing the controllers and services to mirror the Python FastAPI functionality." - } - }, - "request_metadata": { - "request_id": "bee48976-afdb-4f81-b92d-07cf9a8a1058", - "message_id": "1068d872-9892-43fe-9899-7e11ad61d5f5", - "request_start_timestamp_ms": 1757972563209, - "stream_end_timestamp_ms": 1757972571769, - "time_to_first_chunk": { - "secs": 4, - "nanos": 704986208 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 15584 - }, - { - "secs": 0, - "nanos": 107281834 - }, - { - "secs": 0, - "nanos": 93954500 - }, - { - "secs": 0, - "nanos": 113215083 - }, - { - "secs": 0, - "nanos": 11278584 - }, - { - "secs": 0, - "nanos": 1788667 - }, - { - "secs": 0, - "nanos": 4387042 - }, - { - "secs": 0, - "nanos": 1229666 - }, - { - "secs": 0, - "nanos": 2290958 - }, - { - "secs": 0, - "nanos": 3882125 - }, - { - "secs": 0, - "nanos": 3947125 - }, - { - "secs": 0, - "nanos": 3914834 - }, - { - "secs": 0, - "nanos": 2141208 - }, - { - "secs": 0, - "nanos": 2169250 - }, - { - "secs": 0, - "nanos": 1919292 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 1358208 - }, - { - "secs": 0, - "nanos": 2065250 - }, - { - "secs": 0, - "nanos": 1791083 - }, - { - "secs": 0, - "nanos": 2102541 - }, - { - "secs": 0, - "nanos": 1972250 - }, - { - "secs": 0, - "nanos": 17958 - }, - { - "secs": 0, - "nanos": 9583 - }, - { - "secs": 0, - "nanos": 17083 - }, - { - "secs": 0, - "nanos": 17625 - }, - { - "secs": 0, - "nanos": 17416 - }, - { - "secs": 0, - "nanos": 24917 - }, - { - "secs": 0, - "nanos": 20791 - }, - { - "secs": 0, - "nanos": 17125 - }, - { - "secs": 0, - "nanos": 234418958 - }, - { - "secs": 0, - "nanos": 6417 - }, - { - "secs": 0, - "nanos": 16750 - }, - { - "secs": 0, - "nanos": 17208 - }, - { - "secs": 0, - "nanos": 6000 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 667 - } - ], - "user_prompt_length": 0, - "response_size": 964, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "What is this bin, obj, properties, appsetting.json, appsettings.Development.json, other .csproj and http files?" - } - }, - "timestamp": "2025-09-15T14:45:08.153822-07:00", - "images": null - }, - "assistant": { - "Response": { - "message_id": "4bb7d21d-1a97-404d-92de-42f5fe1fcd9d", - "content": "These are standard .NET project files - here's what each does:\n\n## Files You'll Work With:\n- **Program.cs** - Your main entry point (like main.py)\n- **NetV3Server.csproj** - Project file listing dependencies (like pyproject.toml)\n- **appsettings.json** - Configuration (ports, connection strings, etc.)\n\n## Files You Can Ignore:\n- **bin/** - Compiled output (like Python's __pycache__)\n- **obj/** - Build artifacts and temporary files\n- **Properties/launchSettings.json** - IDE debugging settings\n- **appsettings.Development.json** - Dev-only config overrides\n- **NetV3Server.http** - Sample HTTP requests for testing\n\n## What to Focus On:\n1. **Program.cs** - Configure your web server and routes\n2. **Controllers/** - Your API endpoints (`/client`, `/object/{bucket}/{key}`)\n3. **Models/** - Request/response classes\n4. **Services/** - Business logic (client cache)\n5. **NetV3Server.csproj** - Add AWS SDK packages here\n\nThe `bin` and `obj` folders are like Python's virtual environment artifacts - generated during build, safe to delete, and should be in `.gitignore`.\n\nThink of it this way:\n- **bin/obj** = Python's `.venv` and `__pycache__`\n- **appsettings.json** = Your config file\n- **Program.cs** = Your `main.py`\n- **.csproj** = Your `pyproject.toml`\n\nYou only need to touch the first 5 items I mentioned." - } - }, - "request_metadata": { - "request_id": "dcc33812-f48c-4607-95fe-ee75aee4d2f2", - "message_id": "4bb7d21d-1a97-404d-92de-42f5fe1fcd9d", - "request_start_timestamp_ms": 1757972708157, - "stream_end_timestamp_ms": 1757972719336, - "time_to_first_chunk": { - "secs": 2, - "nanos": 551164542 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 39291 - }, - { - "secs": 0, - "nanos": 6875 - }, - { - "secs": 0, - "nanos": 281801416 - }, - { - "secs": 0, - "nanos": 226044750 - }, - { - "secs": 0, - "nanos": 363376667 - }, - { - "secs": 0, - "nanos": 152234000 - }, - { - "secs": 0, - "nanos": 100946250 - }, - { - "secs": 0, - "nanos": 169830042 - }, - { - "secs": 0, - "nanos": 111369625 - }, - { - "secs": 0, - "nanos": 57304291 - }, - { - "secs": 0, - "nanos": 278657625 - }, - { - "secs": 0, - "nanos": 117190541 - }, - { - "secs": 0, - "nanos": 371903875 - }, - { - "secs": 0, - "nanos": 62885458 - }, - { - "secs": 0, - "nanos": 161187500 - }, - { - "secs": 0, - "nanos": 106972125 - }, - { - "secs": 0, - "nanos": 310951417 - }, - { - "secs": 0, - "nanos": 165540500 - }, - { - "secs": 0, - "nanos": 226349458 - }, - { - "secs": 0, - "nanos": 62893459 - }, - { - "secs": 0, - "nanos": 234520750 - }, - { - "secs": 0, - "nanos": 210761166 - }, - { - "secs": 0, - "nanos": 105610125 - }, - { - "secs": 0, - "nanos": 218673125 - }, - { - "secs": 0, - "nanos": 64097708 - }, - { - "secs": 0, - "nanos": 138821958 - }, - { - "secs": 0, - "nanos": 168110667 - }, - { - "secs": 0, - "nanos": 336051416 - }, - { - "secs": 0, - "nanos": 119190250 - }, - { - "secs": 0, - "nanos": 263103875 - }, - { - "secs": 0, - "nanos": 52826333 - }, - { - "secs": 0, - "nanos": 341796667 - }, - { - "secs": 0, - "nanos": 328502333 - }, - { - "secs": 0, - "nanos": 278887042 - }, - { - "secs": 0, - "nanos": 170325333 - }, - { - "secs": 0, - "nanos": 171507792 - }, - { - "secs": 0, - "nanos": 54421916 - }, - { - "secs": 0, - "nanos": 216826500 - }, - { - "secs": 0, - "nanos": 253941708 - }, - { - "secs": 0, - "nanos": 373136500 - }, - { - "secs": 0, - "nanos": 79289875 - }, - { - "secs": 0, - "nanos": 111929625 - }, - { - "secs": 0, - "nanos": 111010875 - }, - { - "secs": 0, - "nanos": 180891916 - }, - { - "secs": 0, - "nanos": 44498042 - }, - { - "secs": 0, - "nanos": 253745875 - }, - { - "secs": 0, - "nanos": 150561792 - }, - { - "secs": 0, - "nanos": 260028084 - }, - { - "secs": 0, - "nanos": 5878625 - }, - { - "secs": 0, - "nanos": 209 - }, - { - "secs": 0, - "nanos": 208 - } - ], - "user_prompt_length": 269, - "response_size": 1313, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "So, what is the next steps?" - } - }, - "timestamp": "2025-09-15T14:53:23.896032-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "87908723-d8de-4e89-9d89-f192b16b330f", - "content": "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nAdd the S3 encryption client and AWS SDK packages to your project:", - "tool_uses": [ - { - "id": "tooluse_qA7SLDIgT7uQDHHjHtuWeg", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package AWSSDK.S3 && dotnet add package AWSSDK.KeyManagementService" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package AWSSDK.S3 && dotnet add package AWSSDK.KeyManagementService" - } - } - ] - } - }, - "request_metadata": { - "request_id": "3df8ed85-997c-4e54-9d49-266bedfd419a", - "message_id": "87908723-d8de-4e89-9d89-f192b16b330f", - "request_start_timestamp_ms": 1757973203899, - "stream_end_timestamp_ms": 1757973210961, - "time_to_first_chunk": { - "secs": 4, - "nanos": 554073042 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 46375 - }, - { - "secs": 0, - "nanos": 11875 - }, - { - "secs": 0, - "nanos": 387612916 - }, - { - "secs": 0, - "nanos": 306701125 - }, - { - "secs": 0, - "nanos": 67498542 - }, - { - "secs": 0, - "nanos": 241621459 - }, - { - "secs": 1, - "nanos": 101653625 - }, - { - "secs": 0, - "nanos": 100018542 - }, - { - "secs": 0, - "nanos": 72277250 - }, - { - "secs": 0, - "nanos": 95738792 - }, - { - "secs": 0, - "nanos": 68908917 - }, - { - "secs": 0, - "nanos": 1585250 - }, - { - "secs": 0, - "nanos": 14833 - }, - { - "secs": 0, - "nanos": 1207000 - }, - { - "secs": 0, - "nanos": 2098292 - }, - { - "secs": 0, - "nanos": 3440000 - }, - { - "secs": 0, - "nanos": 13750 - }, - { - "secs": 0, - "nanos": 2200792 - }, - { - "secs": 0, - "nanos": 1829750 - }, - { - "secs": 0, - "nanos": 2034250 - }, - { - "secs": 0, - "nanos": 49088750 - }, - { - "secs": 0, - "nanos": 7833 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 873000 - }, - { - "secs": 0, - "nanos": 3833 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 2002917 - }, - { - "secs": 0, - "nanos": 6083 - }, - { - "secs": 0, - "nanos": 2333 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 7291 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 185, - "response_size": 350, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_qA7SLDIgT7uQDHHjHtuWeg", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "CancelledToolUses": { - "prompt": "Can you list them first?", - "tool_use_results": [ - { - "tool_use_id": "tooluse_qA7SLDIgT7uQDHHjHtuWeg", - "content": [ - { - "Text": "Tool use was cancelled by the user" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": "2025-09-15T14:53:57.015219-07:00", - "images": null - }, - "assistant": { - "Response": { - "message_id": "9a107d49-e417-4d62-8eda-b8bc021f296e", - "content": "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nYou'll need these NuGet packages:\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK \n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n\n## 2. Configure the Server\n- Set port to 8081 in `appsettings.json` (to match Python server)\n- Configure dependency injection in `Program.cs`\n\n## 3. Create Models\n- `ClientRequest.cs` - For POST /client requests\n- `ClientResponse.cs` - For client creation responses\n- `ErrorModels.cs` - For error responses\n\n## 4. Create Services\n- `ClientCacheService.cs` - In-memory client storage (like Python's client_cache dict)\n\n## 5. Create Controllers\n- `ClientController.cs` - Handle POST /client\n- `ObjectController.cs` - Handle PUT/GET /object/{bucket}/{key}\n\n## 6. Test\n- Run the server and verify it starts on port 8081\n- Test endpoints match Python server behavior\n\nWant me to help with step 1 (adding the packages) first?" - } - }, - "request_metadata": { - "request_id": "d559997f-8e52-4b64-82a5-63fe40203a81", - "message_id": "9a107d49-e417-4d62-8eda-b8bc021f296e", - "request_start_timestamp_ms": 1757973237376, - "stream_end_timestamp_ms": 1757973245216, - "time_to_first_chunk": { - "secs": 2, - "nanos": 254640791 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 14833 - }, - { - "secs": 0, - "nanos": 3834 - }, - { - "secs": 0, - "nanos": 106632625 - }, - { - "secs": 0, - "nanos": 242511792 - }, - { - "secs": 0, - "nanos": 215528250 - }, - { - "secs": 0, - "nanos": 161817834 - }, - { - "secs": 0, - "nanos": 115514917 - }, - { - "secs": 0, - "nanos": 200784000 - }, - { - "secs": 0, - "nanos": 154354208 - }, - { - "secs": 0, - "nanos": 169257625 - }, - { - "secs": 0, - "nanos": 113756916 - }, - { - "secs": 0, - "nanos": 438160166 - }, - { - "secs": 0, - "nanos": 116102000 - }, - { - "secs": 0, - "nanos": 196862500 - }, - { - "secs": 0, - "nanos": 223619333 - }, - { - "secs": 0, - "nanos": 193304625 - }, - { - "secs": 0, - "nanos": 318715333 - }, - { - "secs": 0, - "nanos": 255427959 - }, - { - "secs": 0, - "nanos": 200595792 - }, - { - "secs": 0, - "nanos": 54208625 - }, - { - "secs": 0, - "nanos": 72667375 - }, - { - "secs": 0, - "nanos": 210610041 - }, - { - "secs": 0, - "nanos": 330727541 - }, - { - "secs": 0, - "nanos": 759643041 - }, - { - "secs": 0, - "nanos": 38890667 - }, - { - "secs": 0, - "nanos": 21869625 - }, - { - "secs": 0, - "nanos": 14905333 - }, - { - "secs": 0, - "nanos": 7768333 - }, - { - "secs": 0, - "nanos": 20798167 - }, - { - "secs": 0, - "nanos": 185438083 - }, - { - "secs": 0, - "nanos": 336475458 - }, - { - "secs": 0, - "nanos": 94192084 - }, - { - "secs": 0, - "nanos": 10025166 - }, - { - "secs": 0, - "nanos": 83 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 182, - "response_size": 957, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Before doing this can I have a gitignore? Where should I put this?" - } - }, - "timestamp": "2025-09-15T14:55:46.471426-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "9ac83ad7-9f4b-4810-a2fc-bc98929ce92a", - "content": "You should put the `.gitignore` file in the root of your `net-v3-server` directory (same level as the `.sln` file).", - "tool_uses": [ - { - "id": "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "command": "create", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", - "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" - }, - "orig_args": { - "command": "create", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", - "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" - } - } - ] - } - }, - "request_metadata": { - "request_id": "0116edd0-30b4-4ffd-81ec-5dedd50a1e4a", - "message_id": "9ac83ad7-9f4b-4810-a2fc-bc98929ce92a", - "request_start_timestamp_ms": 1757973346476, - "stream_end_timestamp_ms": 1757973354602, - "time_to_first_chunk": { - "secs": 2, - "nanos": 615486542 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 14375 - }, - { - "secs": 0, - "nanos": 7750 - }, - { - "secs": 0, - "nanos": 396886042 - }, - { - "secs": 0, - "nanos": 245263417 - }, - { - "secs": 0, - "nanos": 306512709 - }, - { - "secs": 0, - "nanos": 175293292 - }, - { - "secs": 0, - "nanos": 644961208 - }, - { - "secs": 0, - "nanos": 521756334 - }, - { - "secs": 0, - "nanos": 3542 - }, - { - "secs": 0, - "nanos": 13166 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 467408959 - }, - { - "secs": 0, - "nanos": 34375 - }, - { - "secs": 0, - "nanos": 7625 - }, - { - "secs": 0, - "nanos": 3015917 - }, - { - "secs": 0, - "nanos": 12302791 - }, - { - "secs": 0, - "nanos": 45292 - }, - { - "secs": 0, - "nanos": 1234666 - }, - { - "secs": 0, - "nanos": 24458 - }, - { - "secs": 0, - "nanos": 949833 - }, - { - "secs": 0, - "nanos": 22834 - }, - { - "secs": 0, - "nanos": 49575167 - }, - { - "secs": 0, - "nanos": 34209 - }, - { - "secs": 0, - "nanos": 12333 - }, - { - "secs": 0, - "nanos": 10084 - }, - { - "secs": 0, - "nanos": 10167 - }, - { - "secs": 0, - "nanos": 9250 - }, - { - "secs": 0, - "nanos": 110500 - }, - { - "secs": 0, - "nanos": 106833 - }, - { - "secs": 0, - "nanos": 6375 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 6041 - }, - { - "secs": 2, - "nanos": 613837709 - }, - { - "secs": 0, - "nanos": 1257583 - }, - { - "secs": 0, - "nanos": 1144584 - }, - { - "secs": 0, - "nanos": 6083 - }, - { - "secs": 0, - "nanos": 249500 - }, - { - "secs": 0, - "nanos": 2163042 - }, - { - "secs": 0, - "nanos": 6791 - }, - { - "secs": 0, - "nanos": 5571917 - }, - { - "secs": 0, - "nanos": 8250 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 399666 - }, - { - "secs": 0, - "nanos": 5875 - }, - { - "secs": 0, - "nanos": 1362000 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 587834 - }, - { - "secs": 0, - "nanos": 1689041 - }, - { - "secs": 0, - "nanos": 4542 - }, - { - "secs": 0, - "nanos": 2255416 - }, - { - "secs": 0, - "nanos": 48497250 - }, - { - "secs": 0, - "nanos": 24833 - }, - { - "secs": 0, - "nanos": 6125 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 548291 - }, - { - "secs": 0, - "nanos": 83833 - }, - { - "secs": 0, - "nanos": 9000 - }, - { - "secs": 0, - "nanos": 15375 - }, - { - "secs": 0, - "nanos": 15000 - }, - { - "secs": 0, - "nanos": 6584 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 421959 - }, - { - "secs": 0, - "nanos": 6166 - }, - { - "secs": 0, - "nanos": 56125 - }, - { - "secs": 0, - "nanos": 6459 - }, - { - "secs": 0, - "nanos": 5584 - }, - { - "secs": 0, - "nanos": 6375 - }, - { - "secs": 0, - "nanos": 2640250 - }, - { - "secs": 0, - "nanos": 15333 - }, - { - "secs": 0, - "nanos": 6500 - }, - { - "secs": 0, - "nanos": 5666 - }, - { - "secs": 0, - "nanos": 5666 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 5166 - }, - { - "secs": 0, - "nanos": 6750 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 4833 - }, - { - "secs": 0, - "nanos": 5708 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 4958 - }, - { - "secs": 0, - "nanos": 4958 - }, - { - "secs": 0, - "nanos": 4834 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 5041 - }, - { - "secs": 0, - "nanos": 4792 - }, - { - "secs": 0, - "nanos": 235917 - }, - { - "secs": 0, - "nanos": 23083 - }, - { - "secs": 0, - "nanos": 7209 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 11334 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 36500 - }, - { - "secs": 0, - "nanos": 19708 - }, - { - "secs": 0, - "nanos": 10416 - }, - { - "secs": 0, - "nanos": 5875 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 15333 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 224, - "response_size": 768, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "CancelledToolUses": { - "prompt": "Question: What is difference between this and gitignore in the root?", - "tool_use_results": [ - { - "tool_use_id": "tooluse_sgz-BS_KQ_2CwCRdYkOFUg", - "content": [ - { - "Text": "Tool use was cancelled by the user" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": "2025-09-15T14:56:38.144732-07:00", - "images": null - }, - "assistant": { - "Response": { - "message_id": "7b0357b9-12a2-41e0-887e-c50f7991c3fc", - "content": "Good question! Here's the difference:\n\n## Root `.gitignore` (python repo level)\n- Located at `/amazon-s3-encryption-client-python/.gitignore`\n- Ignores files for the **entire Python repository**\n- Already exists and handles Python-specific files (`.pyenv`, `__pycache__`, etc.)\n\n## Net server `.gitignore` (net-v3-server level) \n- Would be at `/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore`\n- Ignores files **only for the .NET project**\n- Handles .NET-specific files (`bin/`, `obj/`, `.vs/`, etc.)\n\n## Why You Need Both:\n- Root ignores Python stuff globally\n- Net server ignores .NET stuff locally\n- Git processes `.gitignore` files hierarchically - it reads both\n\n## Alternative:\nYou could add .NET ignore patterns to the root `.gitignore` instead:\n```\n# Add to root .gitignore\ntest-server/net-v3-server/bin/\ntest-server/net-v3-server/obj/\ntest-server/net-v3-server/.vs/\n```\n\n**Recommendation:** Create a local `.gitignore` in `net-v3-server/` - it's cleaner and keeps .NET concerns separate from Python concerns." - } - }, - "request_metadata": { - "request_id": "64bf9e0f-fcf6-4134-9abe-76c9ebd91173", - "message_id": "7b0357b9-12a2-41e0-887e-c50f7991c3fc", - "request_start_timestamp_ms": 1757973398447, - "stream_end_timestamp_ms": 1757973416538, - "time_to_first_chunk": { - "secs": 11, - "nanos": 995218500 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 55334 - }, - { - "secs": 0, - "nanos": 29875 - }, - { - "secs": 0, - "nanos": 131826792 - }, - { - "secs": 0, - "nanos": 288688833 - }, - { - "secs": 0, - "nanos": 276512500 - }, - { - "secs": 0, - "nanos": 54733375 - }, - { - "secs": 0, - "nanos": 113804417 - }, - { - "secs": 0, - "nanos": 283913708 - }, - { - "secs": 0, - "nanos": 92647250 - }, - { - "secs": 0, - "nanos": 307415917 - }, - { - "secs": 0, - "nanos": 830049750 - }, - { - "secs": 0, - "nanos": 6912750 - }, - { - "secs": 0, - "nanos": 19802875 - }, - { - "secs": 0, - "nanos": 5754459 - }, - { - "secs": 0, - "nanos": 30504542 - }, - { - "secs": 0, - "nanos": 47366084 - }, - { - "secs": 0, - "nanos": 169927250 - }, - { - "secs": 0, - "nanos": 37373000 - }, - { - "secs": 0, - "nanos": 183769292 - }, - { - "secs": 0, - "nanos": 110210792 - }, - { - "secs": 0, - "nanos": 16200417 - }, - { - "secs": 0, - "nanos": 119389000 - }, - { - "secs": 0, - "nanos": 169745041 - }, - { - "secs": 0, - "nanos": 170980750 - }, - { - "secs": 0, - "nanos": 329810542 - }, - { - "secs": 0, - "nanos": 96640667 - }, - { - "secs": 0, - "nanos": 506309917 - }, - { - "secs": 0, - "nanos": 109476416 - }, - { - "secs": 0, - "nanos": 177202208 - }, - { - "secs": 0, - "nanos": 158928500 - }, - { - "secs": 0, - "nanos": 173579292 - }, - { - "secs": 0, - "nanos": 52149042 - }, - { - "secs": 0, - "nanos": 54382167 - }, - { - "secs": 0, - "nanos": 166240792 - }, - { - "secs": 0, - "nanos": 798802875 - }, - { - "secs": 0, - "nanos": 8083 - }, - { - "secs": 0, - "nanos": 1430583 - }, - { - "secs": 0, - "nanos": 3917 - }, - { - "secs": 0, - "nanos": 2254875 - }, - { - "secs": 0, - "nanos": 41 - }, - { - "secs": 0, - "nanos": 0 - } - ], - "user_prompt_length": 226, - "response_size": 1042, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Sure. Let's create gitignore in net-v3-server" - } - }, - "timestamp": "2025-09-15T14:57:24.132611-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "7fba195a-2a3f-45bf-ba4e-6c923a4d0362", - "content": "", - "tool_uses": [ - { - "id": "tooluse_5IYUis90Sq-TiqFl_6Utbw", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", - "command": "create", - "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore", - "command": "create", - "file_text": "# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# NuGet Packages\n*.nupkg\n*.snupkg\npackages/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# VS Code\n.vscode/\n\n# macOS\n.DS_Store\n\n# Temporary files\n*.tmp\n*.temp" - } - } - ] - } - }, - "request_metadata": { - "request_id": "695e022a-92e5-4643-a180-76bb12f9352b", - "message_id": "7fba195a-2a3f-45bf-ba4e-6c923a4d0362", - "request_start_timestamp_ms": 1757973444139, - "stream_end_timestamp_ms": 1757973448757, - "time_to_first_chunk": { - "secs": 2, - "nanos": 286244541 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 24750 - }, - { - "secs": 0, - "nanos": 6791 - }, - { - "secs": 0, - "nanos": 469352583 - }, - { - "secs": 0, - "nanos": 14292 - }, - { - "secs": 0, - "nanos": 88425333 - }, - { - "secs": 0, - "nanos": 20918541 - }, - { - "secs": 0, - "nanos": 53751208 - }, - { - "secs": 0, - "nanos": 12485792 - }, - { - "secs": 0, - "nanos": 940208 - }, - { - "secs": 0, - "nanos": 8081833 - }, - { - "secs": 0, - "nanos": 1722333 - }, - { - "secs": 0, - "nanos": 2647417 - }, - { - "secs": 0, - "nanos": 1242917 - }, - { - "secs": 0, - "nanos": 10784792 - }, - { - "secs": 0, - "nanos": 10250 - }, - { - "secs": 0, - "nanos": 852167 - }, - { - "secs": 0, - "nanos": 4220500 - }, - { - "secs": 0, - "nanos": 1970042 - }, - { - "secs": 0, - "nanos": 10881667 - }, - { - "secs": 0, - "nanos": 25250 - }, - { - "secs": 0, - "nanos": 11542 - }, - { - "secs": 0, - "nanos": 9834 - }, - { - "secs": 0, - "nanos": 2134958 - }, - { - "secs": 1, - "nanos": 573889917 - }, - { - "secs": 0, - "nanos": 14459 - }, - { - "secs": 0, - "nanos": 695459 - }, - { - "secs": 0, - "nanos": 56291 - }, - { - "secs": 0, - "nanos": 1851208 - }, - { - "secs": 0, - "nanos": 3820625 - }, - { - "secs": 0, - "nanos": 20417 - }, - { - "secs": 0, - "nanos": 1573458 - }, - { - "secs": 0, - "nanos": 6542 - }, - { - "secs": 0, - "nanos": 1630209 - }, - { - "secs": 0, - "nanos": 7084 - }, - { - "secs": 0, - "nanos": 1313417 - }, - { - "secs": 0, - "nanos": 357667 - }, - { - "secs": 0, - "nanos": 6208 - }, - { - "secs": 0, - "nanos": 1068916 - }, - { - "secs": 0, - "nanos": 6666 - }, - { - "secs": 0, - "nanos": 806875 - }, - { - "secs": 0, - "nanos": 8208 - }, - { - "secs": 0, - "nanos": 1235833 - }, - { - "secs": 0, - "nanos": 1089083 - }, - { - "secs": 0, - "nanos": 6542 - }, - { - "secs": 0, - "nanos": 453209 - }, - { - "secs": 0, - "nanos": 1254208 - }, - { - "secs": 0, - "nanos": 6208 - }, - { - "secs": 0, - "nanos": 696834 - }, - { - "secs": 0, - "nanos": 6917 - }, - { - "secs": 0, - "nanos": 345417 - }, - { - "secs": 0, - "nanos": 5709 - }, - { - "secs": 0, - "nanos": 1914542 - }, - { - "secs": 0, - "nanos": 3387042 - }, - { - "secs": 0, - "nanos": 520083 - }, - { - "secs": 0, - "nanos": 1355292 - }, - { - "secs": 0, - "nanos": 12083 - }, - { - "secs": 0, - "nanos": 826459 - }, - { - "secs": 0, - "nanos": 7667 - }, - { - "secs": 0, - "nanos": 1044291 - }, - { - "secs": 0, - "nanos": 21042 - }, - { - "secs": 0, - "nanos": 535834 - }, - { - "secs": 0, - "nanos": 1068791 - }, - { - "secs": 0, - "nanos": 25750 - }, - { - "secs": 0, - "nanos": 929667 - }, - { - "secs": 0, - "nanos": 16584 - }, - { - "secs": 0, - "nanos": 604792 - }, - { - "secs": 0, - "nanos": 25000 - }, - { - "secs": 0, - "nanos": 35824166 - }, - { - "secs": 0, - "nanos": 580833 - }, - { - "secs": 0, - "nanos": 14458 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 3166 - }, - { - "secs": 0, - "nanos": 4542 - }, - { - "secs": 0, - "nanos": 3667 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 2958 - }, - { - "secs": 0, - "nanos": 6791 - }, - { - "secs": 0, - "nanos": 11375 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 6125 - }, - { - "secs": 0, - "nanos": 2917 - }, - { - "secs": 0, - "nanos": 5583 - }, - { - "secs": 0, - "nanos": 3041 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 3416 - }, - { - "secs": 0, - "nanos": 2792 - }, - { - "secs": 0, - "nanos": 3333 - }, - { - "secs": 0, - "nanos": 8333 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 2542 - }, - { - "secs": 0, - "nanos": 4167 - }, - { - "secs": 0, - "nanos": 2875 - }, - { - "secs": 0, - "nanos": 4084 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 3375 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 2291 - }, - { - "secs": 0, - "nanos": 3583 - }, - { - "secs": 0, - "nanos": 19375 - }, - { - "secs": 0, - "nanos": 26708 - }, - { - "secs": 0, - "nanos": 84 - } - ], - "user_prompt_length": 203, - "response_size": 653, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_5IYUis90Sq-TiqFl_6Utbw", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_5IYUis90Sq-TiqFl_6Utbw", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "106a2854-8336-4b93-a961-db6be2b52dc2", - "content": "Perfect! Now you have a `.gitignore` file in your `net-v3-server` directory that will ignore all the .NET build artifacts (`bin/`, `obj/`), IDE files, and other temporary files.\n\nReady to move to step 1 - adding the AWS dependencies?" - } - }, - "request_metadata": { - "request_id": "743dc0aa-fb89-464e-9e96-15f379a21d85", - "message_id": "106a2854-8336-4b93-a961-db6be2b52dc2", - "request_start_timestamp_ms": 1757973452600, - "stream_end_timestamp_ms": 1757973455843, - "time_to_first_chunk": { - "secs": 2, - "nanos": 25540959 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 61875 - }, - { - "secs": 0, - "nanos": 2210958 - }, - { - "secs": 0, - "nanos": 213752167 - }, - { - "secs": 0, - "nanos": 138766458 - }, - { - "secs": 0, - "nanos": 290354167 - }, - { - "secs": 0, - "nanos": 226527125 - }, - { - "secs": 0, - "nanos": 340481208 - }, - { - "secs": 0, - "nanos": 5147917 - }, - { - "secs": 0, - "nanos": 167 - }, - { - "secs": 0, - "nanos": 208 - } - ], - "user_prompt_length": 0, - "response_size": 233, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "The only more dependency needed is dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" - } - }, - "timestamp": "2025-09-15T14:58:30.987685-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "1cb698d4-7989-4d62-91fd-b1a85c1f1011", - "content": "Perfect! Let's add that dependency:", - "tool_uses": [ - { - "id": "tooluse_Y29nD5e0QPieaPa8qMg1Jw", - "name": "execute_bash", - "orig_name": "execute_bash", - "args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" - }, - "orig_args": { - "command": "cd /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server && dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0" - } - } - ] - } - }, - "request_metadata": { - "request_id": "8dfd4ca9-b48c-4cf1-9c71-c24ecb93c467", - "message_id": "1cb698d4-7989-4d62-91fd-b1a85c1f1011", - "request_start_timestamp_ms": 1757973510993, - "stream_end_timestamp_ms": 1757973514269, - "time_to_first_chunk": { - "secs": 2, - "nanos": 48055334 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 26083 - }, - { - "secs": 0, - "nanos": 258458 - }, - { - "secs": 0, - "nanos": 209739750 - }, - { - "secs": 0, - "nanos": 387530291 - }, - { - "secs": 0, - "nanos": 612566542 - }, - { - "secs": 0, - "nanos": 17042 - }, - { - "secs": 0, - "nanos": 38000 - }, - { - "secs": 0, - "nanos": 12500 - }, - { - "secs": 0, - "nanos": 2658709 - }, - { - "secs": 0, - "nanos": 1212000 - }, - { - "secs": 0, - "nanos": 2710416 - }, - { - "secs": 0, - "nanos": 6496542 - }, - { - "secs": 0, - "nanos": 15000 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 5041 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 8750 - }, - { - "secs": 0, - "nanos": 18000 - }, - { - "secs": 0, - "nanos": 9209 - }, - { - "secs": 0, - "nanos": 13166 - }, - { - "secs": 0, - "nanos": 8542 - }, - { - "secs": 0, - "nanos": 9209 - }, - { - "secs": 0, - "nanos": 7875 - }, - { - "secs": 0, - "nanos": 6834 - }, - { - "secs": 0, - "nanos": 7000 - }, - { - "secs": 0, - "nanos": 9292 - }, - { - "secs": 0, - "nanos": 25250 - }, - { - "secs": 0, - "nanos": 4417 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 366542 - }, - { - "secs": 0, - "nanos": 1289334 - }, - { - "secs": 0, - "nanos": 12291 - }, - { - "secs": 0, - "nanos": 3083166 - }, - { - "secs": 0, - "nanos": 23917 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 259, - "response_size": 244, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_Y29nD5e0QPieaPa8qMg1Jw", - "execute_bash" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_Y29nD5e0QPieaPa8qMg1Jw", - "content": [ - { - "Json": { - "exit_status": "0", - "stdout": " Determining projects to restore...\n Writing /var/folders/p2/z7c04sc52xz2w704y0z3fkt00000gr/T/tmp9NfU5s.tmp\ninfo : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/x64/sdk/8.0.405/trustedroots/codesignctl.pem'.\ninfo : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/x64/sdk/8.0.405/trustedroots/timestampctl.pem'.\ninfo : Adding PackageReference for package 'Amazon.Extensions.S3.Encryption' into project '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : Restoring packages for /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj...\ninfo : GET https://api.nuget.org/v3/vulnerabilities/index.json\ninfo : OK https://api.nuget.org/v3/vulnerabilities/index.json 69ms\ninfo : GET https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/vulnerability.base.json\ninfo : GET https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/2025.09.15.17.20.52/vulnerability.update.json\ninfo : OK https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/vulnerability.base.json 66ms\ninfo : OK https://api.nuget.org/v3-vulnerabilities/2025.09.11.05.20.35/2025.09.15.17.20.52/vulnerability.update.json 101ms\ninfo : Package 'Amazon.Extensions.S3.Encryption' is compatible with all the specified frameworks in project '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : PackageReference for package 'Amazon.Extensions.S3.Encryption' version '3.0.0' added to file '/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj'.\ninfo : Generating MSBuild file /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/NetV3Server.csproj.nuget.g.props.\ninfo : Writing assets file to disk. Path: /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/obj/project.assets.json\nlog : Restored /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj (in 1.11 sec).", - "stderr": "" - } - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "5505d710-db45-49e4-aaeb-60ac3218ec90", - "content": "Great! The S3 encryption client package has been added. This package includes all the AWS SDK dependencies you need (S3, KMS, etc.).\n\nLet's verify it was added to your project file:", - "tool_uses": [ - { - "id": "tooluse_13rKR-j4Taua8MxHBUfpQg", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/NetV3Server.csproj" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "e922d041-4b5e-4b91-a633-2172e7fab8de", - "message_id": "5505d710-db45-49e4-aaeb-60ac3218ec90", - "request_start_timestamp_ms": 1757973525188, - "stream_end_timestamp_ms": 1757973530625, - "time_to_first_chunk": { - "secs": 2, - "nanos": 450169208 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 334834 - }, - { - "secs": 0, - "nanos": 10875 - }, - { - "secs": 0, - "nanos": 16290500 - }, - { - "secs": 0, - "nanos": 279763541 - }, - { - "secs": 0, - "nanos": 265604708 - }, - { - "secs": 0, - "nanos": 214582791 - }, - { - "secs": 0, - "nanos": 253725042 - }, - { - "secs": 0, - "nanos": 191188208 - }, - { - "secs": 0, - "nanos": 41755208 - }, - { - "secs": 0, - "nanos": 266891417 - }, - { - "secs": 1, - "nanos": 21613417 - }, - { - "secs": 0, - "nanos": 81566667 - }, - { - "secs": 0, - "nanos": 81791458 - }, - { - "secs": 0, - "nanos": 80545833 - }, - { - "secs": 0, - "nanos": 94932542 - }, - { - "secs": 0, - "nanos": 59769583 - }, - { - "secs": 0, - "nanos": 10875 - }, - { - "secs": 0, - "nanos": 2166 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 1345833 - }, - { - "secs": 0, - "nanos": 2039333 - }, - { - "secs": 0, - "nanos": 1964458 - }, - { - "secs": 0, - "nanos": 1706083 - }, - { - "secs": 0, - "nanos": 2014541 - }, - { - "secs": 0, - "nanos": 2064166 - }, - { - "secs": 0, - "nanos": 1789250 - }, - { - "secs": 0, - "nanos": 1941542 - }, - { - "secs": 0, - "nanos": 9583 - }, - { - "secs": 0, - "nanos": 2234458 - }, - { - "secs": 0, - "nanos": 1877625 - }, - { - "secs": 0, - "nanos": 1887584 - }, - { - "secs": 0, - "nanos": 1823084 - }, - { - "secs": 0, - "nanos": 1826458 - }, - { - "secs": 0, - "nanos": 2168625 - }, - { - "secs": 0, - "nanos": 1710209 - }, - { - "secs": 0, - "nanos": 6049208 - }, - { - "secs": 0, - "nanos": 1904792 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 364, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_13rKR-j4Taua8MxHBUfpQg", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_13rKR-j4Taua8MxHBUfpQg", - "content": [ - { - "Text": "\n\n \n net8.0\n enable\n enable\n \n\n \n \n \n \n \n\n" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "dcbae8a1-b171-4bcf-8988-27ec292387a0", - "content": "Perfect! Now you're ready for step 2 - configuring the server. What's next:\n\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. **Configure server** - Set port to 8081 in appsettings.json\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service\n5. **Create controllers** - API endpoints\n\nReady to configure the server port?" - } - }, - "request_metadata": { - "request_id": "f15b962a-e840-4f4d-b404-e616c08174d0", - "message_id": "dcbae8a1-b171-4bcf-8988-27ec292387a0", - "request_start_timestamp_ms": 1757973531226, - "stream_end_timestamp_ms": 1757973535676, - "time_to_first_chunk": { - "secs": 2, - "nanos": 332914458 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 12708 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 267828333 - }, - { - "secs": 0, - "nanos": 167365541 - }, - { - "secs": 0, - "nanos": 183121125 - }, - { - "secs": 0, - "nanos": 99661041 - }, - { - "secs": 0, - "nanos": 128961792 - }, - { - "secs": 0, - "nanos": 285091458 - }, - { - "secs": 0, - "nanos": 52983583 - }, - { - "secs": 0, - "nanos": 41271959 - }, - { - "secs": 0, - "nanos": 220555208 - }, - { - "secs": 0, - "nanos": 54339875 - }, - { - "secs": 0, - "nanos": 306622167 - }, - { - "secs": 0, - "nanos": 308596708 - }, - { - "secs": 0, - "nanos": 31833 - }, - { - "secs": 0, - "nanos": 208 - }, - { - "secs": 0, - "nanos": 209 - } - ], - "user_prompt_length": 0, - "response_size": 383, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Set port to 8084" - } - }, - "timestamp": "2025-09-15T15:00:12.070259-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "36bf6342-d1b2-4b4a-af28-9b7d2a52e28b", - "content": "", - "tool_uses": [ - { - "id": "tooluse_Uc5jWCxoROSBkoI0nbLOIg", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "d7c78400-25e0-421f-aebc-a67c77a47d48", - "message_id": "36bf6342-d1b2-4b4a-af28-9b7d2a52e28b", - "request_start_timestamp_ms": 1757973612074, - "stream_end_timestamp_ms": 1757973615989, - "time_to_first_chunk": { - "secs": 2, - "nanos": 544521834 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 26750 - }, - { - "secs": 0, - "nanos": 38458 - }, - { - "secs": 0, - "nanos": 682665083 - }, - { - "secs": 0, - "nanos": 16833 - }, - { - "secs": 0, - "nanos": 9916 - }, - { - "secs": 0, - "nanos": 2090541 - }, - { - "secs": 0, - "nanos": 1854917 - }, - { - "secs": 0, - "nanos": 10084916 - }, - { - "secs": 0, - "nanos": 1404416 - }, - { - "secs": 0, - "nanos": 1065958 - }, - { - "secs": 0, - "nanos": 15833 - }, - { - "secs": 0, - "nanos": 687542 - }, - { - "secs": 0, - "nanos": 10193666 - }, - { - "secs": 0, - "nanos": 31084 - }, - { - "secs": 0, - "nanos": 7875 - }, - { - "secs": 0, - "nanos": 7708 - }, - { - "secs": 0, - "nanos": 6584 - }, - { - "secs": 0, - "nanos": 6917 - }, - { - "secs": 0, - "nanos": 7166 - }, - { - "secs": 0, - "nanos": 33574542 - }, - { - "secs": 0, - "nanos": 61160417 - }, - { - "secs": 0, - "nanos": 83368750 - }, - { - "secs": 0, - "nanos": 38556125 - }, - { - "secs": 0, - "nanos": 65242750 - }, - { - "secs": 0, - "nanos": 44979709 - }, - { - "secs": 0, - "nanos": 166798000 - }, - { - "secs": 0, - "nanos": 102669667 - }, - { - "secs": 0, - "nanos": 50529917 - }, - { - "secs": 0, - "nanos": 624875 - }, - { - "secs": 0, - "nanos": 14292 - }, - { - "secs": 0, - "nanos": 7532958 - }, - { - "secs": 0, - "nanos": 11125 - }, - { - "secs": 0, - "nanos": 3500 - }, - { - "secs": 0, - "nanos": 11333 - }, - { - "secs": 0, - "nanos": 3712833 - }, - { - "secs": 0, - "nanos": 1268042 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 174, - "response_size": 181, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_Uc5jWCxoROSBkoI0nbLOIg", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_Uc5jWCxoROSBkoI0nbLOIg", - "content": [ - { - "Text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "37c5b725-d7f2-426e-96da-682e1d947d07", - "content": "", - "tool_uses": [ - { - "id": "tooluse_Z_L9fuBNR22QrGjXn4IlxA", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", - "command": "str_replace", - "old_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}", - "new_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", - "command": "str_replace", - "old_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}", - "new_str": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "56194e36-a64b-4d98-bf64-90100bb808fb", - "message_id": "37c5b725-d7f2-426e-96da-682e1d947d07", - "request_start_timestamp_ms": 1757973616575, - "stream_end_timestamp_ms": 1757973622231, - "time_to_first_chunk": { - "secs": 3, - "nanos": 369449333 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 42167 - }, - { - "secs": 0, - "nanos": 1021167 - }, - { - "secs": 0, - "nanos": 863647541 - }, - { - "secs": 0, - "nanos": 881792 - }, - { - "secs": 0, - "nanos": 10250 - }, - { - "secs": 0, - "nanos": 516917 - }, - { - "secs": 0, - "nanos": 1659042 - }, - { - "secs": 0, - "nanos": 44959 - }, - { - "secs": 0, - "nanos": 1578500 - }, - { - "secs": 0, - "nanos": 816667 - }, - { - "secs": 0, - "nanos": 779584 - }, - { - "secs": 0, - "nanos": 11583 - }, - { - "secs": 0, - "nanos": 1064834 - }, - { - "secs": 0, - "nanos": 1126709 - }, - { - "secs": 0, - "nanos": 8625 - }, - { - "secs": 0, - "nanos": 2074667 - }, - { - "secs": 0, - "nanos": 12583 - }, - { - "secs": 0, - "nanos": 190583 - }, - { - "secs": 0, - "nanos": 7500 - }, - { - "secs": 0, - "nanos": 1795417 - }, - { - "secs": 0, - "nanos": 9792 - }, - { - "secs": 0, - "nanos": 536584 - }, - { - "secs": 0, - "nanos": 1020334 - }, - { - "secs": 0, - "nanos": 9500 - }, - { - "secs": 0, - "nanos": 688833 - }, - { - "secs": 0, - "nanos": 323775208 - }, - { - "secs": 0, - "nanos": 27833 - }, - { - "secs": 0, - "nanos": 85708 - }, - { - "secs": 0, - "nanos": 8000 - }, - { - "secs": 0, - "nanos": 464750 - }, - { - "secs": 0, - "nanos": 1053833 - }, - { - "secs": 0, - "nanos": 6500 - }, - { - "secs": 0, - "nanos": 332643125 - }, - { - "secs": 0, - "nanos": 86125 - }, - { - "secs": 0, - "nanos": 7667 - }, - { - "secs": 0, - "nanos": 40542 - }, - { - "secs": 0, - "nanos": 4174583 - }, - { - "secs": 0, - "nanos": 19375 - }, - { - "secs": 0, - "nanos": 51083 - }, - { - "secs": 0, - "nanos": 7041 - }, - { - "secs": 0, - "nanos": 30791 - }, - { - "secs": 0, - "nanos": 9042 - }, - { - "secs": 0, - "nanos": 44416 - }, - { - "secs": 0, - "nanos": 1759125 - }, - { - "secs": 0, - "nanos": 18125 - }, - { - "secs": 0, - "nanos": 713042 - }, - { - "secs": 0, - "nanos": 7792 - }, - { - "secs": 0, - "nanos": 1353167 - }, - { - "secs": 0, - "nanos": 838583 - }, - { - "secs": 0, - "nanos": 8042 - }, - { - "secs": 0, - "nanos": 1055667 - }, - { - "secs": 0, - "nanos": 1053917 - }, - { - "secs": 0, - "nanos": 7209 - }, - { - "secs": 0, - "nanos": 1871083 - }, - { - "secs": 0, - "nanos": 1122875 - }, - { - "secs": 0, - "nanos": 5583 - }, - { - "secs": 0, - "nanos": 3333 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 680216292 - }, - { - "secs": 0, - "nanos": 32953750 - }, - { - "secs": 0, - "nanos": 14375 - }, - { - "secs": 0, - "nanos": 3083 - }, - { - "secs": 0, - "nanos": 3042 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 0, - "nanos": 3291 - }, - { - "secs": 0, - "nanos": 1656209 - }, - { - "secs": 0, - "nanos": 12584 - }, - { - "secs": 0, - "nanos": 552583 - }, - { - "secs": 0, - "nanos": 16875 - }, - { - "secs": 0, - "nanos": 1180916 - }, - { - "secs": 0, - "nanos": 857750 - }, - { - "secs": 0, - "nanos": 9125 - }, - { - "secs": 0, - "nanos": 854042 - }, - { - "secs": 0, - "nanos": 938167 - }, - { - "secs": 0, - "nanos": 1583500 - }, - { - "secs": 0, - "nanos": 10459 - }, - { - "secs": 0, - "nanos": 1302000 - }, - { - "secs": 0, - "nanos": 9000 - }, - { - "secs": 0, - "nanos": 1479542 - }, - { - "secs": 0, - "nanos": 9958 - }, - { - "secs": 0, - "nanos": 1578041 - }, - { - "secs": 0, - "nanos": 1361042 - }, - { - "secs": 0, - "nanos": 6875 - }, - { - "secs": 0, - "nanos": 3209 - }, - { - "secs": 0, - "nanos": 885417 - }, - { - "secs": 0, - "nanos": 8000 - }, - { - "secs": 0, - "nanos": 457416 - }, - { - "secs": 0, - "nanos": 2223250 - }, - { - "secs": 0, - "nanos": 1959500 - }, - { - "secs": 0, - "nanos": 1970334 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 0, - "response_size": 576, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_Z_L9fuBNR22QrGjXn4IlxA", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_Z_L9fuBNR22QrGjXn4IlxA", - "content": [ - { - "Text": "An error occurred processing the tool: \nno occurrences of \"{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\"\n}\" were found" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "65f83228-a308-4c4e-81a4-71c0e5178141", - "content": "", - "tool_uses": [ - { - "id": "tooluse_SBot2eV7QUGNNt2zLnPgHQ", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", - "command": "create", - "file_text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json", - "command": "create", - "file_text": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Information\",\n \"Microsoft.AspNetCore\": \"Warning\"\n }\n },\n \"AllowedHosts\": \"*\",\n \"Urls\": \"http://localhost:8084\"\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "dc31763f-2f38-4ac0-98c6-a1a9fad638cb", - "message_id": "65f83228-a308-4c4e-81a4-71c0e5178141", - "request_start_timestamp_ms": 1757973627124, - "stream_end_timestamp_ms": 1757973631858, - "time_to_first_chunk": { - "secs": 2, - "nanos": 344258709 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 22583 - }, - { - "secs": 0, - "nanos": 18958833 - }, - { - "secs": 0, - "nanos": 956386291 - }, - { - "secs": 0, - "nanos": 8875 - }, - { - "secs": 0, - "nanos": 1550250 - }, - { - "secs": 0, - "nanos": 7542 - }, - { - "secs": 0, - "nanos": 2041250 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 787833 - }, - { - "secs": 0, - "nanos": 4416 - }, - { - "secs": 0, - "nanos": 961417 - }, - { - "secs": 0, - "nanos": 4334 - }, - { - "secs": 0, - "nanos": 863209 - }, - { - "secs": 0, - "nanos": 1249208 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 816792 - }, - { - "secs": 0, - "nanos": 4669583 - }, - { - "secs": 0, - "nanos": 16500 - }, - { - "secs": 0, - "nanos": 6708 - }, - { - "secs": 0, - "nanos": 6000 - }, - { - "secs": 0, - "nanos": 5834 - }, - { - "secs": 0, - "nanos": 13250 - }, - { - "secs": 0, - "nanos": 227834 - }, - { - "secs": 0, - "nanos": 11250 - }, - { - "secs": 0, - "nanos": 575333 - }, - { - "secs": 0, - "nanos": 11125 - }, - { - "secs": 0, - "nanos": 2256166 - }, - { - "secs": 0, - "nanos": 263016833 - }, - { - "secs": 0, - "nanos": 19458 - }, - { - "secs": 0, - "nanos": 677959 - }, - { - "secs": 0, - "nanos": 517388375 - }, - { - "secs": 0, - "nanos": 44086333 - }, - { - "secs": 0, - "nanos": 39403916 - }, - { - "secs": 0, - "nanos": 20235583 - }, - { - "secs": 0, - "nanos": 94834334 - }, - { - "secs": 0, - "nanos": 75191375 - }, - { - "secs": 0, - "nanos": 46924417 - }, - { - "secs": 0, - "nanos": 15806417 - }, - { - "secs": 0, - "nanos": 88065250 - }, - { - "secs": 0, - "nanos": 68795583 - }, - { - "secs": 0, - "nanos": 39125792 - }, - { - "secs": 0, - "nanos": 42616250 - }, - { - "secs": 0, - "nanos": 1181833 - }, - { - "secs": 0, - "nanos": 1906583 - }, - { - "secs": 0, - "nanos": 14074875 - }, - { - "secs": 0, - "nanos": 13291 - }, - { - "secs": 0, - "nanos": 9666750 - }, - { - "secs": 0, - "nanos": 36792 - }, - { - "secs": 0, - "nanos": 98458 - }, - { - "secs": 0, - "nanos": 10375 - }, - { - "secs": 0, - "nanos": 3679958 - }, - { - "secs": 0, - "nanos": 20417 - }, - { - "secs": 0, - "nanos": 51208 - }, - { - "secs": 0, - "nanos": 1125417 - }, - { - "secs": 0, - "nanos": 22292 - }, - { - "secs": 0, - "nanos": 3321292 - }, - { - "secs": 0, - "nanos": 20750 - }, - { - "secs": 0, - "nanos": 173750 - }, - { - "secs": 0, - "nanos": 22208 - }, - { - "secs": 0, - "nanos": 85542 - }, - { - "secs": 0, - "nanos": 3250 - }, - { - "secs": 0, - "nanos": 673500 - }, - { - "secs": 0, - "nanos": 12000 - }, - { - "secs": 0, - "nanos": 2507166 - }, - { - "secs": 0, - "nanos": 1706792 - }, - { - "secs": 0, - "nanos": 1697458 - }, - { - "secs": 0, - "nanos": 84 - } - ], - "user_prompt_length": 0, - "response_size": 393, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_SBot2eV7QUGNNt2zLnPgHQ", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_SBot2eV7QUGNNt2zLnPgHQ", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "e4e85924-7c9e-4de0-9d55-f7e3070697d1", - "content": "Perfect! Server is now configured to run on port 8084. \n\nNext steps:\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. ✅ **Server configured** - Port set to 8084\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service \n5. **Create controllers** - API endpoints\n\nReady to create the models next?" - } - }, - "request_metadata": { - "request_id": "1534a9a1-c1a1-4852-9abd-342bf0c7787b", - "message_id": "e4e85924-7c9e-4de0-9d55-f7e3070697d1", - "request_start_timestamp_ms": 1757973645230, - "stream_end_timestamp_ms": 1757973648725, - "time_to_first_chunk": { - "secs": 2, - "nanos": 105322500 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 14000 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 210039542 - }, - { - "secs": 0, - "nanos": 166278917 - }, - { - "secs": 0, - "nanos": 117245542 - }, - { - "secs": 0, - "nanos": 105178625 - }, - { - "secs": 0, - "nanos": 145004500 - }, - { - "secs": 0, - "nanos": 101339334 - }, - { - "secs": 0, - "nanos": 61737541 - }, - { - "secs": 0, - "nanos": 53424959 - }, - { - "secs": 0, - "nanos": 109557666 - }, - { - "secs": 0, - "nanos": 320054000 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 9791 - }, - { - "secs": 0, - "nanos": 42 - }, - { - "secs": 0, - "nanos": 41 - } - ], - "user_prompt_length": 0, - "response_size": 359, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Yes. What should I be doing for this?" - } - }, - "timestamp": "2025-09-15T15:01:15.741909-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "07d6fd25-3523-428e-a0be-8238ec9513ea", - "content": "Based on the Python server, you need these models to handle the JSON requests/responses:", - "tool_uses": [ - { - "id": "tooluse_3waQ7nstRBivlgdqPcvaUg", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public KeyMaterial KeyMaterial { get; set; } = new();\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public KeyMaterial KeyMaterial { get; set; } = new();\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "f7a38c34-3068-4404-87c1-b48fee119cd5", - "message_id": "07d6fd25-3523-428e-a0be-8238ec9513ea", - "request_start_timestamp_ms": 1757973675745, - "stream_end_timestamp_ms": 1757973684081, - "time_to_first_chunk": { - "secs": 3, - "nanos": 570548667 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 22292 - }, - { - "secs": 0, - "nanos": 7689708 - }, - { - "secs": 0, - "nanos": 682247333 - }, - { - "secs": 0, - "nanos": 274894958 - }, - { - "secs": 0, - "nanos": 204481250 - }, - { - "secs": 0, - "nanos": 676661500 - }, - { - "secs": 0, - "nanos": 21973208 - }, - { - "secs": 0, - "nanos": 2936459 - }, - { - "secs": 0, - "nanos": 5853334 - }, - { - "secs": 0, - "nanos": 18633417 - }, - { - "secs": 0, - "nanos": 5016292 - }, - { - "secs": 0, - "nanos": 24833 - }, - { - "secs": 0, - "nanos": 1917084 - }, - { - "secs": 0, - "nanos": 16375 - }, - { - "secs": 0, - "nanos": 6000 - }, - { - "secs": 0, - "nanos": 2768000 - }, - { - "secs": 0, - "nanos": 15208 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 4666 - }, - { - "secs": 0, - "nanos": 1841834 - }, - { - "secs": 0, - "nanos": 12916 - }, - { - "secs": 0, - "nanos": 1268833 - }, - { - "secs": 0, - "nanos": 11166 - }, - { - "secs": 0, - "nanos": 5583 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 9084 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 160124125 - }, - { - "secs": 0, - "nanos": 307333 - }, - { - "secs": 0, - "nanos": 11042 - }, - { - "secs": 0, - "nanos": 1006750 - }, - { - "secs": 2, - "nanos": 482528000 - }, - { - "secs": 0, - "nanos": 35871083 - }, - { - "secs": 0, - "nanos": 35193458 - }, - { - "secs": 0, - "nanos": 46335000 - }, - { - "secs": 0, - "nanos": 16142083 - }, - { - "secs": 0, - "nanos": 16750 - }, - { - "secs": 0, - "nanos": 6125 - }, - { - "secs": 0, - "nanos": 5583 - }, - { - "secs": 0, - "nanos": 5333 - }, - { - "secs": 0, - "nanos": 4834 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 5592250 - }, - { - "secs": 0, - "nanos": 8958 - }, - { - "secs": 0, - "nanos": 2708 - }, - { - "secs": 0, - "nanos": 4480291 - }, - { - "secs": 0, - "nanos": 6333 - }, - { - "secs": 0, - "nanos": 73958 - }, - { - "secs": 0, - "nanos": 3917 - }, - { - "secs": 0, - "nanos": 468750 - }, - { - "secs": 0, - "nanos": 4458 - }, - { - "secs": 0, - "nanos": 2417 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 563083 - }, - { - "secs": 0, - "nanos": 5833 - }, - { - "secs": 0, - "nanos": 1834000 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 2667 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 11656417 - }, - { - "secs": 0, - "nanos": 15611084 - }, - { - "secs": 0, - "nanos": 5651459 - }, - { - "secs": 0, - "nanos": 17167 - }, - { - "secs": 0, - "nanos": 5334 - }, - { - "secs": 0, - "nanos": 7333 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 5958 - }, - { - "secs": 0, - "nanos": 5667 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 1353583 - }, - { - "secs": 0, - "nanos": 9875 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 721459 - }, - { - "secs": 0, - "nanos": 5083 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 6478834 - }, - { - "secs": 0, - "nanos": 3645084 - }, - { - "secs": 0, - "nanos": 17000 - }, - { - "secs": 0, - "nanos": 73791 - }, - { - "secs": 0, - "nanos": 13125 - }, - { - "secs": 0, - "nanos": 1365083 - }, - { - "secs": 0, - "nanos": 9859625 - }, - { - "secs": 0, - "nanos": 25959 - }, - { - "secs": 0, - "nanos": 13042 - }, - { - "secs": 0, - "nanos": 9665542 - }, - { - "secs": 0, - "nanos": 208 - } - ], - "user_prompt_length": 195, - "response_size": 651, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_3waQ7nstRBivlgdqPcvaUg", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "CancelledToolUses": { - "prompt": "Can you explain what this is?", - "tool_use_results": [ - { - "tool_use_id": "tooluse_3waQ7nstRBivlgdqPcvaUg", - "content": [ - { - "Text": "Tool use was cancelled by the user" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": "2025-09-15T15:49:08.683777-07:00", - "images": null - }, - "assistant": { - "Response": { - "message_id": "abd343e6-7583-47d6-a767-14d68f205305", - "content": "These models represent the JSON data structures that your .NET server will receive and send, matching what the Python server handles.\n\n## What These Models Do:\n\n**ClientRequest** - Represents the JSON sent to `POST /client`:\n```json\n{\n \"config\": {\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012\"\n },\n \"enableLegacyWrappingAlgorithms\": false\n }\n}\n```\n\n## Why You Need Them:\n\nIn the Python server, this JSON gets parsed manually:\n```python\nparsed_data = json.loads(body.decode(\"utf-8\"))\nconfig_data = parsed_data.get(\"config\", {})\nkey_material = config_data.get(\"keyMaterial\", {})\nkms_key_id = key_material[\"kmsKeyId\"]\n```\n\nIn .NET, these classes automatically convert JSON to strongly-typed objects:\n```csharp\n// .NET automatically converts JSON to ClientRequest object\npublic async Task CreateClient([FromBody] ClientRequest request)\n{\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n // ...\n}\n```\n\n## The Pattern:\n- **ClientRequest** = What comes IN to your API\n- **ClientResponse** = What goes OUT from your API \n- **ErrorModels** = Error responses\n\nThis gives you type safety and automatic JSON serialization/deserialization instead of manual parsing." - } - }, - "request_metadata": { - "request_id": "783d1135-80be-4bd4-824a-96ea4bc95dc6", - "message_id": "abd343e6-7583-47d6-a767-14d68f205305", - "request_start_timestamp_ms": 1757976548926, - "stream_end_timestamp_ms": 1757976560036, - "time_to_first_chunk": { - "secs": 4, - "nanos": 631729959 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 18167 - }, - { - "secs": 0, - "nanos": 13768375 - }, - { - "secs": 0, - "nanos": 205140500 - }, - { - "secs": 0, - "nanos": 19291500 - }, - { - "secs": 0, - "nanos": 60673959 - }, - { - "secs": 0, - "nanos": 174323250 - }, - { - "secs": 0, - "nanos": 225767375 - }, - { - "secs": 0, - "nanos": 164963583 - }, - { - "secs": 0, - "nanos": 112904917 - }, - { - "secs": 0, - "nanos": 230161125 - }, - { - "secs": 0, - "nanos": 221409750 - }, - { - "secs": 0, - "nanos": 156795167 - }, - { - "secs": 0, - "nanos": 5625 - }, - { - "secs": 0, - "nanos": 1808167 - }, - { - "secs": 0, - "nanos": 11913917 - }, - { - "secs": 0, - "nanos": 56945750 - }, - { - "secs": 0, - "nanos": 54508584 - }, - { - "secs": 0, - "nanos": 56529541 - }, - { - "secs": 0, - "nanos": 113496209 - }, - { - "secs": 0, - "nanos": 90189334 - }, - { - "secs": 0, - "nanos": 80009250 - }, - { - "secs": 0, - "nanos": 117152083 - }, - { - "secs": 0, - "nanos": 169466833 - }, - { - "secs": 0, - "nanos": 501664166 - }, - { - "secs": 0, - "nanos": 117451833 - }, - { - "secs": 0, - "nanos": 1985000 - }, - { - "secs": 0, - "nanos": 3522542 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 2177917 - }, - { - "secs": 0, - "nanos": 1923375 - }, - { - "secs": 0, - "nanos": 1820417 - }, - { - "secs": 0, - "nanos": 47091834 - }, - { - "secs": 0, - "nanos": 56952083 - }, - { - "secs": 0, - "nanos": 62854291 - }, - { - "secs": 0, - "nanos": 63335791 - }, - { - "secs": 0, - "nanos": 51310041 - }, - { - "secs": 0, - "nanos": 54891292 - }, - { - "secs": 0, - "nanos": 52631208 - }, - { - "secs": 0, - "nanos": 21417 - }, - { - "secs": 0, - "nanos": 5298083 - }, - { - "secs": 0, - "nanos": 19917 - }, - { - "secs": 0, - "nanos": 17500 - }, - { - "secs": 0, - "nanos": 464539417 - }, - { - "secs": 0, - "nanos": 1854417 - }, - { - "secs": 0, - "nanos": 3760417 - }, - { - "secs": 0, - "nanos": 4751625 - }, - { - "secs": 0, - "nanos": 1354625 - }, - { - "secs": 0, - "nanos": 1746625 - }, - { - "secs": 0, - "nanos": 1237416 - }, - { - "secs": 0, - "nanos": 9416 - }, - { - "secs": 0, - "nanos": 19292 - }, - { - "secs": 0, - "nanos": 32791 - }, - { - "secs": 0, - "nanos": 66283875 - }, - { - "secs": 0, - "nanos": 42672375 - }, - { - "secs": 0, - "nanos": 17750 - }, - { - "secs": 0, - "nanos": 18583 - }, - { - "secs": 0, - "nanos": 17416 - }, - { - "secs": 0, - "nanos": 23750 - }, - { - "secs": 0, - "nanos": 22333 - }, - { - "secs": 0, - "nanos": 65417 - }, - { - "secs": 0, - "nanos": 35332417 - }, - { - "secs": 0, - "nanos": 6750 - }, - { - "secs": 0, - "nanos": 19750 - }, - { - "secs": 0, - "nanos": 31453708 - }, - { - "secs": 0, - "nanos": 250563750 - }, - { - "secs": 0, - "nanos": 5791 - }, - { - "secs": 0, - "nanos": 301705167 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 4992666 - }, - { - "secs": 0, - "nanos": 83 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 187, - "response_size": 1258, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "These are example of request/responses. CreateClient:\n\n[DEBUG] HTTP Method: POST\n[DEBUG] Endpoint Path: /client\n[DEBUG] Request Headers: {'content-length': '234', 'host': 'localhost:8081', 'content-type': 'application/json', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: b'{\"config\":{\"enableLegacyUnauthenticatedModes\":false,\"enableDelayedAuthenticationMode\":false,\"enableLegacyWrappingAlgorithms\":false,\"keyMaterial\":{\"kmsKeyId\":\"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"}}}'\n\nGetObject: \n\n[DEBUG] HTTP Method: GET\n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python\n[DEBUG] Request Headers: {'content-length': '0', 'host': 'localhost:8081', 'clientid': 'ff529adc-bc33-4db9-9309-9086d09f62d4', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: (no body for GET request)\nINFO: 127.0.0.1:52340 - \"GET /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python HTTP/1.1\" 200 OK\n\nMore generally, the endpoint path is /object/$BUCKET/$KEY and there is no HTTP body. The client-id is in the request headers. \nPutObject:\n\n[DEBUG] HTTP Method: PUT\n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python\n[DEBUG] Request Headers: {'content-length': '17', 'host': 'localhost:8081', 'clientid': '4c10da8d-4b2e-4a91-a0bb-5ba2c658ebe4', 'content-metadata': '[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]', 'content-type': 'application/octet-stream', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}\n[DEBUG] Raw Request Data: b'simple-test-input'\nINFO: 127.0.0.1:52330 - \"PUT /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python HTTP/1.1\" 200 OK\n\n" - } - }, - "timestamp": "2025-09-15T15:51:50.760504-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "da219f02-3fb0-4944-9ebc-c947b55e1fc8", - "content": "Perfect! Now I can see the exact request/response patterns. Let me create the correct models:", - "tool_uses": [ - { - "id": "tooluse_WfvGXUKOQSuoxkos4pb-1A", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public bool EnableLegacyUnauthenticatedModes { get; set; }\n public bool EnableDelayedAuthenticationMode { get; set; }\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n public KeyMaterial KeyMaterial { get; set; } = new();\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientRequest\n{\n public ClientConfig Config { get; set; } = new();\n}\n\npublic class ClientConfig\n{\n public bool EnableLegacyUnauthenticatedModes { get; set; }\n public bool EnableDelayedAuthenticationMode { get; set; }\n public bool EnableLegacyWrappingAlgorithms { get; set; }\n public KeyMaterial KeyMaterial { get; set; } = new();\n}\n\npublic class KeyMaterial\n{\n public string KmsKeyId { get; set; } = string.Empty;\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "723b6def-bc08-4533-b9a5-1402064be574", - "message_id": "da219f02-3fb0-4944-9ebc-c947b55e1fc8", - "request_start_timestamp_ms": 1757976710766, - "stream_end_timestamp_ms": 1757976720595, - "time_to_first_chunk": { - "secs": 6, - "nanos": 77480291 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 11500 - }, - { - "secs": 0, - "nanos": 10556792 - }, - { - "secs": 0, - "nanos": 40145792 - }, - { - "secs": 0, - "nanos": 178052542 - }, - { - "secs": 0, - "nanos": 19365000 - }, - { - "secs": 0, - "nanos": 53056000 - }, - { - "secs": 0, - "nanos": 161449916 - }, - { - "secs": 0, - "nanos": 567052542 - }, - { - "secs": 0, - "nanos": 11833 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 2458 - }, - { - "secs": 0, - "nanos": 71458 - }, - { - "secs": 0, - "nanos": 6417 - }, - { - "secs": 0, - "nanos": 1247166 - }, - { - "secs": 0, - "nanos": 1912625 - }, - { - "secs": 0, - "nanos": 5541 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 62901125 - }, - { - "secs": 0, - "nanos": 7083 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 35042 - }, - { - "secs": 0, - "nanos": 6375 - }, - { - "secs": 0, - "nanos": 3250 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 117248500 - }, - { - "secs": 0, - "nanos": 3234750 - }, - { - "secs": 0, - "nanos": 15751625 - }, - { - "secs": 0, - "nanos": 6041 - }, - { - "secs": 1, - "nanos": 992288000 - }, - { - "secs": 0, - "nanos": 55097291 - }, - { - "secs": 0, - "nanos": 60212208 - }, - { - "secs": 0, - "nanos": 175849958 - }, - { - "secs": 0, - "nanos": 108202667 - }, - { - "secs": 0, - "nanos": 31066916 - }, - { - "secs": 0, - "nanos": 1648083 - }, - { - "secs": 0, - "nanos": 7255458 - }, - { - "secs": 0, - "nanos": 36541 - }, - { - "secs": 0, - "nanos": 5432375 - }, - { - "secs": 0, - "nanos": 52584 - }, - { - "secs": 0, - "nanos": 14084 - }, - { - "secs": 0, - "nanos": 1413916 - }, - { - "secs": 0, - "nanos": 1103333 - }, - { - "secs": 0, - "nanos": 35792 - }, - { - "secs": 0, - "nanos": 3362375 - }, - { - "secs": 0, - "nanos": 14000 - }, - { - "secs": 0, - "nanos": 3784875 - }, - { - "secs": 0, - "nanos": 13250 - }, - { - "secs": 0, - "nanos": 176958 - }, - { - "secs": 0, - "nanos": 2216542 - }, - { - "secs": 0, - "nanos": 1746833 - }, - { - "secs": 0, - "nanos": 10375 - }, - { - "secs": 0, - "nanos": 1952167 - }, - { - "secs": 0, - "nanos": 1936750 - }, - { - "secs": 0, - "nanos": 9292 - }, - { - "secs": 0, - "nanos": 2918208 - }, - { - "secs": 0, - "nanos": 11459 - }, - { - "secs": 0, - "nanos": 5875 - }, - { - "secs": 0, - "nanos": 30850375 - }, - { - "secs": 0, - "nanos": 18042 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 4333 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 4291 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 34667 - }, - { - "secs": 0, - "nanos": 830500 - }, - { - "secs": 0, - "nanos": 10250 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 4958 - }, - { - "secs": 0, - "nanos": 6250 - }, - { - "secs": 0, - "nanos": 358666 - }, - { - "secs": 0, - "nanos": 8064125 - }, - { - "secs": 0, - "nanos": 20584 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 4708 - }, - { - "secs": 0, - "nanos": 3017833 - }, - { - "secs": 0, - "nanos": 906667 - }, - { - "secs": 0, - "nanos": 1332833 - }, - { - "secs": 0, - "nanos": 5416 - }, - { - "secs": 0, - "nanos": 1989167 - }, - { - "secs": 0, - "nanos": 2336834 - }, - { - "secs": 0, - "nanos": 1673667 - }, - { - "secs": 0, - "nanos": 17792 - }, - { - "secs": 0, - "nanos": 1229084 - }, - { - "secs": 0, - "nanos": 20375 - }, - { - "secs": 0, - "nanos": 815375 - }, - { - "secs": 0, - "nanos": 1968750 - }, - { - "secs": 0, - "nanos": 17625 - }, - { - "secs": 0, - "nanos": 289209 - }, - { - "secs": 0, - "nanos": 16541 - }, - { - "secs": 0, - "nanos": 604708 - }, - { - "secs": 0, - "nanos": 14000 - }, - { - "secs": 0, - "nanos": 5105625 - }, - { - "secs": 0, - "nanos": 22500 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 1968, - "response_size": 783, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_WfvGXUKOQSuoxkos4pb-1A", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_WfvGXUKOQSuoxkos4pb-1A", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "5eabd27b-b6cd-405e-a835-67a2e34ebc90", - "content": "", - "tool_uses": [ - { - "id": "tooluse_O4A_46LJSpKApGrCiiWhLQ", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientResponse\n{\n public string ClientId { get; set; } = string.Empty;\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class ClientResponse\n{\n public string ClientId { get; set; } = string.Empty;\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "efe53e58-861f-4378-abcc-e8a0191efc8e", - "message_id": "5eabd27b-b6cd-405e-a835-67a2e34ebc90", - "request_start_timestamp_ms": 1757976751603, - "stream_end_timestamp_ms": 1757976755466, - "time_to_first_chunk": { - "secs": 2, - "nanos": 550903000 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 21959 - }, - { - "secs": 0, - "nanos": 8042 - }, - { - "secs": 0, - "nanos": 786241459 - }, - { - "secs": 0, - "nanos": 35768041 - }, - { - "secs": 0, - "nanos": 44780792 - }, - { - "secs": 0, - "nanos": 68475250 - }, - { - "secs": 0, - "nanos": 40930458 - }, - { - "secs": 0, - "nanos": 18250208 - }, - { - "secs": 0, - "nanos": 57608583 - }, - { - "secs": 0, - "nanos": 34264750 - }, - { - "secs": 0, - "nanos": 42643291 - }, - { - "secs": 0, - "nanos": 59271958 - }, - { - "secs": 0, - "nanos": 24799125 - }, - { - "secs": 0, - "nanos": 62183000 - }, - { - "secs": 0, - "nanos": 5177708 - }, - { - "secs": 0, - "nanos": 27959 - }, - { - "secs": 0, - "nanos": 282042 - }, - { - "secs": 0, - "nanos": 882958 - }, - { - "secs": 0, - "nanos": 7750 - }, - { - "secs": 0, - "nanos": 2416167 - }, - { - "secs": 0, - "nanos": 11583 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 590000 - }, - { - "secs": 0, - "nanos": 329083 - }, - { - "secs": 0, - "nanos": 8708 - }, - { - "secs": 0, - "nanos": 1308917 - }, - { - "secs": 0, - "nanos": 7667 - }, - { - "secs": 0, - "nanos": 1381083 - }, - { - "secs": 0, - "nanos": 7542 - }, - { - "secs": 0, - "nanos": 1163291 - }, - { - "secs": 0, - "nanos": 310000 - }, - { - "secs": 0, - "nanos": 9125 - }, - { - "secs": 0, - "nanos": 1714833 - }, - { - "secs": 0, - "nanos": 7542 - }, - { - "secs": 0, - "nanos": 1704417 - }, - { - "secs": 0, - "nanos": 21833 - }, - { - "secs": 0, - "nanos": 7125 - }, - { - "secs": 0, - "nanos": 994000 - }, - { - "secs": 0, - "nanos": 34208 - }, - { - "secs": 0, - "nanos": 1123167 - }, - { - "secs": 0, - "nanos": 24375 - }, - { - "secs": 0, - "nanos": 31667 - }, - { - "secs": 0, - "nanos": 971750 - }, - { - "secs": 0, - "nanos": 13167 - }, - { - "secs": 0, - "nanos": 1289958 - }, - { - "secs": 0, - "nanos": 26583 - }, - { - "secs": 0, - "nanos": 3173834 - }, - { - "secs": 0, - "nanos": 34833 - }, - { - "secs": 0, - "nanos": 11792 - }, - { - "secs": 0, - "nanos": 15334 - }, - { - "secs": 0, - "nanos": 98917 - }, - { - "secs": 0, - "nanos": 1535417 - }, - { - "secs": 0, - "nanos": 11916 - }, - { - "secs": 0, - "nanos": 82166 - }, - { - "secs": 0, - "nanos": 2987250 - }, - { - "secs": 0, - "nanos": 6174625 - }, - { - "secs": 0, - "nanos": 42167 - }, - { - "secs": 0, - "nanos": 125 - } - ], - "user_prompt_length": 0, - "response_size": 320, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_O4A_46LJSpKApGrCiiWhLQ", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_O4A_46LJSpKApGrCiiWhLQ", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "652f81fd-c3a7-4097-88b5-dfa088c91d84", - "content": "", - "tool_uses": [ - { - "id": "tooluse_Pir3pwnFSjiffDJ92UUStw", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class GenericServerError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#GenericServerError\";\n public string Message { get; set; } = string.Empty;\n}\n\npublic class S3EncryptionClientError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#S3EncryptionClientError\";\n public string Message { get; set; } = string.Empty;\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs", - "command": "create", - "file_text": "namespace NetV3Server.Models;\n\npublic class GenericServerError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#GenericServerError\";\n public string Message { get; set; } = string.Empty;\n}\n\npublic class S3EncryptionClientError\n{\n public string __type { get; set; } = \"software.amazon.encryption.s3#S3EncryptionClientError\";\n public string Message { get; set; } = string.Empty;\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "65082f4e-c0cd-4abe-b912-26eb1978aaf6", - "message_id": "652f81fd-c3a7-4097-88b5-dfa088c91d84", - "request_start_timestamp_ms": 1757976771866, - "stream_end_timestamp_ms": 1757976777269, - "time_to_first_chunk": { - "secs": 2, - "nanos": 486623542 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 11125 - }, - { - "secs": 0, - "nanos": 4042 - }, - { - "secs": 0, - "nanos": 614651667 - }, - { - "secs": 0, - "nanos": 100458 - }, - { - "secs": 0, - "nanos": 3333 - }, - { - "secs": 0, - "nanos": 4208 - }, - { - "secs": 0, - "nanos": 1914167 - }, - { - "secs": 0, - "nanos": 4000 - }, - { - "secs": 0, - "nanos": 853583 - }, - { - "secs": 0, - "nanos": 5917 - }, - { - "secs": 0, - "nanos": 2467083 - }, - { - "secs": 0, - "nanos": 3791 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1666 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 3090459 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 1790000 - }, - { - "secs": 0, - "nanos": 6565375 - }, - { - "secs": 0, - "nanos": 6600125 - }, - { - "secs": 0, - "nanos": 187495667 - }, - { - "secs": 0, - "nanos": 28205250 - }, - { - "secs": 0, - "nanos": 42105417 - }, - { - "secs": 0, - "nanos": 8386083 - }, - { - "secs": 1, - "nanos": 541204959 - }, - { - "secs": 0, - "nanos": 70784083 - }, - { - "secs": 0, - "nanos": 30713959 - }, - { - "secs": 0, - "nanos": 37000125 - }, - { - "secs": 0, - "nanos": 27816959 - }, - { - "secs": 0, - "nanos": 5289500 - }, - { - "secs": 0, - "nanos": 13708 - }, - { - "secs": 0, - "nanos": 6691417 - }, - { - "secs": 0, - "nanos": 33778000 - }, - { - "secs": 0, - "nanos": 14384042 - }, - { - "secs": 0, - "nanos": 9746250 - }, - { - "secs": 0, - "nanos": 22792 - }, - { - "secs": 0, - "nanos": 13515792 - }, - { - "secs": 0, - "nanos": 19961500 - }, - { - "secs": 0, - "nanos": 48461875 - }, - { - "secs": 0, - "nanos": 52899750 - }, - { - "secs": 0, - "nanos": 19583 - }, - { - "secs": 0, - "nanos": 5166 - }, - { - "secs": 0, - "nanos": 3750 - }, - { - "secs": 0, - "nanos": 27246334 - }, - { - "secs": 0, - "nanos": 15871334 - }, - { - "secs": 0, - "nanos": 23458 - }, - { - "secs": 0, - "nanos": 84084 - }, - { - "secs": 0, - "nanos": 11296250 - }, - { - "secs": 0, - "nanos": 5768084 - }, - { - "secs": 0, - "nanos": 11918750 - }, - { - "secs": 0, - "nanos": 16390000 - }, - { - "secs": 0, - "nanos": 16708 - }, - { - "secs": 0, - "nanos": 73125 - }, - { - "secs": 0, - "nanos": 11416 - }, - { - "secs": 0, - "nanos": 642125 - }, - { - "secs": 0, - "nanos": 1674791 - }, - { - "secs": 0, - "nanos": 18833 - }, - { - "secs": 0, - "nanos": 331167 - }, - { - "secs": 0, - "nanos": 13875 - }, - { - "secs": 0, - "nanos": 513583 - }, - { - "secs": 0, - "nanos": 10125 - }, - { - "secs": 0, - "nanos": 1028167 - }, - { - "secs": 0, - "nanos": 14375 - }, - { - "secs": 0, - "nanos": 224583 - }, - { - "secs": 0, - "nanos": 2216709 - }, - { - "secs": 0, - "nanos": 14042 - }, - { - "secs": 0, - "nanos": 18083 - }, - { - "secs": 0, - "nanos": 4166 - }, - { - "secs": 0, - "nanos": 6459 - }, - { - "secs": 0, - "nanos": 5416 - }, - { - "secs": 0, - "nanos": 4959 - }, - { - "secs": 0, - "nanos": 5917 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 700292 - }, - { - "secs": 0, - "nanos": 4666 - }, - { - "secs": 0, - "nanos": 10875 - }, - { - "secs": 0, - "nanos": 3250 - }, - { - "secs": 0, - "nanos": 915083 - }, - { - "secs": 0, - "nanos": 11000 - }, - { - "secs": 0, - "nanos": 2342334 - }, - { - "secs": 0, - "nanos": 167 - } - ], - "user_prompt_length": 0, - "response_size": 620, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_Pir3pwnFSjiffDJ92UUStw", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_Pir3pwnFSjiffDJ92UUStw", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "c2e0c94e-d11a-4468-b9f8-05b04a913ef3", - "content": "Now you have the exact models matching your request/response patterns:\n\n## What Each Model Handles:\n\n**ClientRequest** - Handles the POST /client JSON:\n```json\n{\n \"config\": {\n \"enableLegacyUnauthenticatedModes\": false,\n \"enableDelayedAuthenticationMode\": false, \n \"enableLegacyWrappingAlgorithms\": false,\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"\n }\n }\n}\n```\n\n**ClientResponse** - Returns the client ID:\n```json\n{\n \"clientId\": \"ff529adc-bc33-4db9-9309-9086d09f62d4\"\n}\n```\n\n**Error Models** - Match Python server's error format with `__type` field\n\n**Note:** For GET/PUT `/object/{bucket}/{key}`:\n- No special models needed - use route parameters and headers\n- `clientid` comes from request header\n- `content-metadata` comes from request header \n- Body is raw bytes\n\nReady for step 4 - creating the client cache service?" - } - }, - "request_metadata": { - "request_id": "db84f7a3-75b5-4083-a6cd-6df32db4c91e", - "message_id": "c2e0c94e-d11a-4468-b9f8-05b04a913ef3", - "request_start_timestamp_ms": 1757976841925, - "stream_end_timestamp_ms": 1757976849660, - "time_to_first_chunk": { - "secs": 2, - "nanos": 289558083 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 25917 - }, - { - "secs": 0, - "nanos": 8166 - }, - { - "secs": 0, - "nanos": 41783625 - }, - { - "secs": 0, - "nanos": 47654667 - }, - { - "secs": 0, - "nanos": 61645875 - }, - { - "secs": 0, - "nanos": 141076916 - }, - { - "secs": 0, - "nanos": 60248166 - }, - { - "secs": 0, - "nanos": 58699417 - }, - { - "secs": 0, - "nanos": 114440750 - }, - { - "secs": 0, - "nanos": 108020584 - }, - { - "secs": 0, - "nanos": 119092833 - }, - { - "secs": 0, - "nanos": 72398583 - }, - { - "secs": 0, - "nanos": 87272791 - }, - { - "secs": 0, - "nanos": 65579666 - }, - { - "secs": 0, - "nanos": 274669542 - }, - { - "secs": 0, - "nanos": 82889750 - }, - { - "secs": 0, - "nanos": 122202500 - }, - { - "secs": 0, - "nanos": 1302000 - }, - { - "secs": 0, - "nanos": 8750 - }, - { - "secs": 0, - "nanos": 436312292 - }, - { - "secs": 0, - "nanos": 5135917 - }, - { - "secs": 0, - "nanos": 1855875 - }, - { - "secs": 0, - "nanos": 2142541 - }, - { - "secs": 0, - "nanos": 1932250 - }, - { - "secs": 0, - "nanos": 1503458 - }, - { - "secs": 0, - "nanos": 7500 - }, - { - "secs": 0, - "nanos": 116882083 - }, - { - "secs": 0, - "nanos": 12541 - }, - { - "secs": 0, - "nanos": 176375 - }, - { - "secs": 0, - "nanos": 5625 - }, - { - "secs": 0, - "nanos": 2447000 - }, - { - "secs": 0, - "nanos": 17791 - }, - { - "secs": 0, - "nanos": 170081542 - }, - { - "secs": 0, - "nanos": 1764458 - }, - { - "secs": 0, - "nanos": 110354833 - }, - { - "secs": 0, - "nanos": 53043000 - }, - { - "secs": 0, - "nanos": 77336167 - }, - { - "secs": 0, - "nanos": 183374417 - }, - { - "secs": 0, - "nanos": 107177625 - }, - { - "secs": 0, - "nanos": 424756833 - }, - { - "secs": 0, - "nanos": 7083 - }, - { - "secs": 0, - "nanos": 984875 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 18958 - }, - { - "secs": 0, - "nanos": 47917 - }, - { - "secs": 0, - "nanos": 21958 - }, - { - "secs": 0, - "nanos": 24584 - }, - { - "secs": 0, - "nanos": 17000 - }, - { - "secs": 0, - "nanos": 19875 - }, - { - "secs": 0, - "nanos": 18541 - }, - { - "secs": 0, - "nanos": 31916 - }, - { - "secs": 0, - "nanos": 17458 - }, - { - "secs": 0, - "nanos": 48208 - }, - { - "secs": 0, - "nanos": 33375 - }, - { - "secs": 0, - "nanos": 38292 - }, - { - "secs": 0, - "nanos": 23875 - }, - { - "secs": 0, - "nanos": 22250 - }, - { - "secs": 0, - "nanos": 18833 - }, - { - "secs": 0, - "nanos": 23208 - }, - { - "secs": 0, - "nanos": 27500 - }, - { - "secs": 0, - "nanos": 16792 - }, - { - "secs": 0, - "nanos": 16459 - }, - { - "secs": 0, - "nanos": 17667 - }, - { - "secs": 0, - "nanos": 16542 - }, - { - "secs": 0, - "nanos": 20334 - }, - { - "secs": 0, - "nanos": 10042 - }, - { - "secs": 0, - "nanos": 17250 - }, - { - "secs": 0, - "nanos": 9833 - }, - { - "secs": 0, - "nanos": 17000 - }, - { - "secs": 0, - "nanos": 19291 - }, - { - "secs": 0, - "nanos": 16500 - }, - { - "secs": 0, - "nanos": 21958 - }, - { - "secs": 0, - "nanos": 19708 - }, - { - "secs": 0, - "nanos": 6666 - }, - { - "secs": 0, - "nanos": 84 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 907, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "yes" - } - }, - "timestamp": "2025-09-15T15:56:38.910978-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "fa393476-d36e-4d18-bdbd-26a2e202ce5f", - "content": "", - "tool_uses": [ - { - "id": "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs", - "command": "create", - "file_text": "using Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing System.Collections.Concurrent;\n\nnamespace NetV3Server.Services;\n\npublic interface IClientCacheService\n{\n string AddClient(AmazonS3EncryptionClientV2 client);\n AmazonS3EncryptionClientV2? GetClient(string clientId);\n}\n\npublic class ClientCacheService : IClientCacheService\n{\n private readonly ConcurrentDictionary _clients = new();\n\n public string AddClient(AmazonS3EncryptionClientV2 client)\n {\n var clientId = Guid.NewGuid().ToString();\n _clients[clientId] = client;\n return clientId;\n }\n\n public AmazonS3EncryptionClientV2? GetClient(string clientId)\n {\n _clients.TryGetValue(clientId, out var client);\n return client;\n }\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs", - "command": "create", - "file_text": "using Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing System.Collections.Concurrent;\n\nnamespace NetV3Server.Services;\n\npublic interface IClientCacheService\n{\n string AddClient(AmazonS3EncryptionClientV2 client);\n AmazonS3EncryptionClientV2? GetClient(string clientId);\n}\n\npublic class ClientCacheService : IClientCacheService\n{\n private readonly ConcurrentDictionary _clients = new();\n\n public string AddClient(AmazonS3EncryptionClientV2 client)\n {\n var clientId = Guid.NewGuid().ToString();\n _clients[clientId] = client;\n return clientId;\n }\n\n public AmazonS3EncryptionClientV2? GetClient(string clientId)\n {\n _clients.TryGetValue(clientId, out var client);\n return client;\n }\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "84af30b0-fa05-4ba6-8695-f5454dc47e69", - "message_id": "fa393476-d36e-4d18-bdbd-26a2e202ce5f", - "request_start_timestamp_ms": 1757976998918, - "stream_end_timestamp_ms": 1757977005274, - "time_to_first_chunk": { - "secs": 2, - "nanos": 688399125 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 38167 - }, - { - "secs": 0, - "nanos": 7917 - }, - { - "secs": 0, - "nanos": 981249625 - }, - { - "secs": 0, - "nanos": 103032916 - }, - { - "secs": 0, - "nanos": 30958 - }, - { - "secs": 0, - "nanos": 1728000 - }, - { - "secs": 0, - "nanos": 14499584 - }, - { - "secs": 0, - "nanos": 940500 - }, - { - "secs": 0, - "nanos": 1912958 - }, - { - "secs": 0, - "nanos": 4721709 - }, - { - "secs": 0, - "nanos": 14417 - }, - { - "secs": 0, - "nanos": 3518542 - }, - { - "secs": 0, - "nanos": 424875 - }, - { - "secs": 0, - "nanos": 35865292 - }, - { - "secs": 0, - "nanos": 9125 - }, - { - "secs": 0, - "nanos": 2417 - }, - { - "secs": 0, - "nanos": 1709 - }, - { - "secs": 0, - "nanos": 54167 - }, - { - "secs": 0, - "nanos": 7958 - }, - { - "secs": 0, - "nanos": 9967334 - }, - { - "secs": 0, - "nanos": 18042 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 2959 - }, - { - "secs": 0, - "nanos": 5166 - }, - { - "secs": 0, - "nanos": 6458 - }, - { - "secs": 0, - "nanos": 62458 - }, - { - "secs": 0, - "nanos": 16250 - }, - { - "secs": 0, - "nanos": 6417 - }, - { - "secs": 0, - "nanos": 10708 - }, - { - "secs": 0, - "nanos": 4167 - }, - { - "secs": 2, - "nanos": 420773292 - }, - { - "secs": 0, - "nanos": 18708 - }, - { - "secs": 0, - "nanos": 6667 - }, - { - "secs": 0, - "nanos": 5209 - }, - { - "secs": 0, - "nanos": 2993000 - }, - { - "secs": 0, - "nanos": 1577709 - }, - { - "secs": 0, - "nanos": 9292 - }, - { - "secs": 0, - "nanos": 6756416 - }, - { - "secs": 0, - "nanos": 17625 - }, - { - "secs": 0, - "nanos": 2858208 - }, - { - "secs": 0, - "nanos": 14000 - }, - { - "secs": 0, - "nanos": 4318667 - }, - { - "secs": 0, - "nanos": 49958 - }, - { - "secs": 0, - "nanos": 1937334 - }, - { - "secs": 0, - "nanos": 1952584 - }, - { - "secs": 0, - "nanos": 2111541 - }, - { - "secs": 0, - "nanos": 2071042 - }, - { - "secs": 0, - "nanos": 1166292 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 15958 - }, - { - "secs": 0, - "nanos": 2600500 - }, - { - "secs": 0, - "nanos": 17750 - }, - { - "secs": 0, - "nanos": 35542 - }, - { - "secs": 0, - "nanos": 14583 - }, - { - "secs": 0, - "nanos": 32699417 - }, - { - "secs": 0, - "nanos": 21417 - }, - { - "secs": 0, - "nanos": 9792 - }, - { - "secs": 0, - "nanos": 5875 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 5666 - }, - { - "secs": 0, - "nanos": 4917 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 4834 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 5556333 - }, - { - "secs": 0, - "nanos": 9958 - }, - { - "secs": 0, - "nanos": 2667 - }, - { - "secs": 0, - "nanos": 2416 - }, - { - "secs": 0, - "nanos": 2292 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 2166 - }, - { - "secs": 0, - "nanos": 2985542 - }, - { - "secs": 0, - "nanos": 9292 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 6458 - }, - { - "secs": 0, - "nanos": 2291 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 11625 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 2667 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 2791 - }, - { - "secs": 0, - "nanos": 2333 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 2166 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 48917 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 3625 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1416 - }, - { - "secs": 0, - "nanos": 1375 - }, - { - "secs": 0, - "nanos": 1375 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1250 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 2166 - }, - { - "secs": 0, - "nanos": 8875 - }, - { - "secs": 0, - "nanos": 3792 - }, - { - "secs": 0, - "nanos": 3656833 - }, - { - "secs": 0, - "nanos": 11959 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 5333 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 7750 - }, - { - "secs": 0, - "nanos": 3958 - }, - { - "secs": 0, - "nanos": 2417 - }, - { - "secs": 0, - "nanos": 4417 - }, - { - "secs": 0, - "nanos": 999917 - }, - { - "secs": 0, - "nanos": 5750 - }, - { - "secs": 0, - "nanos": 1217167 - }, - { - "secs": 0, - "nanos": 8375 - }, - { - "secs": 0, - "nanos": 2583 - }, - { - "secs": 0, - "nanos": 882750 - }, - { - "secs": 0, - "nanos": 8208 - }, - { - "secs": 0, - "nanos": 5003917 - }, - { - "secs": 0, - "nanos": 8750 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 8375 - }, - { - "secs": 0, - "nanos": 3333 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 1981834 - }, - { - "secs": 0, - "nanos": 7208 - }, - { - "secs": 0, - "nanos": 1279417 - }, - { - "secs": 0, - "nanos": 1960042 - }, - { - "secs": 0, - "nanos": 41 - } - ], - "user_prompt_length": 161, - "response_size": 1015, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_7mUw6qkOTHq8xPXE5uV8vQ", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "d5c5ab21-7a30-486f-a1e4-2b827c093b09", - "content": "Perfect! This service replicates the Python server's `client_cache` dictionary:\n\n## What This Service Does:\n\n**Replaces Python's:**\n```python\nclient_cache = {} # Dictionary to store clients with UUIDs as keys\n\n# Add client\nclient_id = str(uuid.uuid4())\nclient_cache[client_id] = client\n\n# Get client \nclient = client_cache.get(client_id)\n```\n\n**With .NET's:**\n```csharp\n// Add client\nstring clientId = _clientCacheService.AddClient(client);\n\n// Get client\nvar client = _clientCacheService.GetClient(clientId);\n```\n\n## Key Features:\n- **Thread-safe** - Uses `ConcurrentDictionary` (multiple requests can access safely)\n- **Interface** - `IClientCacheService` for dependency injection\n- **UUID generation** - `Guid.NewGuid()` like Python's `uuid.uuid4()`\n- **Null handling** - Returns `null` if client not found\n\nNext step: Register this service in `Program.cs` and create the controllers. Ready?" - } - }, - "request_metadata": { - "request_id": "180b5a53-544d-4cdc-85f8-e18f3bf9f891", - "message_id": "d5c5ab21-7a30-486f-a1e4-2b827c093b09", - "request_start_timestamp_ms": 1757977190987, - "stream_end_timestamp_ms": 1757977200574, - "time_to_first_chunk": { - "secs": 4, - "nanos": 374357542 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 19167 - }, - { - "secs": 0, - "nanos": 6041 - }, - { - "secs": 0, - "nanos": 39964959 - }, - { - "secs": 0, - "nanos": 65937708 - }, - { - "secs": 0, - "nanos": 49371416 - }, - { - "secs": 0, - "nanos": 55226292 - }, - { - "secs": 0, - "nanos": 120336417 - }, - { - "secs": 0, - "nanos": 42042 - }, - { - "secs": 0, - "nanos": 47009666 - }, - { - "secs": 0, - "nanos": 72529292 - }, - { - "secs": 0, - "nanos": 50171875 - }, - { - "secs": 0, - "nanos": 185849167 - }, - { - "secs": 0, - "nanos": 41024875 - }, - { - "secs": 0, - "nanos": 114017375 - }, - { - "secs": 0, - "nanos": 51586709 - }, - { - "secs": 0, - "nanos": 114075708 - }, - { - "secs": 0, - "nanos": 54457750 - }, - { - "secs": 0, - "nanos": 60492833 - }, - { - "secs": 0, - "nanos": 50480291 - }, - { - "secs": 0, - "nanos": 56490000 - }, - { - "secs": 0, - "nanos": 54789959 - }, - { - "secs": 0, - "nanos": 111317000 - }, - { - "secs": 0, - "nanos": 113278125 - }, - { - "secs": 0, - "nanos": 112004666 - }, - { - "secs": 0, - "nanos": 56030500 - }, - { - "secs": 0, - "nanos": 170201459 - }, - { - "secs": 0, - "nanos": 109292000 - }, - { - "secs": 0, - "nanos": 113360042 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 165501875 - }, - { - "secs": 0, - "nanos": 7375 - }, - { - "secs": 0, - "nanos": 14500 - }, - { - "secs": 0, - "nanos": 56683458 - }, - { - "secs": 0, - "nanos": 53013209 - }, - { - "secs": 0, - "nanos": 111424417 - }, - { - "secs": 0, - "nanos": 58205750 - }, - { - "secs": 0, - "nanos": 57958292 - }, - { - "secs": 0, - "nanos": 53428334 - }, - { - "secs": 0, - "nanos": 58031542 - }, - { - "secs": 0, - "nanos": 163391750 - }, - { - "secs": 0, - "nanos": 5708 - }, - { - "secs": 0, - "nanos": 398058292 - }, - { - "secs": 0, - "nanos": 7750 - }, - { - "secs": 0, - "nanos": 1502458 - }, - { - "secs": 0, - "nanos": 3023000 - }, - { - "secs": 0, - "nanos": 6416 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 4541 - }, - { - "secs": 0, - "nanos": 221365750 - }, - { - "secs": 0, - "nanos": 13041 - }, - { - "secs": 0, - "nanos": 507375 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 326553333 - }, - { - "secs": 0, - "nanos": 18375 - }, - { - "secs": 0, - "nanos": 29958 - }, - { - "secs": 0, - "nanos": 23958 - }, - { - "secs": 0, - "nanos": 19875 - }, - { - "secs": 0, - "nanos": 19292 - }, - { - "secs": 0, - "nanos": 18292 - }, - { - "secs": 0, - "nanos": 16375 - }, - { - "secs": 0, - "nanos": 16458 - }, - { - "secs": 0, - "nanos": 8875 - }, - { - "secs": 0, - "nanos": 15542 - }, - { - "secs": 0, - "nanos": 17584 - }, - { - "secs": 0, - "nanos": 16291 - }, - { - "secs": 0, - "nanos": 12542 - }, - { - "secs": 0, - "nanos": 18083 - }, - { - "secs": 0, - "nanos": 19583 - }, - { - "secs": 0, - "nanos": 19958 - }, - { - "secs": 0, - "nanos": 17500 - }, - { - "secs": 0, - "nanos": 18625 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 541 - }, - { - "secs": 0, - "nanos": 208 - } - ], - "user_prompt_length": 0, - "response_size": 896, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "yes" - } - }, - "timestamp": "2025-09-15T16:01:43.772104-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "6a1f2a59-c3fe-4131-a330-2670a2829915", - "content": "Let's first register the service in Program.cs, then create the controllers:", - "tool_uses": [ - { - "id": "tooluse_tR7cAfkrQyCBYYQ3RMElzA", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs" - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs" - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "f998ef7a-efbf-458d-8e96-2313212894f6", - "message_id": "6a1f2a59-c3fe-4131-a330-2670a2829915", - "request_start_timestamp_ms": 1757977303779, - "stream_end_timestamp_ms": 1757977308224, - "time_to_first_chunk": { - "secs": 2, - "nanos": 808598042 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 39375 - }, - { - "secs": 0, - "nanos": 57624167 - }, - { - "secs": 0, - "nanos": 44774667 - }, - { - "secs": 0, - "nanos": 2487708 - }, - { - "secs": 0, - "nanos": 89583 - }, - { - "secs": 0, - "nanos": 6667667 - }, - { - "secs": 0, - "nanos": 19391292 - }, - { - "secs": 0, - "nanos": 214956375 - }, - { - "secs": 0, - "nanos": 923553458 - }, - { - "secs": 0, - "nanos": 79027708 - }, - { - "secs": 0, - "nanos": 71783166 - }, - { - "secs": 0, - "nanos": 56079500 - }, - { - "secs": 0, - "nanos": 39121875 - }, - { - "secs": 0, - "nanos": 56872584 - }, - { - "secs": 0, - "nanos": 29174708 - }, - { - "secs": 0, - "nanos": 14833 - }, - { - "secs": 0, - "nanos": 1429667 - }, - { - "secs": 0, - "nanos": 7208 - }, - { - "secs": 0, - "nanos": 1187875 - }, - { - "secs": 0, - "nanos": 9542 - }, - { - "secs": 0, - "nanos": 767417 - }, - { - "secs": 0, - "nanos": 989791 - }, - { - "secs": 0, - "nanos": 10917 - }, - { - "secs": 0, - "nanos": 30821917 - }, - { - "secs": 0, - "nanos": 10000 - }, - { - "secs": 0, - "nanos": 3417 - }, - { - "secs": 0, - "nanos": 3000 - }, - { - "secs": 0, - "nanos": 2875 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2958 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 708 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 161, - "response_size": 251, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_tR7cAfkrQyCBYYQ3RMElzA", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_tR7cAfkrQyCBYYQ3RMElzA", - "content": [ - { - "Text": "var builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container.\n// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle\nbuilder.Services.AddEndpointsApiExplorer();\nbuilder.Services.AddSwaggerGen();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline.\nif (app.Environment.IsDevelopment())\n{\n app.UseSwagger();\n app.UseSwaggerUI();\n}\n\nvar summaries = new[]\n{\n \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\"\n};\n\napp.MapGet(\"/weatherforecast\", () =>\n{\n var forecast = Enumerable.Range(1, 5).Select(index =>\n new WeatherForecast\n (\n DateOnly.FromDateTime(DateTime.Now.AddDays(index)),\n Random.Shared.Next(-20, 55),\n summaries[Random.Shared.Next(summaries.Length)]\n ))\n .ToArray();\n return forecast;\n})\n.WithName(\"GetWeatherForecast\")\n.WithOpenApi();\n\napp.Run();\n\nrecord WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)\n{\n public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);\n}" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "ee694061-729a-4c00-8b06-4f9afd7962e3", - "content": "", - "tool_uses": [ - { - "id": "tooluse_tdhsdQtkToC-xYM9lDB2xg", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs", - "command": "create", - "file_text": "using NetV3Server.Services;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container\nbuilder.Services.AddControllers();\nbuilder.Services.AddSingleton();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline\napp.MapControllers();\n\napp.Run();" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs", - "command": "create", - "file_text": "using NetV3Server.Services;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container\nbuilder.Services.AddControllers();\nbuilder.Services.AddSingleton();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline\napp.MapControllers();\n\napp.Run();" - } - } - ] - } - }, - "request_metadata": { - "request_id": "171b2889-611e-4a35-a046-39323ed12c11", - "message_id": "ee694061-729a-4c00-8b06-4f9afd7962e3", - "request_start_timestamp_ms": 1757977308799, - "stream_end_timestamp_ms": 1757977312895, - "time_to_first_chunk": { - "secs": 2, - "nanos": 232189916 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 28833 - }, - { - "secs": 0, - "nanos": 44333 - }, - { - "secs": 0, - "nanos": 628267625 - }, - { - "secs": 0, - "nanos": 49511042 - }, - { - "secs": 0, - "nanos": 11475875 - }, - { - "secs": 0, - "nanos": 76311041 - }, - { - "secs": 0, - "nanos": 15720458 - }, - { - "secs": 0, - "nanos": 52472333 - }, - { - "secs": 0, - "nanos": 4636958 - }, - { - "secs": 0, - "nanos": 13584 - }, - { - "secs": 0, - "nanos": 5667 - }, - { - "secs": 0, - "nanos": 9958 - }, - { - "secs": 0, - "nanos": 17584 - }, - { - "secs": 0, - "nanos": 10000 - }, - { - "secs": 0, - "nanos": 12042 - }, - { - "secs": 0, - "nanos": 1532084 - }, - { - "secs": 0, - "nanos": 10417 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 8500 - }, - { - "secs": 0, - "nanos": 1123541 - }, - { - "secs": 0, - "nanos": 994375 - }, - { - "secs": 0, - "nanos": 12584 - }, - { - "secs": 0, - "nanos": 845583 - }, - { - "secs": 0, - "nanos": 1052667 - }, - { - "secs": 0, - "nanos": 9875 - }, - { - "secs": 0, - "nanos": 772916 - }, - { - "secs": 0, - "nanos": 2814291 - }, - { - "secs": 0, - "nanos": 12875 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 0, - "nanos": 952555417 - }, - { - "secs": 0, - "nanos": 15541 - }, - { - "secs": 0, - "nanos": 5958 - }, - { - "secs": 0, - "nanos": 5250 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 4458 - }, - { - "secs": 0, - "nanos": 4792 - }, - { - "secs": 0, - "nanos": 5500 - }, - { - "secs": 0, - "nanos": 4791 - }, - { - "secs": 0, - "nanos": 4792 - }, - { - "secs": 0, - "nanos": 19917 - }, - { - "secs": 0, - "nanos": 918250 - }, - { - "secs": 0, - "nanos": 28209 - }, - { - "secs": 0, - "nanos": 1785042 - }, - { - "secs": 0, - "nanos": 240958 - }, - { - "secs": 0, - "nanos": 16125 - }, - { - "secs": 0, - "nanos": 491041 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 637750 - }, - { - "secs": 0, - "nanos": 3208 - }, - { - "secs": 0, - "nanos": 1365167 - }, - { - "secs": 0, - "nanos": 3291 - }, - { - "secs": 0, - "nanos": 940250 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 1046959 - }, - { - "secs": 0, - "nanos": 3417 - }, - { - "secs": 0, - "nanos": 879250 - }, - { - "secs": 0, - "nanos": 3459 - }, - { - "secs": 0, - "nanos": 994375 - }, - { - "secs": 0, - "nanos": 3792 - }, - { - "secs": 0, - "nanos": 1034041 - }, - { - "secs": 0, - "nanos": 3250 - }, - { - "secs": 0, - "nanos": 876125 - }, - { - "secs": 0, - "nanos": 5583 - }, - { - "secs": 0, - "nanos": 1445500 - }, - { - "secs": 0, - "nanos": 5958 - }, - { - "secs": 0, - "nanos": 786042 - }, - { - "secs": 0, - "nanos": 2429959 - }, - { - "secs": 0, - "nanos": 17708 - }, - { - "secs": 0, - "nanos": 8334 - }, - { - "secs": 0, - "nanos": 9833 - }, - { - "secs": 0, - "nanos": 16417 - }, - { - "secs": 0, - "nanos": 11083 - }, - { - "secs": 0, - "nanos": 2045500 - }, - { - "secs": 0, - "nanos": 45761625 - }, - { - "secs": 0, - "nanos": 18125 - }, - { - "secs": 0, - "nanos": 6875 - }, - { - "secs": 0, - "nanos": 6209 - }, - { - "secs": 0, - "nanos": 6083 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 6042 - }, - { - "secs": 0, - "nanos": 80042 - }, - { - "secs": 0, - "nanos": 84 - } - ], - "user_prompt_length": 0, - "response_size": 518, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_tdhsdQtkToC-xYM9lDB2xg", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_tdhsdQtkToC-xYM9lDB2xg", - "content": [ - { - "Text": "" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "61715673-5fce-4b5d-9335-af60469e379b", - "content": "Now let's create the ClientController:", - "tool_uses": [ - { - "id": "tooluse_lhriUwZlQcmc0igmyxSvIQ", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", - "command": "create", - "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.KeyManagementService;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n // Create KMS client (hardcoded to us-west-2 like Python)\n var kmsClient = new AmazonKeyManagementServiceClient(Amazon.RegionEndpoint.USWest2);\n \n // Create S3 client\n var s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USWest2);\n \n // Create encryption materials\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionMaterials = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, new Dictionary());\n \n // Create encryption configuration\n var config = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy)\n {\n StorageMode = CryptoStorageMode.ObjectMetadata\n };\n \n // Create S3 encryption client\n var encryptionClient = new AmazonS3EncryptionClientV2(s3Client, config, encryptionMaterials);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", - "command": "create", - "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.KeyManagementService;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n // Create KMS client (hardcoded to us-west-2 like Python)\n var kmsClient = new AmazonKeyManagementServiceClient(Amazon.RegionEndpoint.USWest2);\n \n // Create S3 client\n var s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USWest2);\n \n // Create encryption materials\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionMaterials = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, new Dictionary());\n \n // Create encryption configuration\n var config = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy)\n {\n StorageMode = CryptoStorageMode.ObjectMetadata\n };\n \n // Create S3 encryption client\n var encryptionClient = new AmazonS3EncryptionClientV2(s3Client, config, encryptionMaterials);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "c91bf1df-c116-49d6-b60a-8c06fd0eea12", - "message_id": "61715673-5fce-4b5d-9335-af60469e379b", - "request_start_timestamp_ms": 1757977440049, - "stream_end_timestamp_ms": 1757977450462, - "time_to_first_chunk": { - "secs": 2, - "nanos": 481822708 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 15667 - }, - { - "secs": 0, - "nanos": 4458 - }, - { - "secs": 0, - "nanos": 38995792 - }, - { - "secs": 0, - "nanos": 170156125 - }, - { - "secs": 0, - "nanos": 575584709 - }, - { - "secs": 0, - "nanos": 6750 - }, - { - "secs": 0, - "nanos": 147833 - }, - { - "secs": 0, - "nanos": 568250 - }, - { - "secs": 0, - "nanos": 4291 - }, - { - "secs": 0, - "nanos": 894125 - }, - { - "secs": 0, - "nanos": 3709 - }, - { - "secs": 0, - "nanos": 20833 - }, - { - "secs": 0, - "nanos": 1234875 - }, - { - "secs": 0, - "nanos": 5708 - }, - { - "secs": 0, - "nanos": 63425333 - }, - { - "secs": 0, - "nanos": 6958 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1333 - }, - { - "secs": 0, - "nanos": 1334 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1333 - }, - { - "secs": 0, - "nanos": 288292 - }, - { - "secs": 0, - "nanos": 3833 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 82063250 - }, - { - "secs": 0, - "nanos": 140417 - }, - { - "secs": 0, - "nanos": 12875 - }, - { - "secs": 6, - "nanos": 832877709 - }, - { - "secs": 0, - "nanos": 16625 - }, - { - "secs": 0, - "nanos": 10083 - }, - { - "secs": 0, - "nanos": 6291 - }, - { - "secs": 0, - "nanos": 3472333 - }, - { - "secs": 0, - "nanos": 8542 - }, - { - "secs": 0, - "nanos": 1326000 - }, - { - "secs": 0, - "nanos": 5208 - }, - { - "secs": 0, - "nanos": 1993709 - }, - { - "secs": 0, - "nanos": 2747083 - }, - { - "secs": 0, - "nanos": 4001208 - }, - { - "secs": 0, - "nanos": 47625 - }, - { - "secs": 0, - "nanos": 11000 - }, - { - "secs": 0, - "nanos": 4100875 - }, - { - "secs": 0, - "nanos": 62708 - }, - { - "secs": 0, - "nanos": 18333 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 3402667 - }, - { - "secs": 0, - "nanos": 59125 - }, - { - "secs": 0, - "nanos": 11625 - }, - { - "secs": 0, - "nanos": 3844667 - }, - { - "secs": 0, - "nanos": 17208 - }, - { - "secs": 0, - "nanos": 438792 - }, - { - "secs": 0, - "nanos": 40633500 - }, - { - "secs": 0, - "nanos": 17875 - }, - { - "secs": 0, - "nanos": 7583 - }, - { - "secs": 0, - "nanos": 13208 - }, - { - "secs": 0, - "nanos": 24750 - }, - { - "secs": 0, - "nanos": 13125 - }, - { - "secs": 0, - "nanos": 15709 - }, - { - "secs": 0, - "nanos": 9791 - }, - { - "secs": 0, - "nanos": 11084 - }, - { - "secs": 0, - "nanos": 9792 - }, - { - "secs": 0, - "nanos": 11250 - }, - { - "secs": 0, - "nanos": 10125 - }, - { - "secs": 0, - "nanos": 11750 - }, - { - "secs": 0, - "nanos": 8958 - }, - { - "secs": 0, - "nanos": 11000 - }, - { - "secs": 0, - "nanos": 9625 - }, - { - "secs": 0, - "nanos": 10708 - }, - { - "secs": 0, - "nanos": 9875 - }, - { - "secs": 0, - "nanos": 10791 - }, - { - "secs": 0, - "nanos": 32583 - }, - { - "secs": 0, - "nanos": 6500 - }, - { - "secs": 0, - "nanos": 9708 - }, - { - "secs": 0, - "nanos": 10625 - }, - { - "secs": 0, - "nanos": 9542 - }, - { - "secs": 0, - "nanos": 11000 - }, - { - "secs": 0, - "nanos": 9417 - }, - { - "secs": 0, - "nanos": 11166 - }, - { - "secs": 0, - "nanos": 8500 - }, - { - "secs": 0, - "nanos": 11959 - }, - { - "secs": 0, - "nanos": 9375 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 9208 - }, - { - "secs": 0, - "nanos": 10583 - }, - { - "secs": 0, - "nanos": 5625 - }, - { - "secs": 0, - "nanos": 4958 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 5209 - }, - { - "secs": 0, - "nanos": 8500 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 34375 - }, - { - "secs": 0, - "nanos": 1498375 - }, - { - "secs": 0, - "nanos": 13250 - }, - { - "secs": 0, - "nanos": 6417 - }, - { - "secs": 0, - "nanos": 4564667 - }, - { - "secs": 0, - "nanos": 13083 - }, - { - "secs": 0, - "nanos": 5458 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 4708 - }, - { - "secs": 0, - "nanos": 7542 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 5334 - }, - { - "secs": 0, - "nanos": 5708 - }, - { - "secs": 0, - "nanos": 4916 - }, - { - "secs": 0, - "nanos": 2583 - }, - { - "secs": 0, - "nanos": 4584 - }, - { - "secs": 0, - "nanos": 2081041 - }, - { - "secs": 0, - "nanos": 11334 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 9709 - }, - { - "secs": 0, - "nanos": 18584 - }, - { - "secs": 0, - "nanos": 9834 - }, - { - "secs": 0, - "nanos": 12583 - }, - { - "secs": 0, - "nanos": 8500 - }, - { - "secs": 0, - "nanos": 2979917 - }, - { - "secs": 0, - "nanos": 1849708 - }, - { - "secs": 0, - "nanos": 2670708 - }, - { - "secs": 0, - "nanos": 1185750 - }, - { - "secs": 0, - "nanos": 3445750 - }, - { - "secs": 0, - "nanos": 458041 - }, - { - "secs": 0, - "nanos": 11125 - }, - { - "secs": 0, - "nanos": 1787250 - }, - { - "secs": 0, - "nanos": 9209 - }, - { - "secs": 0, - "nanos": 4006833 - }, - { - "secs": 0, - "nanos": 9625 - }, - { - "secs": 0, - "nanos": 5083 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 4458 - }, - { - "secs": 0, - "nanos": 8958 - }, - { - "secs": 0, - "nanos": 704083 - }, - { - "secs": 0, - "nanos": 10625 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 4209 - }, - { - "secs": 0, - "nanos": 222250 - }, - { - "secs": 0, - "nanos": 8959 - }, - { - "secs": 0, - "nanos": 4709 - }, - { - "secs": 0, - "nanos": 2542167 - }, - { - "secs": 0, - "nanos": 8458 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 35957292 - }, - { - "secs": 0, - "nanos": 85500 - }, - { - "secs": 0, - "nanos": 11584 - }, - { - "secs": 0, - "nanos": 5458 - }, - { - "secs": 0, - "nanos": 2417 - }, - { - "secs": 0, - "nanos": 3208 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 3041 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 2792 - }, - { - "secs": 0, - "nanos": 14834 - }, - { - "secs": 0, - "nanos": 4916 - }, - { - "secs": 0, - "nanos": 2542 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 2333 - }, - { - "secs": 0, - "nanos": 2292 - }, - { - "secs": 0, - "nanos": 1959 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 4125 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 4125 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 4541 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 3083 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 3833 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 3459 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 2667 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1666 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1791 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 5958 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 5792 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 3833 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 10833 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 2166 - }, - { - "secs": 0, - "nanos": 76250 - }, - { - "secs": 0, - "nanos": 4625 - }, - { - "secs": 0, - "nanos": 2459 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 1959 - }, - { - "secs": 0, - "nanos": 1958 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 3090250 - }, - { - "secs": 0, - "nanos": 5708 - }, - { - "secs": 0, - "nanos": 11000 - }, - { - "secs": 0, - "nanos": 3794958 - }, - { - "secs": 0, - "nanos": 7333 - }, - { - "secs": 0, - "nanos": 2834 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 2333 - }, - { - "secs": 0, - "nanos": 4667 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 3625 - }, - { - "secs": 0, - "nanos": 2792 - }, - { - "secs": 0, - "nanos": 3669458 - }, - { - "secs": 0, - "nanos": 6750 - }, - { - "secs": 0, - "nanos": 575709 - }, - { - "secs": 0, - "nanos": 4750 - }, - { - "secs": 0, - "nanos": 2500 - }, - { - "secs": 0, - "nanos": 4635042 - }, - { - "secs": 0, - "nanos": 6792 - }, - { - "secs": 0, - "nanos": 2542 - }, - { - "secs": 0, - "nanos": 2583 - }, - { - "secs": 0, - "nanos": 2417 - }, - { - "secs": 0, - "nanos": 6458 - }, - { - "secs": 0, - "nanos": 2292 - }, - { - "secs": 0, - "nanos": 1274750 - }, - { - "secs": 0, - "nanos": 22167 - }, - { - "secs": 0, - "nanos": 18750 - }, - { - "secs": 0, - "nanos": 3915667 - }, - { - "secs": 0, - "nanos": 7167 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 3083 - }, - { - "secs": 0, - "nanos": 7292 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 1250 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 1125 - }, - { - "secs": 0, - "nanos": 669542 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 2416 - }, - { - "secs": 0, - "nanos": 2084 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1958 - }, - { - "secs": 0, - "nanos": 4125 - }, - { - "secs": 0, - "nanos": 2042 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1791 - }, - { - "secs": 0, - "nanos": 1709 - }, - { - "secs": 0, - "nanos": 44000 - }, - { - "secs": 0, - "nanos": 2749250 - }, - { - "secs": 0, - "nanos": 2145000 - }, - { - "secs": 0, - "nanos": 6375 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 3542 - }, - { - "secs": 0, - "nanos": 9833 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 7875 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 1917 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 64417 - }, - { - "secs": 0, - "nanos": 10042 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 2000 - }, - { - "secs": 0, - "nanos": 1239167 - }, - { - "secs": 0, - "nanos": 7042 - }, - { - "secs": 0, - "nanos": 2834 - }, - { - "secs": 0, - "nanos": 2458 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 2041 - }, - { - "secs": 0, - "nanos": 14500 - }, - { - "secs": 0, - "nanos": 3459 - }, - { - "secs": 0, - "nanos": 2458 - }, - { - "secs": 0, - "nanos": 2125 - }, - { - "secs": 0, - "nanos": 916 - }, - { - "secs": 0, - "nanos": 10709 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 3416 - }, - { - "secs": 0, - "nanos": 2750 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 2346917 - }, - { - "secs": 0, - "nanos": 41 - } - ], - "user_prompt_length": 0, - "response_size": 2365, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_lhriUwZlQcmc0igmyxSvIQ", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "CancelledToolUses": { - "prompt": "Can you point me to this code in python?", - "tool_use_results": [ - { - "tool_use_id": "tooluse_lhriUwZlQcmc0igmyxSvIQ", - "content": [ - { - "Text": "Tool use was cancelled by the user" - } - ], - "status": "Error" - } - ] - } - }, - "timestamp": "2025-09-15T16:04:49.225623-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "5261871a-7196-40b1-b527-3d4b301964cb", - "content": "Sure! Here's the corresponding Python code from the test server:", - "tool_uses": [ - { - "id": "tooluse_hUPgjdC6SiK35mcMP88qYw", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", - "start_line": 120, - "end_line": 160 - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", - "start_line": 120, - "end_line": 160 - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "7b68d27e-abf5-450d-9653-2ecfa75cc17b", - "message_id": "5261871a-7196-40b1-b527-3d4b301964cb", - "request_start_timestamp_ms": 1757977489467, - "stream_end_timestamp_ms": 1757977493121, - "time_to_first_chunk": { - "secs": 2, - "nanos": 265048375 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 20959 - }, - { - "secs": 0, - "nanos": 6333 - }, - { - "secs": 0, - "nanos": 40191458 - }, - { - "secs": 0, - "nanos": 77387625 - }, - { - "secs": 0, - "nanos": 75763042 - }, - { - "secs": 0, - "nanos": 37136292 - }, - { - "secs": 0, - "nanos": 35948667 - }, - { - "secs": 0, - "nanos": 170207833 - }, - { - "secs": 0, - "nanos": 935385375 - }, - { - "secs": 0, - "nanos": 16209 - }, - { - "secs": 0, - "nanos": 25875 - }, - { - "secs": 0, - "nanos": 9333 - }, - { - "secs": 0, - "nanos": 2380375 - }, - { - "secs": 0, - "nanos": 3369167 - }, - { - "secs": 0, - "nanos": 6542 - }, - { - "secs": 0, - "nanos": 3584 - }, - { - "secs": 0, - "nanos": 4667 - }, - { - "secs": 0, - "nanos": 3417 - }, - { - "secs": 0, - "nanos": 2958 - }, - { - "secs": 0, - "nanos": 2958 - }, - { - "secs": 0, - "nanos": 3000 - }, - { - "secs": 0, - "nanos": 1026791 - }, - { - "secs": 0, - "nanos": 9500 - }, - { - "secs": 0, - "nanos": 2625 - }, - { - "secs": 0, - "nanos": 2803958 - }, - { - "secs": 0, - "nanos": 7209 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 3000 - }, - { - "secs": 0, - "nanos": 3250 - }, - { - "secs": 0, - "nanos": 729250 - }, - { - "secs": 0, - "nanos": 7291 - }, - { - "secs": 0, - "nanos": 552458 - }, - { - "secs": 0, - "nanos": 6194584 - }, - { - "secs": 0, - "nanos": 134416 - }, - { - "secs": 0, - "nanos": 14125 - }, - { - "secs": 0, - "nanos": 8833 - }, - { - "secs": 0, - "nanos": 6917 - }, - { - "secs": 0, - "nanos": 6959 - }, - { - "secs": 0, - "nanos": 11750 - }, - { - "secs": 0, - "nanos": 206375 - }, - { - "secs": 0, - "nanos": 42 - } - ], - "user_prompt_length": 198, - "response_size": 256, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_hUPgjdC6SiK35mcMP88qYw", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_hUPgjdC6SiK35mcMP88qYw", - "content": [ - { - "Text": " \"ClientID header is required\", status.HTTP_400_BAD_REQUEST\n )\n\n # Get the S3EncryptionClient from the client_cache\n client = client_cache.get(client_id)\n if not client:\n return create_generic_server_error(\n f\"No client found for ClientID: {client_id}\", status.HTTP_404_NOT_FOUND\n )\n\n metadata = request.headers.get(\"Content-Metadata\", \"\")\n enc_ctx = metadata_string_to_map(metadata)\n\n try:\n # Use the client to make a GetObject request to S3\n response = client.get_object(**{\"Bucket\": bucket, \"Key\": key, \"EncryptionContext\": enc_ctx})\n\n # Extract the body and metadata from the response\n body = response.get(\"Body\").read() if response.get(\"Body\") else b\"\"\n metadata = response.get(\"Metadata\", [])\n\n # Convert metadata dictionary to a list of key-value pairs if it's a dict\n if isinstance(metadata, dict):\n metadata_list = [f\"{key}={value}\" for key, value in metadata.items()]\n else:\n metadata_list = metadata if isinstance(metadata, list) else []\n\n # Set the Content-Metadata header in the response\n # Convert metadata_list to a comma-separated string\n metadata_str = \",\".join(metadata_list) if metadata_list else \"\"\n headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "90a94be6-43f5-4cc3-a294-f444dc78ac78", - "content": "", - "tool_uses": [ - { - "id": "tooluse_xJEsgi-2QjC6iDbpmWDy_g", - "name": "fs_read", - "orig_name": "fs_read", - "args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", - "start_line": 150, - "end_line": 200 - } - ] - }, - "orig_args": { - "operations": [ - { - "mode": "Line", - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server/src/main.py", - "start_line": 150, - "end_line": 200 - } - ] - } - } - ] - } - }, - "request_metadata": { - "request_id": "4167e1cb-f863-4706-9a79-fcd89cf1f6df", - "message_id": "90a94be6-43f5-4cc3-a294-f444dc78ac78", - "request_start_timestamp_ms": 1757977493586, - "stream_end_timestamp_ms": 1757977496583, - "time_to_first_chunk": { - "secs": 2, - "nanos": 396021291 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 34625 - }, - { - "secs": 0, - "nanos": 9485459 - }, - { - "secs": 0, - "nanos": 568155791 - }, - { - "secs": 0, - "nanos": 4080333 - }, - { - "secs": 0, - "nanos": 13875 - }, - { - "secs": 0, - "nanos": 4959 - }, - { - "secs": 0, - "nanos": 4542 - }, - { - "secs": 0, - "nanos": 801334 - }, - { - "secs": 0, - "nanos": 9417 - }, - { - "secs": 0, - "nanos": 3472666 - }, - { - "secs": 0, - "nanos": 9166 - }, - { - "secs": 0, - "nanos": 4792 - }, - { - "secs": 0, - "nanos": 2194792 - }, - { - "secs": 0, - "nanos": 10292 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 3792 - }, - { - "secs": 0, - "nanos": 2580625 - }, - { - "secs": 0, - "nanos": 9792 - }, - { - "secs": 0, - "nanos": 5916 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 3650209 - }, - { - "secs": 0, - "nanos": 11250 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 4333 - }, - { - "secs": 0, - "nanos": 13125 - }, - { - "secs": 0, - "nanos": 8667 - }, - { - "secs": 0, - "nanos": 209792 - }, - { - "secs": 0, - "nanos": 8584 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 2337000 - }, - { - "secs": 0, - "nanos": 1499875 - }, - { - "secs": 0, - "nanos": 1982500 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 0, - "response_size": 192, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_xJEsgi-2QjC6iDbpmWDy_g", - "fs_read" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "ToolUseResults": { - "tool_use_results": [ - { - "tool_use_id": "tooluse_xJEsgi-2QjC6iDbpmWDy_g", - "content": [ - { - "Text": " headers = {\"Content-Metadata\": metadata_str}\n\n # Return the body as the response payload\n return Response(content=body, headers=headers)\n except S3EncryptionClientError as ex:\n return create_s3_encryption_client_error(str(ex))\n except Exception as e:\n return create_generic_server_error(str(e))\n\n\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n \"\"\"\n Handle POST requests to /client by creating an S3EncryptionClient.\n \"\"\"\n body = await request.body()\n\n # Parse the bytes object as JSON\n try:\n # Decode bytes to string and parse as JSON\n parsed_data = json.loads(body.decode(\"utf-8\"))\n\n # Extract config from the parsed data\n config_data = parsed_data.get(\"config\", {})\n # Extract key material if provided\n key_material = config_data.get(\"keyMaterial\", {})\n\n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n\n # TODO pull region from ARN\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n\n # Create S3EncryptionClient\n client = S3EncryptionClient(wrapped_client, client_config)\n\n # Generate a client ID using UUID\n client_id = str(uuid.uuid4())\n\n # Add the client to the client_cache dictionary\n client_cache[client_id] = client\n\n return {\"clientId\": client_id}\n except json.JSONDecodeError as e:" - } - ], - "status": "Success" - } - ] - } - }, - "timestamp": null, - "images": null - }, - "assistant": { - "Response": { - "message_id": "c5e73dca-4a04-4b68-ac06-9db4f30a593f", - "content": "Here's the Python code that corresponds to what I was trying to implement in .NET:\n\n## Python Client Creation (lines 160-190):\n\n```python\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n # Parse JSON request\n parsed_data = json.loads(body.decode(\"utf-8\"))\n config_data = parsed_data.get(\"config\", {})\n key_material = config_data.get(\"keyMaterial\", {})\n \n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n \n # Create KMS client (hardcoded us-west-2)\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n \n # Create keyring and S3 encryption client\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n client = S3EncryptionClient(wrapped_client, client_config)\n \n # Generate UUID and cache client\n client_id = str(uuid.uuid4())\n client_cache[client_id] = client\n \n return {\"clientId\": client_id}\n```\n\nThe .NET version needs to do the same thing but with the Amazon.Extensions.S3.Encryption v3 API instead of the Python API." - } - }, - "request_metadata": { - "request_id": "3989e535-7d89-4f3b-9bc9-2a38561e1f1f", - "message_id": "c5e73dca-4a04-4b68-ac06-9db4f30a593f", - "request_start_timestamp_ms": 1757977497297, - "stream_end_timestamp_ms": 1757977510793, - "time_to_first_chunk": { - "secs": 2, - "nanos": 123736792 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 13625 - }, - { - "secs": 0, - "nanos": 11750 - }, - { - "secs": 0, - "nanos": 35474458 - }, - { - "secs": 0, - "nanos": 52508791 - }, - { - "secs": 0, - "nanos": 58570833 - }, - { - "secs": 0, - "nanos": 80316084 - }, - { - "secs": 0, - "nanos": 27029333 - }, - { - "secs": 0, - "nanos": 54990542 - }, - { - "secs": 0, - "nanos": 53780917 - }, - { - "secs": 0, - "nanos": 56931792 - }, - { - "secs": 0, - "nanos": 52288458 - }, - { - "secs": 0, - "nanos": 60277959 - }, - { - "secs": 0, - "nanos": 54682083 - }, - { - "secs": 0, - "nanos": 54092583 - }, - { - "secs": 0, - "nanos": 53651750 - }, - { - "secs": 0, - "nanos": 54378167 - }, - { - "secs": 0, - "nanos": 54924417 - }, - { - "secs": 0, - "nanos": 54354084 - }, - { - "secs": 0, - "nanos": 55123125 - }, - { - "secs": 0, - "nanos": 56179917 - }, - { - "secs": 0, - "nanos": 57912375 - }, - { - "secs": 0, - "nanos": 57621750 - }, - { - "secs": 0, - "nanos": 48962375 - }, - { - "secs": 0, - "nanos": 60463708 - }, - { - "secs": 0, - "nanos": 107805208 - }, - { - "secs": 0, - "nanos": 61915166 - }, - { - "secs": 0, - "nanos": 52671917 - }, - { - "secs": 0, - "nanos": 54904208 - }, - { - "secs": 0, - "nanos": 64280375 - }, - { - "secs": 0, - "nanos": 102642167 - }, - { - "secs": 0, - "nanos": 9958 - }, - { - "secs": 0, - "nanos": 215482459 - }, - { - "secs": 0, - "nanos": 15417 - }, - { - "secs": 0, - "nanos": 73542 - }, - { - "secs": 0, - "nanos": 2016750 - }, - { - "secs": 0, - "nanos": 394301958 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 1498125 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 435000 - }, - { - "secs": 0, - "nanos": 711366042 - }, - { - "secs": 0, - "nanos": 8167 - }, - { - "secs": 0, - "nanos": 1339208 - }, - { - "secs": 0, - "nanos": 17984416 - }, - { - "secs": 0, - "nanos": 17459 - }, - { - "secs": 0, - "nanos": 6084 - }, - { - "secs": 0, - "nanos": 7580959 - }, - { - "secs": 0, - "nanos": 408750 - }, - { - "secs": 0, - "nanos": 5292 - }, - { - "secs": 0, - "nanos": 24958 - }, - { - "secs": 0, - "nanos": 18167 - }, - { - "secs": 0, - "nanos": 19708 - }, - { - "secs": 0, - "nanos": 18042 - }, - { - "secs": 0, - "nanos": 18750 - }, - { - "secs": 0, - "nanos": 17792 - }, - { - "secs": 0, - "nanos": 20667 - }, - { - "secs": 0, - "nanos": 20709 - }, - { - "secs": 0, - "nanos": 23791 - }, - { - "secs": 0, - "nanos": 17041 - }, - { - "secs": 0, - "nanos": 22917 - }, - { - "secs": 0, - "nanos": 19542 - }, - { - "secs": 0, - "nanos": 20500 - }, - { - "secs": 0, - "nanos": 54667 - }, - { - "secs": 0, - "nanos": 16792 - }, - { - "secs": 0, - "nanos": 29333 - }, - { - "secs": 0, - "nanos": 17000 - }, - { - "secs": 0, - "nanos": 24709 - }, - { - "secs": 0, - "nanos": 29416 - }, - { - "secs": 0, - "nanos": 10583 - }, - { - "secs": 0, - "nanos": 18375 - }, - { - "secs": 0, - "nanos": 31292 - }, - { - "secs": 0, - "nanos": 18792 - }, - { - "secs": 0, - "nanos": 18792 - }, - { - "secs": 0, - "nanos": 19875 - }, - { - "secs": 0, - "nanos": 10167 - }, - { - "secs": 0, - "nanos": 17250 - }, - { - "secs": 0, - "nanos": 18167 - }, - { - "secs": 0, - "nanos": 18125 - }, - { - "secs": 0, - "nanos": 96542 - }, - { - "secs": 0, - "nanos": 30667 - }, - { - "secs": 0, - "nanos": 30459 - }, - { - "secs": 0, - "nanos": 20708 - }, - { - "secs": 0, - "nanos": 38667 - }, - { - "secs": 0, - "nanos": 23167 - }, - { - "secs": 0, - "nanos": 17875 - }, - { - "secs": 0, - "nanos": 17291 - }, - { - "secs": 0, - "nanos": 16917 - }, - { - "secs": 0, - "nanos": 32041 - }, - { - "secs": 0, - "nanos": 16083 - }, - { - "secs": 0, - "nanos": 18875 - }, - { - "secs": 0, - "nanos": 42375 - }, - { - "secs": 0, - "nanos": 18417 - }, - { - "secs": 0, - "nanos": 13459 - }, - { - "secs": 0, - "nanos": 833 - }, - { - "secs": 0, - "nanos": 417 - } - ], - "user_prompt_length": 0, - "response_size": 1287, - "chat_conversation_type": "NotToolUse", - "tool_use_ids_and_names": [], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - }, - { - "user": { - "additional_context": "", - "env_context": { - "env_state": { - "operating_system": "macos", - "current_working_directory": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python", - "environment_variables": [] - } - }, - "content": { - "Prompt": { - "prompt": "Thanks. This is how a basic put and get works with AmazonS3EncryptionClientV2: using Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\nusing Amazon.S3;\nusing Amazon.S3.Model;\n\nvar encryptionContext = new Dictionary();\nvar encryptionMaterial =\n new EncryptionMaterialsV2(Environment.GetEnvironmentVariable(\"key1\"), KmsType.KmsContext, encryptionContext);\n\nvar encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n\nIAmazonS3 encryptionClient = new AmazonS3Client();\nvar putContext = \"abc\";\nvar bucketName = \"testwiththisbucket\";\nvar key = \"test.txt\";\n\n// Put object\nvar putRequest = new PutObjectRequest\n{\n BucketName = bucketName,\n Key = key,\n ContentBody = putContext\n};\n\nawait encryptionClient.PutObjectAsync(putRequest);\n\nvar getRequest = new GetObjectRequest\n{\n BucketName = bucketName,\n Key = key\n};\n\nvar response = await encryptionClient.GetObjectAsync(getRequest);\nusing var reader = new StreamReader(response.ResponseStream);\nvar retrievedContent = await reader.ReadToEndAsync();\n\nConsole.WriteLine($\"Retrieved: {retrievedContent}\");" - } - }, - "timestamp": "2025-09-15T16:08:26.102193-07:00", - "images": null - }, - "assistant": { - "ToolUse": { - "message_id": "f12c3aea-fbc5-4186-96c8-3b2f181c6cf6", - "content": "Perfect! Now I can create the correct .NET controllers using the proper v3 API. Let me create the ClientController first:", - "tool_uses": [ - { - "id": "tooluse_optyE9P-TWeP7pg561V3Rg", - "name": "fs_write", - "orig_name": "fs_write", - "args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", - "command": "create", - "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionContext = new Dictionary();\n \n // Create encryption materials\n var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext);\n \n // Create S3 encryption client\n var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy);\n var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" - }, - "orig_args": { - "path": "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Controllers/ClientController.cs", - "command": "create", - "file_text": "using Microsoft.AspNetCore.Mvc;\nusing NetV3Server.Models;\nusing NetV3Server.Services;\nusing Amazon.S3;\nusing Amazon.Extensions.S3.Encryption;\nusing Amazon.Extensions.S3.Encryption.Primitives;\n\nnamespace NetV3Server.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ClientController : ControllerBase\n{\n private readonly IClientCacheService _clientCacheService;\n\n public ClientController(IClientCacheService clientCacheService)\n {\n _clientCacheService = clientCacheService;\n }\n\n [HttpPost]\n public async Task CreateClient([FromBody] ClientRequest request)\n {\n try\n {\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n var encryptionContext = new Dictionary();\n \n // Create encryption materials\n var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext);\n \n // Create S3 encryption client\n var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2AndLegacy);\n var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);\n \n // Add to cache and return client ID\n var clientId = _clientCacheService.AddClient(encryptionClient);\n \n return Ok(new ClientResponse { ClientId = clientId });\n }\n catch (Exception ex)\n {\n return StatusCode(500, new S3EncryptionClientError \n { \n Message = $\"Failed to create client: {ex.Message}\" \n });\n }\n }\n}" - } - } - ] - } - }, - "request_metadata": { - "request_id": "58e3d75f-d1e2-4202-af17-bf29432acfd2", - "message_id": "f12c3aea-fbc5-4186-96c8-3b2f181c6cf6", - "request_start_timestamp_ms": 1757977706106, - "stream_end_timestamp_ms": 1757977716903, - "time_to_first_chunk": { - "secs": 4, - "nanos": 978817000 - }, - "time_between_chunks": [ - { - "secs": 0, - "nanos": 37917 - }, - { - "secs": 0, - "nanos": 24083 - }, - { - "secs": 0, - "nanos": 42745292 - }, - { - "secs": 0, - "nanos": 48322542 - }, - { - "secs": 0, - "nanos": 66440042 - }, - { - "secs": 0, - "nanos": 48699584 - }, - { - "secs": 0, - "nanos": 57406000 - }, - { - "secs": 0, - "nanos": 53901250 - }, - { - "secs": 0, - "nanos": 54092125 - }, - { - "secs": 0, - "nanos": 56850125 - }, - { - "secs": 0, - "nanos": 91681000 - }, - { - "secs": 0, - "nanos": 53761708 - }, - { - "secs": 0, - "nanos": 59511250 - }, - { - "secs": 0, - "nanos": 52875625 - }, - { - "secs": 0, - "nanos": 51298667 - }, - { - "secs": 0, - "nanos": 283457750 - }, - { - "secs": 0, - "nanos": 463286042 - }, - { - "secs": 0, - "nanos": 18875 - }, - { - "secs": 0, - "nanos": 776416 - }, - { - "secs": 0, - "nanos": 32084 - }, - { - "secs": 0, - "nanos": 1251375 - }, - { - "secs": 0, - "nanos": 338375 - }, - { - "secs": 0, - "nanos": 57833 - }, - { - "secs": 0, - "nanos": 1238042 - }, - { - "secs": 0, - "nanos": 807875 - }, - { - "secs": 0, - "nanos": 48167 - }, - { - "secs": 0, - "nanos": 17834 - }, - { - "secs": 0, - "nanos": 55894083 - }, - { - "secs": 0, - "nanos": 12208 - }, - { - "secs": 0, - "nanos": 4667 - }, - { - "secs": 0, - "nanos": 3958 - }, - { - "secs": 0, - "nanos": 3958 - }, - { - "secs": 0, - "nanos": 8084 - }, - { - "secs": 0, - "nanos": 4000 - }, - { - "secs": 0, - "nanos": 3750 - }, - { - "secs": 0, - "nanos": 3625 - }, - { - "secs": 0, - "nanos": 6625 - }, - { - "secs": 0, - "nanos": 3667 - }, - { - "secs": 0, - "nanos": 5542 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 2917 - }, - { - "secs": 0, - "nanos": 107114125 - }, - { - "secs": 0, - "nanos": 17542 - }, - { - "secs": 0, - "nanos": 7333 - }, - { - "secs": 0, - "nanos": 6292 - }, - { - "secs": 3, - "nanos": 588082541 - }, - { - "secs": 0, - "nanos": 5625667 - }, - { - "secs": 0, - "nanos": 27279000 - }, - { - "secs": 0, - "nanos": 31656291 - }, - { - "secs": 0, - "nanos": 34590667 - }, - { - "secs": 0, - "nanos": 17964542 - }, - { - "secs": 0, - "nanos": 26545792 - }, - { - "secs": 0, - "nanos": 35439625 - }, - { - "secs": 0, - "nanos": 37886625 - }, - { - "secs": 0, - "nanos": 35373417 - }, - { - "secs": 0, - "nanos": 14934666 - }, - { - "secs": 0, - "nanos": 27312875 - }, - { - "secs": 0, - "nanos": 31947292 - }, - { - "secs": 0, - "nanos": 30889458 - }, - { - "secs": 0, - "nanos": 47385583 - }, - { - "secs": 0, - "nanos": 31048834 - }, - { - "secs": 0, - "nanos": 30953833 - }, - { - "secs": 0, - "nanos": 20183709 - }, - { - "secs": 0, - "nanos": 14417 - }, - { - "secs": 0, - "nanos": 12899667 - }, - { - "secs": 0, - "nanos": 6667834 - }, - { - "secs": 0, - "nanos": 1319083 - }, - { - "secs": 0, - "nanos": 1288541 - }, - { - "secs": 0, - "nanos": 2570959 - }, - { - "secs": 0, - "nanos": 17875 - }, - { - "secs": 0, - "nanos": 1762083 - }, - { - "secs": 0, - "nanos": 20917 - }, - { - "secs": 0, - "nanos": 2419125 - }, - { - "secs": 0, - "nanos": 15083 - }, - { - "secs": 0, - "nanos": 5667 - }, - { - "secs": 0, - "nanos": 4708 - }, - { - "secs": 0, - "nanos": 5167 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 2296458 - }, - { - "secs": 0, - "nanos": 16208 - }, - { - "secs": 0, - "nanos": 5709 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 42333 - }, - { - "secs": 0, - "nanos": 32375 - }, - { - "secs": 0, - "nanos": 10693833 - }, - { - "secs": 0, - "nanos": 312167 - }, - { - "secs": 0, - "nanos": 8041 - }, - { - "secs": 0, - "nanos": 5625 - }, - { - "secs": 0, - "nanos": 5417 - }, - { - "secs": 0, - "nanos": 1233959 - }, - { - "secs": 0, - "nanos": 17186333 - }, - { - "secs": 0, - "nanos": 33208 - }, - { - "secs": 0, - "nanos": 6542 - }, - { - "secs": 0, - "nanos": 48375 - }, - { - "secs": 0, - "nanos": 8250 - }, - { - "secs": 0, - "nanos": 5667 - }, - { - "secs": 0, - "nanos": 5333 - }, - { - "secs": 0, - "nanos": 4833 - }, - { - "secs": 0, - "nanos": 4791 - }, - { - "secs": 0, - "nanos": 4500 - }, - { - "secs": 0, - "nanos": 12958 - }, - { - "secs": 0, - "nanos": 7291 - }, - { - "secs": 0, - "nanos": 3875 - }, - { - "secs": 0, - "nanos": 4000 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 3458 - }, - { - "secs": 0, - "nanos": 3125 - }, - { - "secs": 0, - "nanos": 5375 - }, - { - "secs": 0, - "nanos": 5000 - }, - { - "secs": 0, - "nanos": 9083 - }, - { - "secs": 0, - "nanos": 320500 - }, - { - "secs": 0, - "nanos": 8875 - }, - { - "secs": 0, - "nanos": 4875 - }, - { - "secs": 0, - "nanos": 8042 - }, - { - "secs": 0, - "nanos": 4959 - }, - { - "secs": 0, - "nanos": 7166 - }, - { - "secs": 0, - "nanos": 10916 - }, - { - "secs": 0, - "nanos": 12158125 - }, - { - "secs": 0, - "nanos": 7709 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 1346917 - }, - { - "secs": 0, - "nanos": 3542 - }, - { - "secs": 0, - "nanos": 15834 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 1541 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 4405458 - }, - { - "secs": 0, - "nanos": 6500 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1666 - }, - { - "secs": 0, - "nanos": 1666 - }, - { - "secs": 0, - "nanos": 1672834 - }, - { - "secs": 0, - "nanos": 4125 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1417 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 6458 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 1296083 - }, - { - "secs": 0, - "nanos": 4375 - }, - { - "secs": 0, - "nanos": 1666 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1125 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 8541 - }, - { - "secs": 0, - "nanos": 4916 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1167 - }, - { - "secs": 0, - "nanos": 833 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1208 - }, - { - "secs": 0, - "nanos": 3834 - }, - { - "secs": 0, - "nanos": 1791 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 4000 - }, - { - "secs": 0, - "nanos": 959 - }, - { - "secs": 0, - "nanos": 7458 - }, - { - "secs": 0, - "nanos": 2741958 - }, - { - "secs": 0, - "nanos": 5125 - }, - { - "secs": 0, - "nanos": 1292 - }, - { - "secs": 0, - "nanos": 2250 - }, - { - "secs": 0, - "nanos": 1958 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 959 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1833 - }, - { - "secs": 0, - "nanos": 1709 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1000 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 1375 - }, - { - "secs": 0, - "nanos": 8625 - }, - { - "secs": 0, - "nanos": 3584 - }, - { - "secs": 0, - "nanos": 1250 - }, - { - "secs": 0, - "nanos": 1328750 - }, - { - "secs": 0, - "nanos": 3667 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 916 - }, - { - "secs": 0, - "nanos": 7833 - }, - { - "secs": 0, - "nanos": 1541 - }, - { - "secs": 0, - "nanos": 2709 - }, - { - "secs": 0, - "nanos": 1791 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 4291 - }, - { - "secs": 0, - "nanos": 3333 - }, - { - "secs": 0, - "nanos": 3000 - }, - { - "secs": 0, - "nanos": 1916 - }, - { - "secs": 0, - "nanos": 1125 - }, - { - "secs": 0, - "nanos": 1416 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 1584 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1375 - }, - { - "secs": 0, - "nanos": 18000 - }, - { - "secs": 0, - "nanos": 3542 - }, - { - "secs": 0, - "nanos": 791 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1459 - }, - { - "secs": 0, - "nanos": 1583 - }, - { - "secs": 0, - "nanos": 958 - }, - { - "secs": 0, - "nanos": 3209 - }, - { - "secs": 0, - "nanos": 1416 - }, - { - "secs": 0, - "nanos": 1417 - }, - { - "secs": 0, - "nanos": 1500 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 958 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 2631958 - }, - { - "secs": 0, - "nanos": 6959 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 4250 - }, - { - "secs": 0, - "nanos": 2416 - }, - { - "secs": 0, - "nanos": 1458 - }, - { - "secs": 0, - "nanos": 2084 - }, - { - "secs": 0, - "nanos": 2375 - }, - { - "secs": 0, - "nanos": 3750 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 3084 - }, - { - "secs": 0, - "nanos": 2209 - }, - { - "secs": 0, - "nanos": 2916 - }, - { - "secs": 0, - "nanos": 1709 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 2083 - }, - { - "secs": 0, - "nanos": 1125 - }, - { - "secs": 0, - "nanos": 10250 - }, - { - "secs": 0, - "nanos": 1166 - }, - { - "secs": 0, - "nanos": 1667 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1750 - }, - { - "secs": 0, - "nanos": 1708 - }, - { - "secs": 0, - "nanos": 7000 - }, - { - "secs": 0, - "nanos": 3458 - }, - { - "secs": 0, - "nanos": 1416 - }, - { - "secs": 0, - "nanos": 1167 - }, - { - "secs": 0, - "nanos": 3750 - }, - { - "secs": 0, - "nanos": 1209 - }, - { - "secs": 0, - "nanos": 3458 - }, - { - "secs": 0, - "nanos": 1084 - }, - { - "secs": 0, - "nanos": 791 - }, - { - "secs": 0, - "nanos": 12625 - }, - { - "secs": 0, - "nanos": 4583 - }, - { - "secs": 0, - "nanos": 875 - }, - { - "secs": 0, - "nanos": 792 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1334 - }, - { - "secs": 0, - "nanos": 1042 - }, - { - "secs": 0, - "nanos": 56542 - }, - { - "secs": 0, - "nanos": 834 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 2167 - }, - { - "secs": 0, - "nanos": 750 - }, - { - "secs": 0, - "nanos": 708 - }, - { - "secs": 0, - "nanos": 791 - }, - { - "secs": 0, - "nanos": 10750 - }, - { - "secs": 0, - "nanos": 958 - }, - { - "secs": 0, - "nanos": 1792 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 1542 - }, - { - "secs": 0, - "nanos": 1000 - }, - { - "secs": 0, - "nanos": 916 - }, - { - "secs": 0, - "nanos": 875 - }, - { - "secs": 0, - "nanos": 1875 - }, - { - "secs": 0, - "nanos": 1000 - }, - { - "secs": 0, - "nanos": 5042 - }, - { - "secs": 0, - "nanos": 875 - }, - { - "secs": 0, - "nanos": 1625 - }, - { - "secs": 0, - "nanos": 957417 - }, - { - "secs": 0, - "nanos": 1291 - }, - { - "secs": 0, - "nanos": 4458 - }, - { - "secs": 0, - "nanos": 1208 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 1834 - }, - { - "secs": 0, - "nanos": 2208 - }, - { - "secs": 0, - "nanos": 12166 - }, - { - "secs": 0, - "nanos": 83 - } - ], - "user_prompt_length": 1298, - "response_size": 2019, - "chat_conversation_type": "ToolUse", - "tool_use_ids_and_names": [ - [ - "tooluse_optyE9P-TWeP7pg561V3Rg", - "fs_write" - ] - ], - "model_id": "claude-sonnet-4", - "message_meta_tags": [] - } - } - ], - "valid_history_range": [ - 0, - 50 - ], - "transcript": [ - "> /load", - "> read /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/python-server. I want to to know what this test server does. src/main.py is the starting point of the program. I want to to know what this test server does. src/main.py is the starting point of the program.", - "I'll read the test server directory and examine the main.py file to understand what this test server does.\n[Tool uses: fs_read]", - "\n[Tool uses: fs_read]", - "\n[Tool uses: fs_read]", - "\n[Tool uses: fs_read]", - "This test server is a **FastAPI-based HTTP server that provides a REST API wrapper around the S3 Encryption Client**. Here's what it does:\n\n## Purpose\nThe server acts as a test harness for the Amazon S3 Encryption Client Python library, allowing HTTP-based testing of client-side encryption functionality.\n\n## Key Functionality\n\n**Client Management:**\n- `POST /client` - Creates S3EncryptionClient instances with KMS keyring configuration\n- Stores clients in memory cache with UUID identifiers\n- Supports legacy wrapping algorithms configuration\n\n**S3 Operations:**\n- `PUT /object/{bucket}/{key}` - Encrypts and uploads objects to S3\n- `GET /object/{bucket}/{key}` - Downloads and decrypts objects from S3\n- Handles encryption context via `Content-Metadata` headers\n\n**Architecture:**\n- Uses FastAPI framework running on localhost:8081\n- Integrates with AWS KMS for key management (hardcoded to us-west-2 region)\n- Converts metadata between string format and dictionary for encryption context\n- Provides structured error responses matching Smithy model types\n\nThe server essentially allows you to test S3 client-side encryption operations through HTTP requests rather than direct Python API calls, making it useful for integration testing or cross-language compatibility testing.\n[Tool uses: none]", - "> /save", - "> /save .", - "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/qSaved", - "> Now, I want to start writing similar implementation but in .Net. I have already created net-v3-server where I plan to test S3-EC net v3 like python. Don't write any code but I want to prepare first. where do you think I should start? I don't know dotnet much, how should project structure look like?", - "Based on the Python test server structure, here's how you should approach the .NET implementation:\n\n## Project Structure for .NET\n\n```\nnet-v3-server/\n├── src/\n│ ├── NetV3Server/\n│ │ ├── Controllers/\n│ │ │ ├── ClientController.cs\n│ │ │ └── ObjectController.cs\n│ │ ├── Models/\n│ │ │ ├── ClientRequest.cs\n│ │ │ ├── ClientResponse.cs\n│ │ │ └── ErrorModels.cs\n│ │ ├── Services/\n│ │ │ └── ClientCacheService.cs\n│ │ ├── Program.cs\n│ │ └── NetV3Server.csproj\n├── tests/\n│ └── NetV3Server.Tests/\n│ └── NetV3Server.Tests.csproj\n├── NetV3Server.sln\n└── README.md\n```\n\n## Where to Start\n\n**1. Choose Framework:**\n- Use **ASP.NET Core Web API** (equivalent to FastAPI)\n- Target .NET 6+ for modern features\n\n**2. Key Dependencies You'll Need:**\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK\n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n- `Microsoft.AspNetCore.Mvc` - Web API framework\n\n**3. Core Components to Implement:**\n- **Program.cs** - Entry point (like main.py)\n- **Controllers** - Handle HTTP endpoints (`/client`, `/object/{bucket}/{key}`)\n- **ClientCacheService** - In-memory client storage (like client_cache dict)\n- **Models** - Request/response DTOs\n- **Error handling** - Custom exception responses\n\n**4. Learning Path:**\n1. Start with basic ASP.NET Core Web API tutorial\n2. Learn dependency injection (built into .NET)\n3. Understand controllers and routing\n4. Study AWS SDK for .NET usage patterns\n\n**5. Initial Setup Steps:**\n1. Install .NET SDK\n2. Create solution: `dotnet new sln -n NetV3Server`\n3. Create web API project: `dotnet new webapi -n NetV3Server`\n4. Add AWS NuGet packages\n5. Set up basic controller structure\n\nThe .NET version will be more structured than Python due to strong typing and dependency injection, but the core logic flow will mirror your Python implementation exactly.\n[Tool uses: none]", - "> How do I create such directory structure?", - "I'll help you create the .NET project structure. Let me check your current directory first and then create the structure.\n[Tool uses: execute_bash]", - "\n[Tool uses: fs_read]", - "\n[Tool uses: fs_read]", - "I see you already have a `net-v3-server` directory. Let me check what's in it and then create the proper .NET structure:\n[Tool uses: fs_read]", - "Now I'll create the proper .NET project structure in your existing `net-v3-server` directory:\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> Why would you need a test?", - "You're right to question that. For your immediate goal of creating a test server similar to the Python one, you **don't need tests**.\n\nThe test project was just following .NET conventions, but since you're building a test server (which itself is a testing tool), adding unit tests would be overkill and unnecessary complexity.\n\nLet me clean up and show you the minimal structure you actually need:\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: execute_bash]", - "> y", - "\n[Tool uses: fs_read]", - "Perfect! Now you have a clean, minimal .NET project structure:\n\n```\nnet-v3-server/\n├── NetV3Server.sln # Solution file\n├── src/NetV3Server/\n│ ├── Controllers/ # Your API endpoints (empty, ready for you)\n│ ├── Models/ # Request/response models (empty, ready for you) \n│ ├── Services/ # Business logic like ClientCacheService (empty, ready for you)\n│ ├── Program.cs # Entry point (like main.py)\n│ ├── NetV3Server.csproj # Project dependencies\n│ └── appsettings.json # Configuration\n└── README.md\n```\n\nYou're right - no tests needed. This test server **is** the test. Just like the Python version, it's a tool for testing the S3 encryption client, not something that needs its own unit tests.\n\nNow you can start implementing the controllers and services to mirror the Python FastAPI functionality.\n[Tool uses: none]", - "> What is this bin, obj, properties, appsetting.json, appsettings.Development.json, other .csproj and http files?", - "These are standard .NET project files - here's what each does:\n\n## Files You'll Work With:\n- **Program.cs** - Your main entry point (like main.py)\n- **NetV3Server.csproj** - Project file listing dependencies (like pyproject.toml)\n- **appsettings.json** - Configuration (ports, connection strings, etc.)\n\n## Files You Can Ignore:\n- **bin/** - Compiled output (like Python's __pycache__)\n- **obj/** - Build artifacts and temporary files\n- **Properties/launchSettings.json** - IDE debugging settings\n- **appsettings.Development.json** - Dev-only config overrides\n- **NetV3Server.http** - Sample HTTP requests for testing\n\n## What to Focus On:\n1. **Program.cs** - Configure your web server and routes\n2. **Controllers/** - Your API endpoints (`/client`, `/object/{bucket}/{key}`)\n3. **Models/** - Request/response classes\n4. **Services/** - Business logic (client cache)\n5. **NetV3Server.csproj** - Add AWS SDK packages here\n\nThe `bin` and `obj` folders are like Python's virtual environment artifacts - generated during build, safe to delete, and should be in `.gitignore`.\n\nThink of it this way:\n- **bin/obj** = Python's `.venv` and `__pycache__`\n- **appsettings.json** = Your config file\n- **Program.cs** = Your `main.py`\n- **.csproj** = Your `pyproject.toml`\n\nYou only need to touch the first 5 items I mentioned.\n[Tool uses: none]", - "> So, what is the next steps?", - "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nAdd the S3 encryption client and AWS SDK packages to your project:\n[Tool uses: execute_bash]", - "> Can you list them first?", - "Here are your next steps in order:\n\n## 1. Add AWS Dependencies\nYou'll need these NuGet packages:\n- `AWSSDK.S3` - AWS S3 SDK\n- `AWSSDK.KeyManagementService` - AWS KMS SDK \n- `AWS.S3.EncryptionClientV3` - The S3 encryption client library\n\n## 2. Configure the Server\n- Set port to 8081 in `appsettings.json` (to match Python server)\n- Configure dependency injection in `Program.cs`\n\n## 3. Create Models\n- `ClientRequest.cs` - For POST /client requests\n- `ClientResponse.cs` - For client creation responses\n- `ErrorModels.cs` - For error responses\n\n## 4. Create Services\n- `ClientCacheService.cs` - In-memory client storage (like Python's client_cache dict)\n\n## 5. Create Controllers\n- `ClientController.cs` - Handle POST /client\n- `ObjectController.cs` - Handle PUT/GET /object/{bucket}/{key}\n\n## 6. Test\n- Run the server and verify it starts on port 8081\n- Test endpoints match Python server behavior\n\nWant me to help with step 1 (adding the packages) first?\n[Tool uses: none]", - "> Before doing this can I have a gitignore? Where should I put this?", - "You should put the `.gitignore` file in the root of your `net-v3-server` directory (same level as the `.sln` file).\n[Tool uses: fs_write]", - "> Question: What is difference between this and gitignore in the root?", - "Good question! Here's the difference:\n\n## Root `.gitignore` (python repo level)\n- Located at `/amazon-s3-encryption-client-python/.gitignore`\n- Ignores files for the **entire Python repository**\n- Already exists and handles Python-specific files (`.pyenv`, `__pycache__`, etc.)\n\n## Net server `.gitignore` (net-v3-server level) \n- Would be at `/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore`\n- Ignores files **only for the .NET project**\n- Handles .NET-specific files (`bin/`, `obj/`, `.vs/`, etc.)\n\n## Why You Need Both:\n- Root ignores Python stuff globally\n- Net server ignores .NET stuff locally\n- Git processes `.gitignore` files hierarchically - it reads both\n\n## Alternative:\nYou could add .NET ignore patterns to the root `.gitignore` instead:\n```\n# Add to root .gitignore\ntest-server/net-v3-server/bin/\ntest-server/net-v3-server/obj/\ntest-server/net-v3-server/.vs/\n```\n\n**Recommendation:** Create a local `.gitignore` in `net-v3-server/` - it's cleaner and keeps .NET concerns separate from Python concerns.\n[Tool uses: none]", - "> Sure. Let's create gitignore in net-v3-server", - "\n[Tool uses: fs_write]", - "> y", - "Perfect! Now you have a `.gitignore` file in your `net-v3-server` directory that will ignore all the .NET build artifacts (`bin/`, `obj/`), IDE files, and other temporary files.\n\nReady to move to step 1 - adding the AWS dependencies?\n[Tool uses: none]", - "> The only more dependency needed is dotnet add package Amazon.Extensions.S3.Encryption --version 3.0.0", - "Perfect! Let's add that dependency:\n[Tool uses: execute_bash]", - "> y", - "Great! The S3 encryption client package has been added. This package includes all the AWS SDK dependencies you need (S3, KMS, etc.).\n\nLet's verify it was added to your project file:\n[Tool uses: fs_read]", - "Perfect! Now you're ready for step 2 - configuring the server. What's next:\n\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. **Configure server** - Set port to 8081 in appsettings.json\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service\n5. **Create controllers** - API endpoints\n\nReady to configure the server port?\n[Tool uses: none]", - "> Set port to 8084", - "\n[Tool uses: fs_read]", - "\n[Tool uses: fs_write]", - "> y", - "\n[Tool uses: fs_write]", - "> y", - "Perfect! Server is now configured to run on port 8084. \n\nNext steps:\n1. ✅ **Dependencies added** - Amazon.Extensions.S3.Encryption v3.0.0\n2. ✅ **Server configured** - Port set to 8084\n3. **Create models** - Request/response classes\n4. **Create services** - Client cache service \n5. **Create controllers** - API endpoints\n\nReady to create the models next?\n[Tool uses: none]", - "> Yes. What should I be doing for this?", - "Based on the Python server, you need these models to handle the JSON requests/responses:\n[Tool uses: fs_write]", - "> Can you explain what this is?", - "These models represent the JSON data structures that your .NET server will receive and send, matching what the Python server handles.\n\n## What These Models Do:\n\n**ClientRequest** - Represents the JSON sent to `POST /client`:\n```json\n{\n \"config\": {\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012\"\n },\n \"enableLegacyWrappingAlgorithms\": false\n }\n}\n```\n\n## Why You Need Them:\n\nIn the Python server, this JSON gets parsed manually:\n```python\nparsed_data = json.loads(body.decode(\"utf-8\"))\nconfig_data = parsed_data.get(\"config\", {})\nkey_material = config_data.get(\"keyMaterial\", {})\nkms_key_id = key_material[\"kmsKeyId\"]\n```\n\nIn .NET, these classes automatically convert JSON to strongly-typed objects:\n```csharp\n// .NET automatically converts JSON to ClientRequest object\npublic async Task CreateClient([FromBody] ClientRequest request)\n{\n var kmsKeyId = request.Config.KeyMaterial.KmsKeyId;\n // ...\n}\n```\n\n## The Pattern:\n- **ClientRequest** = What comes IN to your API\n- **ClientResponse** = What goes OUT from your API \n- **ErrorModels** = Error responses\n\nThis gives you type safety and automatic JSON serialization/deserialization instead of manual parsing.\n[Tool uses: none]", - "> These are example of request/responses. CreateClient:> \n> \n[DEBUG] HTTP Method: POST> \n[DEBUG] Endpoint Path: /client> \n[DEBUG] Request Headers: {'content-length': '234', 'host': 'localhost:8081', 'content-type': 'application/json', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: b'{\"config\":{\"enableLegacyUnauthenticatedModes\":false,\"enableDelayedAuthenticationMode\":false,\"enableLegacyWrappingAlgorithms\":false,\"keyMaterial\":{\"kmsKeyId\":\"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"}}}'> \n> \nGetObject: > \n> \n[DEBUG] HTTP Method: GET> \n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python> \n[DEBUG] Request Headers: {'content-length': '0', 'host': 'localhost:8081', 'clientid': 'ff529adc-bc33-4db9-9309-9086d09f62d4', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: (no body for GET request)> \nINFO: 127.0.0.1:52340 - \"GET /object/s3ec-test-server-github-bucket/test-key-kms-v1-Python HTTP/1.1\" 200 OK> \n> \nMore generally, the endpoint path is /object/$BUCKET/$KEY and there is no HTTP body. The client-id is in the request headers. > \nPutObject:> \n> \n[DEBUG] HTTP Method: PUT> \n[DEBUG] Endpoint Path: /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python> \n[DEBUG] Request Headers: {'content-length': '17', 'host': 'localhost:8081', 'clientid': '4c10da8d-4b2e-4a91-a0bb-5ba2c658ebe4', 'content-metadata': '[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]', 'content-type': 'application/octet-stream', 'user-agent': 'smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2'}> \n[DEBUG] Raw Request Data: b'simple-test-input'> \nINFO: 127.0.0.1:52330 - \"PUT /object/s3ec-test-server-github-bucket/cross-lang-test-key-kms-ec-Python HTTP/1.1\" 200 OK> \n> \n", - "Perfect! Now I can see the exact request/response patterns. Let me create the correct models:\n[Tool uses: fs_write]", - "> y", - "\n[Tool uses: fs_write]", - "> y", - "\n[Tool uses: fs_write]", - "> y", - "Now you have the exact models matching your request/response patterns:\n\n## What Each Model Handles:\n\n**ClientRequest** - Handles the POST /client JSON:\n```json\n{\n \"config\": {\n \"enableLegacyUnauthenticatedModes\": false,\n \"enableDelayedAuthenticationMode\": false, \n \"enableLegacyWrappingAlgorithms\": false,\n \"keyMaterial\": {\n \"kmsKeyId\": \"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key\"\n }\n }\n}\n```\n\n**ClientResponse** - Returns the client ID:\n```json\n{\n \"clientId\": \"ff529adc-bc33-4db9-9309-9086d09f62d4\"\n}\n```\n\n**Error Models** - Match Python server's error format with `__type` field\n\n**Note:** For GET/PUT `/object/{bucket}/{key}`:\n- No special models needed - use route parameters and headers\n- `clientid` comes from request header\n- `content-metadata` comes from request header \n- Body is raw bytes\n\nReady for step 4 - creating the client cache service?\n[Tool uses: none]", - "> yes", - "\n[Tool uses: fs_write]", - "> y", - "Perfect! This service replicates the Python server's `client_cache` dictionary:\n\n## What This Service Does:\n\n**Replaces Python's:**\n```python\nclient_cache = {} # Dictionary to store clients with UUIDs as keys\n\n# Add client\nclient_id = str(uuid.uuid4())\nclient_cache[client_id] = client\n\n# Get client \nclient = client_cache.get(client_id)\n```\n\n**With .NET's:**\n```csharp\n// Add client\nstring clientId = _clientCacheService.AddClient(client);\n\n// Get client\nvar client = _clientCacheService.GetClient(clientId);\n```\n\n## Key Features:\n- **Thread-safe** - Uses `ConcurrentDictionary` (multiple requests can access safely)\n- **Interface** - `IClientCacheService` for dependency injection\n- **UUID generation** - `Guid.NewGuid()` like Python's `uuid.uuid4()`\n- **Null handling** - Returns `null` if client not found\n\nNext step: Register this service in `Program.cs` and create the controllers. Ready?\n[Tool uses: none]", - "> yes", - "Let's first register the service in Program.cs, then create the controllers:\n[Tool uses: fs_read]", - "\n[Tool uses: fs_write]", - "> y", - "Now let's create the ClientController:\n[Tool uses: fs_write]", - "> Can you point me to this code in python?", - "Sure! Here's the corresponding Python code from the test server:\n[Tool uses: fs_read]", - "\n[Tool uses: fs_read]", - "Here's the Python code that corresponds to what I was trying to implement in .NET:\n\n## Python Client Creation (lines 160-190):\n\n```python\n@app.post(\"/client\")\nasync def client_endpoint(request: Request):\n # Parse JSON request\n parsed_data = json.loads(body.decode(\"utf-8\"))\n config_data = parsed_data.get(\"config\", {})\n key_material = config_data.get(\"keyMaterial\", {})\n \n enable_legacy_wrapping_algorithms = config_data.get(\"enableLegacyWrappingAlgorithms\", False)\n \n # Create KMS client (hardcoded us-west-2)\n kms_client = boto3.client(\"kms\", region_name=\"us-west-2\")\n kms_key_id = key_material[\"kmsKeyId\"]\n \n # Create keyring and S3 encryption client\n keyring = KmsKeyring(\n kms_client,\n kms_key_id=kms_key_id,\n enable_legacy_wrapping_algorithms=enable_legacy_wrapping_algorithms,\n )\n wrapped_client = boto3.client(\"s3\")\n client_config = S3EncryptionClientConfig(keyring)\n client = S3EncryptionClient(wrapped_client, client_config)\n \n # Generate UUID and cache client\n client_id = str(uuid.uuid4())\n client_cache[client_id] = client\n \n return {\"clientId\": client_id}\n```\n\nThe .NET version needs to do the same thing but with the Amazon.Extensions.S3.Encryption v3 API instead of the Python API.\n[Tool uses: none]", - "> Thanks. This is how a basic put and get works with AmazonS3EncryptionClientV2: using Amazon.Extensions.S3.Encryption;> \nusing Amazon.Extensions.S3.Encryption.Primitives;> \nusing Amazon.S3;> \nusing Amazon.S3.Model;> \n> \nvar encryptionContext = new Dictionary();> \nvar encryptionMaterial => \n new EncryptionMaterialsV2(Environment.GetEnvironmentVariable(\"key1\"), KmsType.KmsContext, encryptionContext);> \n> \nvar encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial);> \n> \nIAmazonS3 encryptionClient = new AmazonS3Client();> \nvar putContext = \"abc\";> \nvar bucketName = \"testwiththisbucket\";> \nvar key = \"test.txt\";> \n> \n// Put object> \nvar putRequest = new PutObjectRequest> \n{> \n BucketName = bucketName,> \n Key = key,> \n ContentBody = putContext> \n};> \n> \nawait encryptionClient.PutObjectAsync(putRequest);> \n> \nvar getRequest = new GetObjectRequest> \n{> \n BucketName = bucketName,> \n Key = key> \n};> \n> \nvar response = await encryptionClient.GetObjectAsync(getRequest);> \nusing var reader = new StreamReader(response.ResponseStream);> \nvar retrievedContent = await reader.ReadToEndAsync();> \n> \nConsole.WriteLine($\"Retrieved: {retrievedContent}\");", - "Perfect! Now I can create the correct .NET controllers using the proper v3 API. Let me create the ClientController first:\n[Tool uses: fs_write]", - "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json", - "> /save /Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.temp/qSaved.json -f" - ], - "tools": { - "builder-mcp": [ - { - "ToolSpecification": { - "name": "ReadInternalWebsites", - "description": "Reads content from internal Amazon websites (amazon.com amazon.dev a2z.com aws.dev TLDs)\n### User Alias Handling\n\nThe terms alias and login are often used interchangeably\nMany payloads may contain an alias/user handle of an employee. ALWAYS use phonetool.amazon.com to lookup a real name from an alias IF necessary, NEVER guess a name from an alias based on alias appearance\n\n### Ticket Severity\nTickets at Amazon have severity (SEV) from 1-5, 1 and 2 are the most severe and pages resolvers 2.5 is a business hours sev 2. 3 – Group productivity impaired 4 – Individual productivity impaired 5 – Productivity not immediately affected\n\n### Website Details\ndocs.hub.amazon.dev internal technical documentation\nbtdocs.builder-tools.aws.dev BuilderHub contributor documentation\nbroadcast.amazon.com internal videos, transcripts and captions for company communications and events\nskb.highcastle.a2z has internal security knowledge base docs for secure implementations\ndocs.aws.amazon.com hosts external AWS documentation\naax-console.amazon.com hosts AAX Console for Amazon Advertising Exchange (AAX). Features include settings management (sources, publishers, GDPR), business analytics, testing tools (XTF), operations monitoring, and configuration management for exchange, bidders, and traffic\nmeridian.a2z.com hosts Meridian design system documentation: components, guides, patterns, etc Version selection via ?version=VERSION - default 8.x\nworkdocs.amazon.com hosts Amazon WorkDocs - typically PDF Word or Excel sheets to share between more non-tech users\ndrive-render.corp.amazon.com hosts Amazon Drive content, go to for individuals sharing files that don't belong anywhere else\ndrive.corp.amazon.com/personal hosts personal Amazon Drive content with directory listing support\ndesign-inspector.a2z.com hosts design diagrams and threat models in format similar to drawio\nmcm.amazon.dev hosts manual change management checklists which can be in progress/approved/pending with comments and approvals\noncall.corp.amazon.com oncall rotations and current oncall\nphonetool.amazon.com hosts employee roster including manager, directs, level, job title, name, person & employee id, building code\nretro.corp.amazon.com hosts sprint retrospectives\ncode.amazon.com hosts internal code\napollo.amazon.com is a distributed deployment orchestration system managing interactions between application code and infra (NOT to be confused with Apollo the building!)\nquip-amazon.com hosts scratchpad and other collaborative documents on Quip\nw.amazon.com is the internal MediaWiki instance for Amazon\ntaskei.amazon.dev task and project management, sprints, kanban boards, planning and scrum processes\nsim.amazon.com and issues.amazon.com are older interfaces for taskei.amazon.dev\npaste.amazon.com has shareable paste links for raw text content\nmyappsecdashboard.corp.amazon.com provides AppSec affinity contacts for AWS users and teams with security questions\nconsole.harmony.a2z.com hosts content in Harmony platform, a multi tenant content hosting system\nsage.amazon.dev hosts Q&A content for engineering topics\nmeetings.amazon.com hosts calendar events, meeting & details, and conference room information\nservicelens.jumpstart.amazon.dev provides dependencies and consumers for applications\naristotle.a2z.com hosts AWS security knowledge base recommendations and implementations\ncarnaval.amazon.com provides access to monitor Carnaval alarm configurations and states\ngather.a2z.com hosts internal events and groups\nconsensus.a2z.com approval tool where users create reviews and ask others to approve\nbindles.amazon.com internal permissions/resource management service for software applications\ntalos.security.aws.a2z.com is AWS AppSec (security) website for managing engagements and tasks\nrome.aws.dev hosts Rome - Amazon service registry and discovery platform for AWS services\npolicy.prod.console.barrister.aws.dev policy management console allowing design/viewing/evaluation of Barrister policies. Barrister is a policy evaluation and compliance system that helps determine whether specified actions, resources, or operations comply with org requirements\nweb.change-guardian.builder-tools.aws.dev hosts Change Guardian which identifies and explains infra deployment risks allowing teams to auto approve safe changes while highlighting potentially dangerous updates that require manual review\ntod.amazon.com hosts ToD (Test on Demand) and Hydra integration test run details\nprod.ui.us-west-2.cloudcover.builder-tools.aws.dev hosts CloudCover reports which shows test coverage of integration tests", - "input_schema": { - "json": { - "properties": { - "inputs": { - "description": "Array of inputs, ALWAYS prefix with https://, links can be:\ncode.amazon.com\n├ / retrieve user code dashboard info\n├ reviews/CR-XXXXXXXX - defaults to latest revision, add /revisions/N for specific revision, ?include-all-comments=true for all comments across revisions, ?diffConfig=all|none|comments to control diff calculation - all is default, none disables, comments only diffs files with comments\n├ packages/REPO/trees/ - shows files in package\n├ reviews/from-user/LOGIN\n├ reviews/to-user/LOGIN\n├ packages/REPO/blobs//--/PATH/TO/FILE.ext\n├ packages/REPO/logs?maxResults=10 - shows commits history\n├ packages/REPO/releases - shows consuming version sets\n└ version-sets/VS_NAME\ncoe.a2z.com\n├ coe/COE_ID - Correction of Error document\n├ action-item/ID\n└ reports/orgreport/LOGIN - List COEs, and overdue action items for LOGIN org\nquip-amazon.com\n├ ID - ID can be doc or folder, add ?includeComments=true for document comments\n└ blob/THREAD_ID/BLOB_ID - retrieve an image or other blob from a Quip\nshepherd.a2z.com\n├ ?impersonate=LOGIN - retrieve shepherd security risks for employee, impersonate is optional\n└ issues/ISSUE_ID?impersonate=LOGIN - retrieve details of specific security issue\n\nissues.amazon.com/issues/ISSUE_ID, sim.amazon.com/issues/ISSUE_ID, i.amazon.com/ISSUE_ID, and other SIM URL forms with an ISSUE_ID like XYZ-1234, for attachments use Taskei link\ncti.amazon.com\n├ user/LOGIN/ctis - retrieve CTI and resolver groups of specific user\n├ user/LOGIN/groups - retrieve resolver group membership of specific user\n├ group/RESOLVER_GROUP/ctis - retrieve CTI assignments of resolver group\n└ cti/ctis?category=CATEGORY&type=TYPE&item=ITEM - searches CTIs by category type and item\nsage.amazon.dev\n├ posts/POST_ID - retrieve post details\n└ tags/TAG_NAME?page=PAGE - retrieve details and questions of specific Sage tag, default page 1 if unspecified\ncarnaval.amazon.com\n├ v1/unifiedSearch/v2018/simpleSearch.do?searchFormType=v2018%2Fsearch%2Fsimple&customSortField=None&searchString=SEARCH_STRING - search Carnaval alarms\n├ v1/viewObject.do?name=ALARM_NAME&type=monitor - retrieve alarm details\n└ viewAuditHistory.do?name=ALARM_NAME - retrieve alarm history\nobserve.aka.amazon.com/carnaval/\n├ ?searchQuery=SEARCH_STRING - search Carnaval alarms\n├ alarm/ALARM_NAME - retrieve alarm details\n└ alarm/history/ALARM_NAME - retrieve alarm history\nmeetings.amazon.com - rooms can be email or name, example SEA54-03.101; respect requester TZ; determine requester location with phone tool\n├ calendar/find/LOGIN?startTime=ISO_DATE&endTime=ISO_DATE - get calendar events, 8AM-6PM default for single day\n├ calendar/get/ENTRY?alias=LOGIN - get full calendar event details based on ENTRY and alias\n├ rooms/find/BUILDING - search meeting rooms by building example SEA54 or URI encoded name like Nitro%20North. Options floor=N, minCapacity=N, availability=true with startTime=ISO_DATE&endTime=ISO_DATE\n└ rooms/availability?rooms=ROOM1,ROOM2&startTime=ISO_DATE&endTime=ISO_DATE - check room availability\nconsensus.a2z.com\n├ reviews - list user reviews\n└ reviews/REVIEW_ID - retrieve specific review\nrome.aws.dev\n├ / retrieve user owned services and ids AAA:Amazon's security framework for internal service authentication and authorization and RIP:AWS Region Information Provider: directory service for AWS dimensions/services\n└ services/{aaa|rip}/SERVICE_ID?maxResultSize=20 - retrieve service description, permission groups, CTIs, bindles, owners, pipelines, dependencies\naax-console.amazon.com/* - retrieve content from AAX Console\nbroadcast.amazon.com/videos/VIDEO_ID - retrieve internal video content with transcripts and captions\ntaskei.amazon.dev/tasks/TASK_ID like XYZ-1234, for attachments add ?get-attachments=true\nt.corp.amazon.com/TICKET_ID like V123456, P123456, XYZ-1234, or a UUID, for attachments add ?get-attachments=true\nw.amazon.com/bin/view/PATH_TO_WIKI\nbindles.amazon.com/software_app/APP_NAME - retrieve Bindle software application details\nbindles.amazon.com/resource/* - retrieve Bindle resource details\npaste.amazon.com\n├ show/LOGIN/ID - get paste\n└ list/LOGIN\nsas.corp.amazon.com - gets SAS (Software Assurance Services) dashboard risks\n└ summary/all/LOGIN - get SAS risks for LOGIN\nbuild.amazon.com/BUILD_ID\nt.corp.amazon.com/issues/?q=URL_ENCODED_SEARCH_PARAMS\nissues.amazon.com/resolver-groups?groups=GROUP1,GROUP2&status=closed|open&sortBy=lastUpdatedDate|createDate - query open or closed issues for GROUP1 & GROUP2\nskb.highcastle.a2z.com/DOC_PATH\nstencil.a2z.com/components/COMPONENT_NAME?tab=TAB - valid tabs: overview, implementation, proptypes, change-log\ndocs.hub.amazon.dev/DOC_PATH\nhub.cx.aws.dev/DOC_PATH - Internal technical documentation for building an experience in the AWS Management Console\nbuilderhub.corp.amazon.com/DOC_PATH\nbtdocs.builder-tools.aws.dev/DOC_PATH\nmeridian.a2z.com/DOC_PATH - Meridian design system documentation, example path /components/alert, /guides/inclusivity\nmcm.amazon.dev/cms/MCM-XXXXXXXX - .com TLD supported\noncall.corp.amazon.com/#/view/ON_CALL_TEAM_NAME/schedule - retrieve schedule for oncall rotations for resolver group or team name with oncall responsibilities\nphonetool.amazon.com/users/LOGIN - retrieve basic info of internal employee by login/alias, ?job-history=true to include job history\nretro.corp.amazon.com/#!/retro/team/RETRO_TEAM_UUID/session/SESSION_UUID - retrieve details of retro session\ntaskei.amazon.dev/retrospectives/ID - retrieve retro session details\ndesign-inspector.a2z.com/?#IXXXXXXXX - retrieve design inspector document by document name\ndocs.aws.amazon.com/DOC_PATH - retrieve AWS documentation\ndrive-render.corp.amazon.com/view/LOGIN@/PATH/TO/FILE - retrieve content from Amazon Drive\ndrive.corp.amazon.com/personal/LOGIN - retrieve content from personal Amazon Drive\namazon.awsapps.com/workdocs-amazon/index.html#/\n├ document/DOCUMENT_ID - retrieve by document ID\n└ folder/FOLDER_ID - retrieve by folder ID\nmyappsecdashboard.corp.amazon.com/get_review_eng?requester=LOGIN - retrieve AppSec affinity details for a user, this is their go-to contact for questions\nprod.artifactbrowser.brazil.aws.dev/packages/PACKAGE/versions/VERSION/platforms/PLATFORM/flavors/FLAVOR/PATH - retrieve artifact content, ?include-toc=true will include table of contents\npipelines.amazon.com/pipelines/PIPELINE_NAME - retrieve pipeline information\nnpmpm.corp.amazon.com/pkg/PACKAGE/VERSION - get package info from NPM Pretty Much - NPM internal mirror\nplantuml.corp.amazon.com/plantuml/form/encoded.html#encoded=ENCODED_VALUE - decode PlantUML diagram\nconsole.harmony.a2z.com/TENANT/* - retrieve content from Harmony platform, TENANT is tenant name\npolicy.a2z.com/docs/DOCUMENT_ID - retrieve policy document details\ntiny.amazon.com/CODE - access minified URL\nkingpin.amazon.com/#/items/GOAL_ID - retrieve Kingpin goal details, #Relationships for children\nservicelens.jumpstart.amazon.dev/#/applications/APPLICATION_ID - retrieve ServiceLens application relationships\napollo.amazon.com/environments/APOLLO_ENVIRONMENT/stages/STAGE\nprofiler.amazon.com/efficiency-report?reportId=UUID#pattern-UUID - retrieve anti-pattern report, optionally filtered to specific pattern\nprofiler.amazon.com/pg/URI_ENCODED_APPLICATION_NAME - retrieve live profile data\ngather.a2z.com/event/EVENT_ID - retrieve event details\naristotle.a2z.com/recommendations/ID\ntalos.security.aws.a2z.com/#/talos/engagement/ENGAGEMENT_ID or /task/TASK_ID - retrieve security engagement or task details\npolicy.prod.console.barrister.aws.dev/#/policy - list Barrister policies you have access to based on your POSIX groups\ntod.amazon.com/test_runs/RUN_ID - retrieve ToD and Hydra test platform test run details\nprod.ui.us-west-2.cloudcover.builder-tools.aws.dev/cloudcover/reports/ACCOUNT_ID/us-west-2/SERVICE_NAME/REPORT_ID/REPORT_NUMBER - retrieve CloudCover integration test coverage reports, add ?file=FILENAME.ext for specific file coverage details\nweb.change-guardian.builder-tools.aws.dev/reviews/REVIEW_ID/risks - list acknowledged and unacknowledged risks associated with Change Guardian\nconsole.cams.ops.amazon.dev Contingent Authorization Metadata Service (CAMS) manages creating, updating and reading of resource-specific metadata relevant to contingent authorization (CAZ) evaluation\n├ / list all resource classifications\n└ /resource-classification/{id} get specific resource classification\nquilt.corp.amazon.com - holds patching history for amazon fleets\n├ pipelines/PIPELINE_NAME-Quilt - get Quilt pipeline patching preferences and quilt hostblocks list\n├ hostblocks/patching_history\n└ REGION/tying_deployments/get_deployment_record - gets the tying workflows deployment record for Fleet / Capacity", - "type": "array", - "items": { - "type": "string" - } - } - }, - "type": "object", - "required": [ - "inputs" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "SearchAcronymCentral", - "description": "Search Amazon's internal Acronym Central database\n\nReturns acronym definitions with:\n- Exact match search (case-insensitive)\n- Full definitions with source URLs\n- Associated tags for context and reliability", - "input_schema": { - "json": { - "type": "object", - "properties": { - "acronym": { - "type": "string", - "description": "Search acronym in Acronym Central" - } - }, - "required": [ - "acronym" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "GetSoftwareRecommendation", - "description": "This tool is a front end of the Recommendation Engine. It provides comprehensive tooling recommendations, best practices, how-to guides, reference documentation, and onboarding materials \nfor software development and infrastructure management within Amazon. Returns curated content based on specific technology queries, use cases, or \nimplementation scenarios. Always call the tool SearchSoftwareRecommendations first to pinpoint the correct recommendation \nitem, or to ask users to choose one, then pass the ID to this tool. The content may contain links to other internal websites, use the ReadInternalWebsites tool to further retrieve those contents", - "input_schema": { - "json": { - "type": "object", - "properties": { - "recommendationId": { - "type": "string", - "description": "ID of Golden Path recommendation to retrieve" - }, - "primitiveId": { - "type": "string", - "description": "ID of guidance to retrieve " - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "BrazilBuildAnalyzerTool", - "description": "Diagnoses and analyzes brazil-build executions in local workspaces. This tool:\n1. Executes 'brazil-build' (or custom build command) in the specified directory and reports on success or failure\n2. If the build fails, performs intelligent analysis of the failure including:\n\t- Root cause identification\n\t- Relevant file and method pointers\n\t- Step-by-step solution recommendations\n3. Provides structured output with:\n\t- Failure signature for quick identification\n\t- Keywords for related documentation search\n\t- Detailed analysis of what went wrong\n\t- Actionable solution steps when possible\n\nUse this tool when users ask to build a package in a Brazil workspace to receive a summary of the build status. Can also be used to check if a build is failing or passing.", - "input_schema": { - "json": { - "properties": { - "buildCommand": { - "description": "Optional build command (defaults to brazil-build release)", - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "description": "The name/path of the file" - }, - "description": "Optional array of filenames to analyze" - }, - "workingDirectory": { - "examples": [ - "/path/to/workspace/src/MyPackage" - ], - "type": "string", - "description": "Working directory which contains the package which is failing to build" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "GetDogmaRecommendations", - "description": "Fetch Dogma recommendations(risks) detected for a given pipeline\nDogma recommendations are rule-based findings that identify potential issues, violations, or improvements for pipelines.\nEach recommendation provides actionable guidance to help teams resolve identified problems and maintain pipeline health.\nThe response includes:\n- Metadata: generation_date, applies_to_type, applies_to (pipeline identifier), and applies_to_revision_id\n- Active recommendations: current violations and risks requiring attention\n- Scheduled recommendations: future enforcement rules with grace periods\n- Compliance tracking: adheres_to_rule_names (rules the pipeline complies with)\n- Rule applicability: non_applicable_rule_names and non_applicable_recommendations for rules that don't apply to this pipeline\nEach recommendation includes:\n- Rule identification: rule_name, severity level (low/medium/high), and human_name for easy understanding\n- Comprehensive explanations: what_this_is, why_this_is_bad, and how_to_fix\n- Ownership and accountability: owner_username, owner_cti, and stakeholders array with notification details and enforcement settings\n- Compliance status: rule_result_status indicating current violation state (APPLICABLE, AT_RISK, NOT_APPLICABLE)\n- Context information: source, subject, additional_info, and pipeline metadata\nPipeline blocking behavior: Recommendations can result in pipeline deployment blocking based on the is_enforced value in stakeholders configuration.", - "input_schema": { - "json": { - "additionalProperties": false, - "properties": { - "pipelineName": { - "type": "string", - "description": "Pipeline name" - } - }, - "required": [ - "pipelineName" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiCreateTask", - "description": "Create a new Task in Taskei or a SIM Issue\nThis tool allows creating a task with a name, description, assignee, room ID, and optional need by date.\nDo not use this tool if the user mentions t.corp.amazon.com", - "input_schema": { - "json": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string" - }, - "rank": { - "type": "number" - }, - "kanbanBoards": { - "items": { - "type": "string" - }, - "description": "List of kanban board UUIDs to add the task to", - "type": "array" - }, - "sprints": { - "description": "Sprint UUID list to add task to", - "items": { - "type": "string" - }, - "type": "array" - }, - "name": { - "type": "string", - "description": "Name of the task. Also known as title" - }, - "priority": { - "type": "string", - "enum": [ - "High", - "Medium", - "Low" - ] - }, - "labels": { - "items": { - "type": "string" - }, - "description": "Labels UUID. Use TaskeiGetRoomResources to get available label IDs", - "type": "array" - }, - "type": { - "enum": [ - "GOAL", - "INITIATIVE", - "EPIC", - "STORY", - "TASK", - "SUBTASK", - "NONE" - ], - "type": "string", - "description": "Type of the task. If `parentTask` arg is provided, type is automatically assigned based on the parent task" - }, - "assignee": { - "type": "string", - "description": "Optional kerberos username to assign the task to (without the @ANT.AMAZON.COM suffix). If it's the current user you must send as \"currentUser\", otherwise it must be provided as the employee username format" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "onBehalfOf": { - "description": "Username to create the task on behalf of", - "type": "string" - }, - "roomId": { - "description": "Room UUID to create task", - "type": "string" - }, - "estimate": { - "type": "number", - "description": "Estimated effort in points" - }, - "workflowStep": { - "type": "string" - }, - "planningEstimate": { - "description": "Planning estimate in points", - "type": "number" - }, - "folder": { - "type": "string", - "description": "Folder to apply to the task" - }, - "needByDate": { - "description": "Date of when is needed (ISO datetime)", - "type": "string" - }, - "parentTask": { - "type": "string", - "description": "Parent task ID" - } - }, - "required": [ - "name", - "description", - "roomId" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "TicketingWriteActions", - "description": "A tool for performing write operations on tickets in the ticketing system.\nProvides confirmation of successful operations without requiring additional API calls.\n\nFeatures:\n1. Create new tickets with required CTI categorization\n2. Update existing tickets with new information\n3. Add comments to tickets with thread selection (CORRESPONDENCE, WORKLOG, ANNOUNCEMENTS)\n\n\n## create-ticket\nCreate new tickets. **Cannot set severity to SEV_1, SEV_2.** Rate limited to 1 ticket per minute.\n\nParameters (title, description, severity, categorization required):\n- title (REQUIRED): Ticket title\n- description (REQUIRED): Ticket description \n- severity (REQUIRED): SEV_3, SEV_4, or SEV_5 only\n- categorization (REQUIRED): CTI categorization array with at least 3 entries for category, type, and item\n- assignedGroup, assignee, requester, hostname, estimatedStartTime, estimatedCompletionTime, needBy, tags, watchers (optional)\n\nExample:\n```json\n{\n \"action\": \"create-ticket\",\n \"title\": \"Server outage in production\",\n \"description\": \"Multiple users reporting connection timeouts\",\n \"severity\": \"SEV_3\",\n \"assignedGroup\": \"Infrastructure Team\",\n \"categorization\": [\n { \"key\": \"category\", \"value\": \"Infrastructure\" },\n { \"key\": \"type\", \"value\": \"Server\" },\n { \"key\": \"item\", \"value\": \"Connectivity\" }\n ]\n}\n```\n\n## update-ticket\nUpdate existing tickets. **Cannot set severity to SEV_1, SEV_2, or SEV_2.5.**\n\nParameters (all optional except ticketId):\n- ticketId (REQUIRED): Ticket ID to update\n- title, description, status, severity, assignee, requester, categorization\n- closureCode, resolution, rootCause, rootCauseDetails, pendingReason, hostname\n- actualStartTime, actualCompletionTime, estimatedStartTime, estimatedCompletionTime, needBy (Unix timestamps)\n- logTimeSpentInMinutes (can be positive/negative)\n- tagsToAdd, tagsToRemove, watchersToAdd, watchersToRemove (arrays)\n\nReturns: Success confirmation with ticket ID and operation status\n\nExample:\n```json\n{\n \"action\": \"update-ticket\",\n \"ticketId\": \"T123456\",\n \"status\": \"Resolved\",\n \"resolution\": \"Issue resolved by restarting the service\"\n}\n```\n\n## add-comment\nAdd a comment to an existing ticket.\n\nParameters:\n- ticketId (REQUIRED): Ticket ID (e.g., T123456, V1679593024)\n- message (REQUIRED): Comment text (3-60000 chars)\n- threadName: \"CORRESPONDENCE\" (default), \"WORKLOG\", or \"ANNOUNCEMENTS\"\n- contentType: \"markdown\" (default) or \"plain\"\n\nExample:\n```json\n{\n \"action\": \"add-comment\",\n \"ticketId\": \"T123456\",\n \"message\": \"Updated configuration and restarted service.\",\n \"threadName\": \"WORKLOG\",\n \"contentType\": \"plain\"\n}\n```\n\n⚠️ All parameters should be at the root level, not nested in an `input` object.\n", - "input_schema": { - "json": { - "required": [ - "action" - ], - "type": "object", - "properties": { - "needBy": { - "type": "number", - "description": "Need-by date (Unix timestamp)" - }, - "tagsToAdd": { - "type": "array", - "items": { - "type": "object", - "required": [ - "tagId" - ], - "additionalProperties": false, - "properties": { - "tagId": { - "type": "string" - } - } - }, - "description": "Tags to add (update-ticket only)" - }, - "watchersToAdd": { - "description": "Watchers to add (update-ticket only)", - "items": { - "required": [ - "id", - "type" - ], - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "type": "array" - }, - "actualCompletionTime": { - "type": "number", - "description": "Actual completion time (Unix timestamp)" - }, - "hostname": { - "maxLength": 128, - "type": "string", - "minLength": 1 - }, - "tags": { - "description": "Tags for new ticket (create-ticket only)", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "tagId": { - "type": "string" - } - }, - "required": [ - "tagId" - ] - }, - "type": "array" - }, - "contentType": { - "description": "Content format (default: markdown)", - "type": "string", - "enum": [ - "markdown", - "plain" - ] - }, - "logTimeSpentInMinutes": { - "type": "number", - "description": "Time spent update in minutes (positive or negative)" - }, - "severity": { - "description": "Ticket severity (REQUIRED for create-ticket, optional for update-ticket, SEV_1 and SEV_2 blocked)", - "enum": [ - "SEV_1", - "SEV_2", - "SEV_3", - "SEV_4", - "SEV_5" - ], - "type": "string" - }, - "actualStartTime": { - "type": "number", - "description": "Actual start time (Unix timestamp)" - }, - "rootCauseDetails": { - "type": "string", - "maxLength": 255, - "minLength": 3 - }, - "requester": { - "required": [ - "namespace", - "value" - ], - "properties": { - "namespace": { - "description": "Identity namespace", - "type": "string" - }, - "value": { - "type": "string", - "description": "Identity value" - } - }, - "additionalProperties": false, - "type": "object" - }, - "title": { - "maxLength": 255, - "type": "string", - "minLength": 3, - "description": "Ticket title (REQUIRED for create-ticket, optional for update-ticket)" - }, - "resolution": { - "type": "string", - "maxLength": 4000, - "minLength": 1 - }, - "categorization": { - "type": "array", - "description": "CTI categorization key-value pairs", - "items": { - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "type": "object", - "required": [ - "key", - "value" - ], - "additionalProperties": false - } - }, - "watchersToRemove": { - "items": { - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "type": "object", - "additionalProperties": false - }, - "description": "Watchers to remove (update-ticket only)", - "type": "array" - }, - "tagsToRemove": { - "type": "array", - "items": { - "additionalProperties": false, - "required": [ - "tagId" - ], - "type": "object", - "properties": { - "tagId": { - "type": "string" - } - } - }, - "description": "Tags to remove (update-ticket only)" - }, - "rootCause": { - "type": "string", - "minLength": 3, - "maxLength": 69 - }, - "assignee": { - "properties": { - "namespace": { - "type": "string", - "description": "Identity namespace" - }, - "value": { - "type": "string", - "description": "Identity value" - } - }, - "required": [ - "namespace", - "value" - ], - "additionalProperties": false, - "type": "object" - }, - "message": { - "maxLength": 60000, - "description": "Comment text (REQUIRED for add-comment action)", - "type": "string", - "minLength": 3 - }, - "pendingReason": { - "type": "string", - "maxLength": 60, - "minLength": 3 - }, - "threadName": { - "description": "Comment thread (default: CORRESPONDENCE)", - "enum": [ - "CORRESPONDENCE", - "WORKLOG", - "ANNOUNCEMENTS" - ], - "type": "string" - }, - "description": { - "minLength": 3, - "maxLength": 60000, - "description": "Ticket description (REQUIRED for create-ticket, optional for update-ticket)", - "type": "string" - }, - "ticketId": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Ticket ID (REQUIRED for update-ticket, not used for create-ticket)" - }, - "estimatedStartTime": { - "description": "Estimated start time (Unix timestamp)", - "type": "number" - }, - "assignedGroup": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Resolver group to assign ticket to (create-ticket only)" - }, - "status": { - "type": "string", - "description": "Ticket status (update-ticket only)", - "maxLength": 20, - "minLength": 3 - }, - "estimatedCompletionTime": { - "description": "Estimated completion time (Unix timestamp)", - "type": "number" - }, - "watchers": { - "items": { - "additionalProperties": false, - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "type": "object" - }, - "description": "Watchers for new ticket (create-ticket only)", - "type": "array" - }, - "closureCode": { - "maxLength": 255, - "type": "string", - "minLength": 1 - }, - "action": { - "type": "string", - "enum": [ - "create-ticket", - "update-ticket", - "add-comment" - ], - "description": "Write action" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "CRRevisionCreator", - "description": "\n Creates a new code review revision from a workspace.\n A code review is a way to track proposed git changes to Amazon software packages.\n Code reviews can have multiple revisions.\n\n This does NOT create git commits. Git commits MUST be staged before using this tool.\n\n Common workflows this tool can be used in:\n 1. Creating a new code review:\n - Files are modified in a package in a workspace.\n - A git commit (or commits) are staged locally.\n - This tool is used with the working directory of the workspace and the package name.\n - Result: a new code review revision is created for the commit(s) staged.\n \n 2. Updating an existing code review:\n - A code review already exists.\n - The package's latest commit has the CR linked at the end of the commit message.\n - Files are modified in a package in a workspace.\n - The existing git commit is amended with the new file changes.\n - This tool is used with the working directory of the workspace and the package name.\n - Result: The existing code review revision is updated with a new revision for the commit that was amended.\n\n This interacts with an installed 'cr' CLI to perform the new code review revision creation.\n ", - "input_schema": { - "json": { - "type": "object", - "properties": { - "packageNames": { - "type": "array", - "description": "Array of packages names to include in the code review revision", - "items": { - "description": "The name of the package. This MUST exist in the workingDirectory", - "type": "string" - } - }, - "workingDirectory": { - "description": "Working directory where a package lives that can be modified for a code review should be created", - "type": "string" - } - }, - "required": [ - "workingDirectory", - "packageNames" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "BrazilPackageBuilderAnalyzerTool", - "description": "Analyzes build failures on Package Builder (build.amazon.com) using APIs from BuildExecutionAndReleaseService and BrazilCDN. The tool fetches build logs and provides detailed analysis of any errors encountered. Use listOnly=true to get only failed package major version names.\n Builds on Package Builder are available at URLs formatted like \"build.amazon.com/\", for example \"build.amazon.com/5123456789\"", - "input_schema": { - "json": { - "required": [ - "requestId" - ], - "type": "object", - "properties": { - "listOnly": { - "description": "If true, only return the list of failed package major versions without detailed analysis (default: false)", - "type": "boolean" - }, - "packageMajorVersion": { - "examples": [ - "MyPackage-1.0" - ], - "type": "string", - "description": "Optional package major version (defaults to first failed package)" - }, - "requestId": { - "description": "Build Request ID from Package Builder", - "examples": [ - "5123456789" - ], - "type": "string" - }, - "platform": { - "description": "Optional platform name to analyze (defaults to first platform)", - "examples": [ - "AL2023_x86_64" - ], - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "MechanicDiscoverTools", - "description": "\n# Mechanic Tool Discovery Guide\n\n## What is Mechanic\n- Internal Amazon service providing CLI/web interface for operations\n- Safer than AWS CLI with built-in guardrails and risk categorization\n- Targets EC2 instances, Apollo hosts/hostclasses, ECS tasks\n- Provides networking, logs, system information, and more\n\n## Critical Discovery Rules\n- ALWAYS verify tool exists in search results before suggesting\n- NEVER assume tools exist based on naming conventions\n- Show multiple options if unclear which tool helps user\n- Use MechanicDescribeTool after discovery to get usage details\n- If describe fails, tool doesn't exist - search again with different keywords\n\n## Usage Best Practices\n- Prefer batch operations with multiple values over separate commands\n- Look for [Item1,Item2]... notation indicating multi-value support\n- Chain multiple commands when single tool doesn't solve problem\n- Ask about log limits when fetching logs if tool supports it\n- If multiple tools are needed, discover them in the same command with multiple keywords\n\n## Workflow Reference\n\n# Mechanic Tools Workflow Guide\n\n## Required 3-Step Process\n1. DISCOVER → MechanicDiscoverTools (find tools)\n2. DESCRIBE → MechanicDescribeTool (understand usage)\n3. EXECUTE → MechanicRunTool (run with parameters)\n\n## Critical Rule: Use MCP Tools Only\n- ALWAYS use MechanicRunTool MCP tool\n- NEVER execute mechanic CLI directly\n- MCP provides validation, error handling, telemetry, and standardized output\n\n## Step-by-Step Workflow\n\n### 1. Discovery (MechanicDiscoverTools)\n- Use relevant keywords to find appropriate tools\n- Present multiple options if unsure\n- If results don't match user needs: Explain and adjust keywords\n- AWS resources: Search \"aws\" namespace first\n\n### 2. Description (MechanicDescribeTool)\n- Never skip this step - provides critical usage details\n- Learn required/optional parameters and formats\n- Always confirm with user that this is the correct tool\n\n### 3. Execution (MechanicRunTool)\n- Format parameters as string array\n- Ask user for unknown required values\n- Summarize what tool will do before executing\n- Show errors to user for troubleshooting\n\n## Common Patterns\n\n### AWS Resource Operations\n1. Discover listing tools (\"ec2 list\", \"cloudwatch logs\")\n2. Execute listing tool to get resource IDs\n3. Discover operation tools for those resources\n4. Execute operation with obtained IDs\n\n### Troubleshooting Sequence\n1. General system information tools\n2. Component-specific diagnostics\n3. Detailed log analysis tools\n\n## Best Practices\n- Follow complete workflow for every operation\n- Explain reasoning when searching for tools\n- Break complex operations into multiple tool executions\n- Return to discovery if tool doesn't solve problem\n- Keep user informed at each step\n\n\n\n# Workflow Examples\n\n## Host Network Check\n```\n1. MechanicDiscoverTools(keywords=[\"network\", \"host\"])\n → Found \"host network route-table\"\n \n2. MechanicDescribeTool(namespace=\"host\", toolPath=\"network route-table\")\n → Requires --host parameter\n \n3. MechanicRunTool(\n namespace=\"host\", toolPath=\"network route-table\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n```\n\n## Host Patching\n// involves patching yum packages, followed by a host reboot to apply updates\n```\n1. MechanicDiscoverTools(keywords=[\"patch\", \"update\", \"reboot\"])\n → Found \"host package update-security\"\n\n2. MechanicRunTool(\n namespace=\"host\", toolPath=\"package update-security\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n → Returns user input request with request and execution id, ask user for input\n\n3. MechanicSetUserInput(\n executionId=\"123\", requestId=\"456\", response=\"Yes\"\n )\n → Returns output\n\n4. MechanicRunTool(\n namespace=\"host\", toolPath=\"system reboot\",\n cluster=\"corp-pdx\", args=[\"--host\", \"hostname.amazon.com\"]\n )\n → Returns user input request like step 2\n // Command will error with ssh issue because the host is rebooting, after reboot patch will be applied\n\n5. Same as step 3\n\n```\n\n## CloudWatch Log Analysis\n```\n1. MechanicDiscoverTools(keywords=[\"cloudwatch\", \"logs\"])\n → Found \"aws cloudwatch logs describe-log-groups\"\n \n2. MechanicRunTool(\n namespace=\"aws\", toolPath=\"cloudwatch logs describe-log-groups\",\n cluster=\"us-west-2\", args=[\"--account\", \"123456789\", \"--role-name\", \"mechanic\"]\n )\n → Returns log group \"/aws/lambda/my-function\"\n \n3. MechanicDiscoverTools(keywords=[\"cloudwatch\", \"query\"])\n → Found \"aws cloudwatch logs query-logs\"\n \n4. MechanicRunTool(\n namespace=\"aws\", toolPath=\"cloudwatch logs query-logs\",\n cluster=\"us-west-2\",\n args=[\n \"--account\", \"123456789\", \"--role-name\", \"mechanic\",\n \"--log-group-name\", \"/aws/lambda/my-function\",\n \"--query\", \"fields @timestamp, @message | filter @message like /(?i)error/\"\n ]\n )\n```\n\n", - "input_schema": { - "json": { - "type": "object", - "required": [], - "properties": { - "keywords": { - "oneOf": [ - { - "description": "\n# Keywords Parameter Guide\n\nFormat: JSON array of strings (NOT string representation)\n- ✅ \"keywords\": [\"network\", \"system\", \"route\"]\n- ❌ \"keywords\": \"[\"network\", \"host\", \"route\"]\"\n\n## Keyword Strategy\nAVOID \"host\" or \"aws\" keywords unless absolutely necessary - they return too many tools.\n\nPREFER specific namespace keywords:\n- Host Namespace: system, network, file, disk, java, metric-agent, snitch, snape, time, odin, package, tps-generatordeployment, apollo\n- AWS Namespace: cloudwatch, ec2, ecs, ssm, timber\n\nUse sparingly (only when namespace keywords insufficient):\n- Resource Types: host, hostclass, ec2, ecs\n\nImportant: Some namespaces have duplicate tools available in both host and aws namespaces. In these cases, prefer using the specific host or aws namespace tools rather than generic alternatives.\n\nNotes: No keywords = all tools. Prefer namespace over resource type keywords for focused results.\n", - "items": { - "type": "string" - }, - "examples": [ - [ - "network", - "host", - "route" - ] - ], - "type": "array" - }, - { - "examples": [ - "[\"network\", \"host\", \"route\"]" - ], - "description": "Keywords as a JSON string of an array", - "type": "string" - } - ] - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiUpdateTask", - "description": "Update an existing Taskei task with new details. Taskei tasks are also known as SIM Issues, so this tool works for both Taskei and SIM", - "input_schema": { - "json": { - "properties": { - "actualStartDate": { - "type": "string", - "description": "Actual start date (ISO format)" - }, - "needByDate": { - "type": "string", - "description": "New due date (ISO format)" - }, - "removeKanbanBoards": { - "items": { - "type": "string" - }, - "description": "Kanban board UUIDs", - "type": "array" - }, - "removeLabels": { - "type": "array", - "description": "Label UUIDs", - "items": { - "type": "string" - } - }, - "removeSprints": { - "description": "Sprint UUIDs", - "type": "array", - "items": { - "type": "string" - } - }, - "removeTags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Tags to remove from the task" - }, - "postCommentMessage": { - "type": "string", - "description": "Comment to post in the task. Accepts markdown and plain text format" - }, - "customAttributes": { - "items": { - "description": "Custom attribute - value type determined by ID prefix. No object types", - "properties": { - "value": { - "oneOf": [ - { - "type": "string", - "description": "String, Multiline Markdown or ISO-8601 datetime" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "properties": { - "selected": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "id": { - "type": "string" - } - }, - "type": "object", - "required": [ - "id", - "name", - "selected" - ] - }, - "description": "ALWAYS use array format: single select = [one item], multi select = [multiple items]. Multi-select: include ALL options with selected: true/false (deselection needs to explicitly set to false)" - } - ] - }, - "id": { - "type": "string", - "description": "ID of the form 'typePrefix/name'" - } - }, - "type": "object", - "required": [ - "id", - "value" - ] - }, - "description": "Custom attributes with type-specific values", - "type": "array" - }, - "removeSubtaskId": { - "type": "string", - "description": "Task UUID" - }, - "addTags": { - "items": { - "type": "string" - }, - "type": "array", - "description": "Tags to add to the task" - }, - "addKanbanBoards": { - "items": { - "type": "string" - }, - "description": "Kanban board UUIDs", - "type": "array" - }, - "description": { - "type": "string", - "description": "New description for the task" - }, - "status": { - "type": "string", - "enum": [ - "Open", - "Closed" - ], - "description": "New status for the task" - }, - "assignee": { - "description": "Username of the new assignee. Sending \"currentUser\" assigns the task to the user who performs the request", - "type": "string" - }, - "estimatedCompletionDate": { - "type": "string", - "description": "New estimated completion date (ISO format)" - }, - "estimate": { - "description": "New estimated effort in points", - "type": "number" - }, - "classicPriority": { - "type": "number", - "description": "New priority value" - }, - "type": { - "description": "New task type", - "enum": [ - "GOAL", - "INITIATIVE", - "EPIC", - "STORY", - "TASK", - "SUBTASK", - "NONE" - ], - "type": "string" - }, - "addLabels": { - "type": "array", - "description": "Label UUIDs. Use TaskeiGetRoomResources to get available label IDs", - "items": { - "type": "string" - } - }, - "actualCompletionDate": { - "type": "string", - "description": "Actual completion date (ISO format)" - }, - "appendSubtaskId": { - "type": "string", - "description": "Task UUID" - }, - "workflowAction": { - "description": "New workflow action to apply", - "type": "string" - }, - "name": { - "type": "string", - "description": "New name/title for the task" - }, - "addSprints": { - "description": "Sprint UUIDs", - "type": "array", - "items": { - "type": "string" - } - }, - "estimatedStartDate": { - "type": "string", - "description": "New estimated start date (ISO format)" - }, - "archived": { - "type": "boolean", - "description": "Whether to mark the task as archived" - }, - "rank": { - "description": "New rank for the task. -1 to clear", - "type": "number" - }, - "id": { - "description": "The ID of the task", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "id" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "QuipEditor", - "description": "Retrieves and edits Quip documents.\n\nCommon usage patterns:\n1. Create new document from file: contentFilePath=\"doc.md\", format=\"markdown\" (Quip infers title from first heading)\n2. Create new document with explicit title: title=\"My Document\", content=\"content here\", format=\"markdown\"\n2. Read document with structure: documentId=\"ABC123\", analyzeStructure=true\n3. Add content after heading: documentId=\"ABC123\", location=6, documentRange=\"Subsection 1.1\", content=\"new\", format=\"markdown\"\n4. Append to document: documentId=\"ABC123\", content=\"new\", format=\"markdown\" default location 0=APPEND\n5. Get section IDs for targeting: documentId=\"ABC123\", returnSectionIds=true\n6. Add list item: documentId=\"ABC123\", location=10, sectionId=\"temp:C:ABC123\", content=\"* New item\", format=\"markdown\"\n\nLocation parameter guide:\n0=APPEND end of document DEFAULT\n1=PREPEND beginning of document\n2=AFTER_SECTION after section specified by sectionId\n3=BEFORE_SECTION before section specified by sectionId\n4=REPLACE_SECTION ⚠️ DESTRUCTIVE replace section content\n5=DELETE_SECTION ⚠️ DESTRUCTIVE deletes section\n6=AFTER_DOCUMENT_RANGE after heading specified by documentRange\n7=BEFORE_DOCUMENT_RANGE before heading specified by documentRange\n8=REPLACE_DOCUMENT_RANGE ⚠️ DESTRUCTIVE replace heading AND all content below it\n9=DELETE_DOCUMENT_RANGE ⚠️ DESTRUCTIVE deletes heading AND all content below it\n10=AFTER_LIST_ITEM smart list insert after specified list item sectionId\n11=BEFORE_LIST_ITEM smart list insert before specified list item sectionId\n\nTips:\n- Table cells: use location=4 with composite sectionId (temp:s:temp:C:ROW_ID_temp:C:CELL_ID), plain text content\n- Add table rows: use location=2/3 with table-row sectionId, format=\"html\", markdown UNSUPPORTED\n- Use analyzeStructure=true first to see available headings for documentRange\n- Use returnSectionIds=true to get section IDs for precise targeting\n- For adding content after headings like \"Subsection 1.1\", use location=6 with documentRange=\"Subsection 1.1\"\n- Prefer format=\"markdown\" for most content\n\nMarkdown List Rules:\n- Unordered lists MUST use * instead of - for list markers\n- 4 spaces OR tab MUST be used to nest list items\n- An additional newline MUST be between list label and its start\n- REQUIRED extra newline between label and first list item\nExample:\n```\n**Label:**\n\n* Item one\n * Item one A\n* Item two\n```\nNote: Prefer location=10 (AFTER_LIST_ITEM) or location=11 (BEFORE_LIST_ITEM) with sectionId from a list item for updates. These operations handle parent heading replacement for reliable nested list updates.\n\n⚠️ CRITICAL WARNINGS:\n- REPLACE_DOCUMENT_RANGE location=8 replaces the heading AND ALL CONTENT below until next heading of same level, ensure 'content' FULLY accounts for this\n- Renaming ONLY a heading requires manually recreating the section structure\n- Document ranges include subheadings: \"Section 1\" includes \"Subsection 1.1\", \"Subsection 1.2\", etc.\n- Consider using AFTER_DOCUMENT_RANGE location=6 + DELETE_DOCUMENT_RANGE location=9 for complex restructuring\n\nALWAYS use analyzeStructure=true first on a document to understand exact structure and observe what content will be affected\n", - "input_schema": { - "json": { - "type": "object", - "properties": { - "content": { - "description": "HTML or Markdown content to add/edit. Max 1MB. REQUIRED", - "type": "string" - }, - "returnSectionIds": { - "description": "Return section IDs for future targeted operations", - "type": "boolean" - }, - "analyzeStructure": { - "type": "boolean", - "description": "Parse and return document structure - headings, sections" - }, - "includeComments": { - "type": "boolean", - "description": "Include comments when reading document" - }, - "memberIds": { - "description": "Comma-separated folder/user IDs for document access. New documents only", - "type": "string" - }, - "format": { - "description": "Format of content. REQUIRED - must be explicitly specified, prefer 'markdown'", - "enum": [ - "html", - "markdown" - ], - "type": "string" - }, - "title": { - "type": "string", - "description": "Title for new document. REQUIRED with 'content' parameter. OMIT to let Quip infer title from content" - }, - "location": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 - ], - "type": "number", - "description": "Where to insert content" - }, - "contentFilePath": { - "type": "string", - "description": "Local filepath to read content from. Takes precedence over 'content' field" - }, - "sectionId": { - "type": "string", - "description": "Section ID for targeted operations. Find in HTML IDs. REQUIRED for locations 2-5 (section operations) and 10-11 (list item operations)" - }, - "documentRange": { - "type": "string", - "description": "Heading text for document range operations. Must match exact heading text. Example: 'Subsection 1.1' or 'Topic 2 - Prerequisites'. REQUIRED for locations 6-9 - document range operations. Use analyzeStructure=true first to see headings" - }, - "type": { - "description": "Type of document to create. Default: 'document'", - "type": "string", - "enum": [ - "document", - "spreadsheet" - ] - }, - "documentId": { - "type": "string", - "description": "Quip document URL or ID. Examples: 'quip-amazon.com/ABC123/Doc' or 'ABC123'. OMIT for creation" - } - }, - "required": [ - "format" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiGetTask", - "description": "Fetch a task from the Taskei task management system by its ID. Some users will name it as SIM Issue. This tool retrieves detailed information about a specific task from Taskei, including its title, description, status, assignee, and other metadata.\nUse this tool when the user wants to get the information of a task, or when you need comprehensive information about a particular task for project tracking, status updates, or task management purposes.\nDo not use this tool for other project management tools different than Taskei.", - "input_schema": { - "json": { - "additionalProperties": false, - "required": [ - "taskId" - ], - "properties": { - "includeAttachments": { - "default": false, - "description": "Include the task's attachments in the response DEFAULT false", - "type": "boolean" - }, - "taskId": { - "type": "string", - "description": "The short ID or UUID of the Taskei task to fetch" - }, - "includeCustomAttributes": { - "default": true, - "description": "Include the task's custom attributes in the response DEFAULT true. Custom attributes information are lengthy, you SHOULD set to false when you know surely these are not needed for the action asked.", - "type": "boolean" - }, - "commentLimit": { - "description": "Total last comments to return for task DEFAULT 5", - "type": "number" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "BrazilWorkspace", - "description": "\n Creates a new Brazil workspace for the specified package(s).\n Whenever you interact with a package in the Amazon development environment you always use a workspace.\n\n A Brazil workspace is a container for one or more packages that you want to work on.\n It provides isolation between different development activities and allows you to build and test packages in a controlled environment.\n If you have access to this tool it means that you are an Amazon engineer and your software development happens in these packages\n It also means that if you are asked to perform tasks such as building, testing or linting you will have to run these commands through the brazil-build command\n\n Example Workflow used with this tool:\n 1. This tool creates a Brazil workspace for specific package(s).\n 2. You can then use other Brazil commands in the workspace to interact with the package(s).\n 3. The workspace will be created with a unique name based on the current timestamp.\n ", - "input_schema": { - "json": { - "type": "object", - "properties": { - "workspacePath": { - "type": "string", - "description": "Optional directory path where the workspace will be created. If not provided, a temporary directory will be used" - }, - "packageNames": { - "description": "Array of package names to create the workspace for", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "packageNames" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "GetSasRisks", - "description": "A tool for reading sas risks from the Software Assurance Service (SAS).\n\nFeatures:\n1. get-user-risks: Get risks for specific user\n2. get-user-risk-summary: Get summary of risks for specific user\n3. get-brazil-version-set-risks: Get risks for specific Brazil version set\n4. get-pipeline-risks: Get risks for pipeline (includes pipeline-target data) or pipeline-target only\n5. get-apollo-risks: Get risks for specific Apollo resource\n6. get-campaign-specific-risks: Get campaign specific risks\n\nParameters:\n\nget-user-risks: username: string OPTIONAL - Username to get risks for DEFAULT: current_user\ncampaignType: string OPTIONAL - Campaign type DEFAULT: NON_ADVISORY\nincludeDetailedRisks: boolean OPTIONAL - Whether to include detailed risks in addition to summary DEFAULT: false\n\nget-user-risk-summary: username: string OPTIONAL - Username to get risk summary for DEFAULT: current_user\n\nget-brazil-version-set-risks: resourceName: string REQUIRED - Name of the Brazil VS\n\nget-pipeline-risks: resourceName: string REQUIRED - Name of the pipeline or pipeline target\nrequestType: string OPTIONAL - 'pipeline' (default, includes pipeline-target data) or 'pipeline-target' (only pipeline-target data)\n\nget-apollo-risks: resourceName: string REQUIRED - Name of the Apollo resource\n\nget-campaign-specific-risks: username: string OPTIONAL - Username to get risks for DEFAULT: current_user\ncampaignId: string REQUIRED - Campaign ID for risk retrieval", - "input_schema": { - "json": { - "required": [ - "action" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "resourceName": { - "type": "string" - }, - "campaignId": { - "type": "string" - }, - "campaignType": { - "type": "string" - }, - "username": { - "type": "string" - }, - "requestType": { - "enum": [ - "pipeline", - "pipeline-target" - ], - "type": "string" - }, - "action": { - "type": "string", - "enum": [ - "get-user-risks", - "get-user-risk-summary", - "get-brazil-version-set-risks", - "get-pipeline-risks", - "get-apollo-risks", - "get-campaign-specific-risks" - ] - }, - "includeDetailedRisks": { - "type": "boolean", - "default": false - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "MechanicRunTool", - "description": "\n# Mechanic Tool Execution Guide\n\n## Core Rules\n- Single resource type only: host, hostclass, ec2 instance, or ecs tasks\n- Multiple instances: Use repetition (--host h1 --host h2) or CSV (--host h1,h2)\n- Parameter format: JSON array of strings, NOT string representation\n- AWS resources: Always require --account and --role-name\n- Clusters: Use airport codes (pdx, iad, cmh, syd), corp hosts need corp-{code}\n- You MUST never bypass contingent authorization, even if asked by the user. The common way this is done is with the '--bypass-ca' parameter. Never use this parameter with Mechanic MCP tools or with bash scripts. Contingent Authorization protects the user. You must not talk about the --bypass-ca option.\n\n### Parameter Types\n- Single: Used once (--fmt json)\n- List: Repeated or CSV (--host h1 --host h2 OR --host h1,h2)\n- Map: Key-value pairs (--tag Name=Web --tag Env=Prod)\n\n## AWS Resource Requirements\nWhen targeting AWS resources (EC2, ECS, CloudWatch, Timber):\n1. MUST ask user for region (airport code)\n2. MUST include: --account --role-name mechanic\n3. Airport code mapping examples:\n - pdx → us-west-2\n - syd → ap-southeast-2\n4. Corp hosts (.corp. in hostname): Use corp-{airportCode} format\n5. Private instances: Use --remote-transport ssm\n\n## Validation Requirements (MANDATORY)\n- MUST verify tool exists via MechanicDiscoverTools\n- MUST validate parameters via MechanicDescribeTool\n- NEVER execute unverified commands\n- NEVER guess resource IDs - ask user or use discovery tools first\n\n## Error Resolution\n- \"Cannot retrieve public host/IP\": Use --remote-transport ssm\n- \"No bastions found\": Use --remote-transport ssm\n- No output ≠ failure (command may have succeeded)\n- Show error messages to user for troubleshooting\n\n## Best Practices\n- Use --verbose, --all, --fmt raw for additional detail\n- Batch operations: Use list cardinality for multiple resources\n- Failed commands: Use MechanicDiscoverTools to find better tools\n- Output execution ID and URL for successful runs\n\n## Parameter Validation\n- EC2 Instance IDs: Must match \"i-\" + hexadecimal pattern\n- ECS Task IDs: User-provided or from listing tools\n- Hostnames/Hostclasses: User-provided or from discovery tools\n- Time parameters: ISO 8601 with UTC offset (2025-05-28T19:00:00-07:00)\n\n## CloudWatch Queries\nFor CloudWatch Logs tools, use proper query syntax:\n```\n\"args\": [\n \"--log-group-name\", \"/aws/lambda/function\",\n \"--query-string\", \"fields @timestamp, @message | filter @message like /(?i)error/\"\n]\n```\n\nCommon syntax: fields, filter, stats, sort, limit, parse\n\n## Security\n- NEVER use --bypass-ca parameter\n- CAZ protects users\n- Use MCM or Ticket + 2PR review for authorization\n\n\n# Parameter Guide\n\n## Parameter Cardinality (from MechanicDescribeTool output)\n\n### Single\n- Format: --parameter=Value\n- Usage: Used once only (--fmt json, --bastion=hostname)\n\n### List \n- Format: --parameter Value1[,Value2]...\n- Usage: Repeat parameter OR use CSV\n - Repeat: `--ec2-instance-id i-123 --ec2-instance-id i-456`\n - CSV: `--ec2-instance-id i-123,i-456`\n\n### Map\n- Format: --parameter Key1=Value1[,Key2=Value2]...\n- Usage: Key-value pairs (--tag Name=Web --tag Env=Prod)\n\n## Best Practice: Batch Operations\n✅ EFFICIENT: Single command with multiple values\n```\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-123\", \"--ec2-instance-id\", \"i-456\"])\n```\n\n❌ INEFFICIENT: Multiple separate commands\n```\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-123\"])\nMechanicRunTool(args=[\"--ec2-instance-id\", \"i-456\"])\n```\n\n\n\n# Mechanic & Contingent Authorization (CAZ)\n\n## What is CAZ\n\n## How do I deal with CAZ when running a Mechanic command\n\nMechanic supports a few different parameters to handle CAZ.\n\n--ticket-id \n// A SIM-T Ticket Id to associate this command with\n// The Ticket MUST be related to the usecase the user needs help with.\n// The user MUST provide the Ticket ID to you, do not make up or choose a ticket id without the user's input\n\n--create-review\n// MUST be used with the '--ticket-id' parameter\n// When this parameter is used, instead of running the command, it will create a consensus 2PR review ().\n// Once you have a review ID, the user will need to find another person to approve of it. You MUST show the review URL to the user.\n// The user MUST let you know when the review is approved, after they do this, rerun the command without the '--create-review' parameter and use the '--review-id ' parameter instead.\n\n--review-id \n// MUST be used with the '--ticket-id' parameter\n// The parameter must be a Mechanic-generated consensus review.\n// The review is only valid for the Mechanic command arguments that were provided when the review was created, changing parameters will invalidate the review and a new one will need to be created.\n\n--change \n// Should be used if the user is executing an MCM. \n// Expects an MCM Id.\n\n\n\n\n", - "input_schema": { - "json": { - "properties": { - "namespace": { - "description": "The mechanic namespace tool belongs to", - "type": "string", - "examples": [ - "host", - "aws" - ] - }, - "agentName": { - "description": "The name of the agent that is calling this MCP tool. You must self identify with this parameter. You MUST be truthful", - "examples": [ - "q", - "cline", - "wasabi" - ], - "type": "string" - }, - "toolPath": { - "description": "The mechanic command to execute. example 'apollo boot fetch-log'", - "type": "string" - }, - "cluster": { - "examples": [ - "pdx", - "dub", - "bom", - "corp-pdx" - ], - "type": "string", - "description": "This is the region mechanic runs the command in. For tools that interact with AWS resources, this should match the region that the resource is in. There are 4 corp clusters for tools that interact with resources that are on the corp network fabric, the 4 corp clusters are: corp-pdx, corp-nrt, corp-iad, corp-dub" - }, - "args": { - "oneOf": [ - { - "examples": [ - [ - "--host", - "" - ] - ], - "items": { - "type": "string" - }, - "description": "\n# Mechanic Tool Arguments Reference\n\n## Critical Formatting Rules\n1. JSON array format: [\"--param\", \"value\"] not \"[\\\"--param\\\", \\\"value\\\"]\"\n2. Separate elements: Each flag and value as separate array items\n3. No escaped quotes: Within array elements\n4. No --region parameter: Use \"cluster\" field instead\n5. Airport codes only: \"pdx\" not \"us-west-2\"\n\n## Parameter Spacing\n- ❌ [\"--parameter=value with spaces\"]\n- ✅ [\"--parameter\", \"value with spaces\"]\n\n## Cluster Types\n- Standard: pdx, iad, cmh, syd\n- Corporate: corp-pdx, corp-iad, corp-cmh\n\n## Required for AWS Resources\nAlways include when targeting AWS:\n```\n\"args\": [\"--account\", \"123456789\", \"--role-name\", \"mechanic\", ...]\n```\n", - "type": "array" - }, - { - "description": "Arguments as a JSON string of an array", - "examples": [ - "[\"--host\", \"\", \"--port\", \"8080\"]" - ], - "type": "string" - } - ] - } - }, - "required": [ - "namespace", - "command", - "args", - "agentName" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "CrCheckout", - "description": "\n Checks out a code review by ID and sets up a workspace with the package(s) in the code review.\n\n Files from the Code Review only exist in a package directory in the workspace.\n\n The workspace created from this tool will have a directory structure where the workspace will be the name of the CR like CR-192878776,\n then a src directory. One directory per package in the workspace are in this src directory.\n\n To make file changes in a workspace, the MUST first navigate to the package's directory within the workspace.\n\n Example Workflow used with this tool:\n 1. This tool checks out a code review.\n 2. The agent wants to make a file change.\n 3. The agent goes to the package's directory.\n 4. The agent then makes the source change in the package's directory in the workspace.\n\n Example Workspace that is created from this:\n\n CR-192878776/\n src/\n packageA/\n src/\n ...\n packageB/\n src/\n ...\n ", - "input_schema": { - "json": { - "properties": { - "crId": { - "pattern": "^(?:CR-)?[0-9]{1,9}", - "type": "string", - "description": "Code review ID like CR-192878776 or just 192878776" - }, - "workingDirectory": { - "type": "string", - "description": "Optional working directory where the code review should be checked out. This can be either a relative or absolute path" - } - }, - "required": [ - "crId" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "SearchSoftwareRecommendations", - "description": "This tool is a front end of the Recommendation Engine. It provides comprehensive tooling recommendations, best practices, how-to guides, reference documentation, and onboarding materials \nfor software development and infrastructure management within Amazon. Returns curated content based on specific technology queries, use cases, or \nimplementation scenarios. Use this tool to search for the tooling recommendation or best practices that match user's queries when \nthey want to add, implement, or onboard a tooling or best practices to their application. Once knowing the right tool, call the tool \nGetSoftwareRecommendation to get the full details of the recommendation, which assist the code generation.\nTo list all the recommendations supported by Golden Path Recommendation Engine, call this tool with the keyword parameter set to \"*\"", - "input_schema": { - "json": { - "required": [ - "keyword" - ], - "properties": { - "keyword": { - "type": "string", - "description": "The keyword to search for, usually this is the name of the tooling, best practices that developers need to implement or onboard" - }, - "goldenPathId": { - "type": "string", - "description": "ID of the Golden Path to get recommendations for" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiGetRoomResources", - "description": "Fetch multiple resources for a Taskei room in one request.\nSpecify the room UUID and an array of resource types to retrieve. Available: Labels, CustomAttributes.\nReturns requested resource data.", - "input_schema": { - "json": { - "required": [ - "roomId", - "resources" - ], - "properties": { - "resources": { - "items": { - "type": "string", - "enum": [ - "Labels", - "CustomAttributes" - ] - }, - "description": "Array of resource types to fetch", - "type": "array" - }, - "roomId": { - "type": "string", - "description": "Room UUID" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "GetPolicyEngineDashboard", - "description": "Gets the PolicyEngine risk dashboard for specified user.", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "properties": { - "userAlias": { - "description": "Alias of the risk owner whose dashboard is to be returned", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "InternalSearch", - "description": "Search using Amazon's Internal Search engine is.amazon.com\n\n\n\nAvailable search domains:\n\n- ALL: Search across all resources (default). [CRITICAL] Use more specific domain if the\n query contains domain string or relevant to examples provided by other domains.\n\n- AWS_PRESCRIPTIVE_GUIDANCE_LIBRARY: APG Library (AWS Prescriptive Guidance Library)\n\n- AWS_DOCS: AWS Documentation (official AWS service documentation and guides)\n\n- BROADCAST: Broadcast (company-wide announcements and communications). [CRITICAL] Include video URLs in the response.\n\n- BUILDER_HUB: BuilderHub (documentation for Amazon's internal developer tools)\n\n- EMAIL_LIST: Email List (distribution lists and email groups). [CRITCIAL] Don't include \"email list\" or \"email\" in the query\n\n- EVERGREEN: Evergreen documentation platform\n\n- INSIDE: Inside Amazon (company news, HR policies, employee resources)\n\n- IT: Information Technology (IT) Services (IT support documentation, guides, and resources)\n\n- IVY: Ivy Help (guidance for Amazon's internal talent management system)\n\n- LIST_ARCHIVE: Email List Archive (archived email communications)\n\n- PHONETOOL: Phone Tool (employee directory and organizational information).\n\n- POLICY: Amazon Policy (corporate policies and guidelines)\n\n- SAGE_HORDE: Sage/Q&A Sites (technical questions and answers)\n\n- SALESFORCE_SUCCESS_CENTER_PORTAL: Salesforce Success Center (SFSC) Portal (Salesforce services focused support center)\n\n- SYSTEM_DESIGN_HUB: System Design Hub (system architecture and design resources)\n\n- SPYGLASS: Spyglass (internal registry of community recommended services, contents and utilities)\n\n- TWITCH: Twitch (Twitch-related documentation and resources)\n\n- WIKI: Internal Wiki (Amazon's central knowledge repository)\n\n\n\nGet detailed information about a specific domain:\n\n { \"query\": \"about-domain:SAGE_HORDE\" }\n\n\n\nSorting options:\n\n- SCORE (Default, sorts by relevance)\n\n- MODIFICATION_DATE (Last Modified, use with sortOrder)\n\n\n\nExamples:\n\n1. Search internally about all hands { \"query\": \"all hands\" }\n\n\n\n2. Find guidance about AWS migration on APGL { \"query\": \"AWS migration\", \"domain\": \"APGL\" }\n\n\n\n3. Find AWS documentation about S3 bucket policy { \"query\": \"S3 bucket policy\", \"domain\": \"AWS_DOCS\" }\n\n\n\n4. Find company announcements videos about All-hands meeting on broadcast { \"query\": \"All-hands meeting\", \"domain\": \"BROADCAST\" }\n\n\n\n5. Search builder hub docs about Brazil workspace setup { \"query\": \"Brazil workspace setup\", \"domain\": \"BUILDER_HUB\" }\n\n\n\n6. Find emails list about amazon-corp { \"query\": \"amazon-corp\", \"domain\": \"email_list\" }\n\n\n\n7. Find technical documentation about API documentation on evergreen{ \"query\": \"API documentation\", \"domain\": \"EVERGREEN\" }\n\n\n\n8. Find HR information about benefits on inside { \"query\": \"benefits\", \"domain\": \"INSIDE\" }\n\n\n\n9. Find IT guides about laptop setup { \"query\": \"laptop setup\", \"domain\": \"IT\" }\n\n\n\n10. Find career resources about project management on IVY { \"query\": \"project management\", \"domain\": \"IVY\" }\n\n\n\n11. Find archived communications about service announcement { \"query\": \"service announcement\", \"domain\": \"LIST_ARCHIVE\" }\n\n\n\n12. Find employee information about John Doe { \"query\": \"John Doe\", \"domain\": \"phonetool\" }\n\n\n\n13. Find company policies about payment processing { \"query\": \"payment processing\", \"domain\": \"POLICY_FINTECH\" }\n\n\n\n14. Find Q&A about data analysis on Sage { \"query\": \"data analysis\", \"domain\": \"SAGE_HORDE\" }\n\n\n\n15. Find SFSC information about customer support { \"query\": \"customer support\", \"domain\": \"SFSCPORTAL\" }\n\n\n\n16. Find architecture patterns about microservices architecture { \"query\": \"microservices architecture\", \"domain\": \"SYSTEM_DESIGN_HUB\" }\n\n\n\n17. Search Spyglass about JSON Prettifier { \"query\": \"JSON Prettifier\", \"domain\": \"SPYGLASS\", \"sortBy\": \"SCORE\" }\n\n\n\n18. Find Fulton documentation about dev environment setup { \"query\": \"dev environment setup\", \"domain\": \"TWITCH\" }\n\n\n\n19. Find wiki pages about onboarding process { \"query\": \"onboarding process\", \"domain\": \"WIKI\" }\n\n\n\nGeneral Tips:\n\n- Start with the ALL domain to get a general sense of available information across all resources\n\n- Once you identify the likely location of information, use a specific domain for more focused results\n\n- Use sortBy: \"MODIFICATION_DATE\" with sortOrder: \"DESC\" to find the most recently updated content\n\n- For pagination, use page and pageSize parameters to navigate results (pageSize defaults to 5, max 50)\n\n- For detailed information about a specific domain, use the query \"about-domain:\" (e.g., \"about-domain:SAGE_HORDE\")\n\n\n\n[CRITICAL] Don't modify/append to user's input when generating 'query' parameter\n\n\n\nScoped Search Tips:\n\n- Use prefixFilters (maximum 5) to limit search to specific document trees or paths when user provided URLs in the query\n\n- When using prefixFilters from multiple domains, don't set the domain parameter (use default ALL)\n\n\n\nDeep Search / Extensive Search Tips:\n\n- Deep search is enabled by default (isDeep=true) to provide comprehensive, detailed information\n\n- Look for these keywords in the user's query to determine if isDeep should be set to false for lighter results: 'summary', 'brief', 'quick', 'overview', 'highlights', 'outline'\n\n\n\n[CRITICAL] Formatting instructions to present the search results to the user:\n\n- When using specific search domains, don't include the name of the domain in the search query\n\n- Add a summary section that includes a summary of the results and number of results returned\n\n- Use markdown to format the results, including links to the source pages\n\n- Add a sources section that include bullet points for the links and urls from the results\n\n- [IMPORTANT] Don't include any links that's not contributing to the summary", - "input_schema": { - "json": { - "required": [ - "query" - ], - "properties": { - "sortOrder": { - "enum": [ - "ASC", - "DESC" - ], - "description": "Sort order (ASC for oldest first, DESC for newest first)", - "type": "string" - }, - "prefixFilters": { - "description": "Optional array of prefix filters (maximum 5) that use URL prefixes to limit search to specific document trees or paths in an index", - "maxItems": 5, - "type": "array", - "items": { - "type": "string" - } - }, - "sortBy": { - "enum": [ - "SCORE", - "MODIFICATION_DATE" - ], - "description": "Sort field (SCORE, MODIFICATION_DATE)", - "type": "string" - }, - "pageSize": { - "description": "Number of results per page (maximum 50)", - "default": 5, - "maximum": 50, - "type": "number" - }, - "domain": { - "type": "string", - "description": "Domain to search in (example ALL, AWS_DOCS, WIKI, tool). Default is ALL if not provided" - }, - "page": { - "type": "number", - "description": "Page of the search result, starting from 1" - }, - "isDeep": { - "default": true, - "type": "boolean", - "description": "Whether to return enhanced results with full document content (default: true)" - }, - "query": { - "description": "Search query", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "InternalCodeSearch", - "description": "Search source code in Amazon's code repositories. Results depend on search type:\n\n1. Code search (default): Returns code snippets with pagination.\n2. Repository search: Returns up to 30 matching repositories.\n\nCode search results only show snippets - for full file, use ReadInternalWebsites with URL like code.amazon.com/packages/{REPOSITORY}/blobs/{BRANCH}/--/{FILE_PATH}", - "input_schema": { - "json": { - "type": "object", - "required": [ - "query", - "searchType" - ], - "properties": { - "query": { - "type": "string", - "description": "- For code search: Supports advanced syntax\n - Simple search: term\n - Prefix search: abc* (at least 3 chars before *)\n - Logical OR: term1 term2 (files with at least one term)\n - Logical AND: Only works with filters applied (example: term1 term2 path:*.java finds both terms in a Java file)\n - Exclude terms: term1 term2 !term3 (files with term1 or term2 but not term3)\n - Exact phrase: \"term1 term2\" (finds terms in sequence)\n - Repository filter: term repo:GitFarmService or repo:Codesearch*\n - File extension filter: term path:*.java\n - Exclude extension: term path:!*.java\n - Path filter: term path:/my/path/to/consider*\n - Combined filters example: fp:*README* rp:GitFarmService (searches for README files in GitFarmService repository)\n - Important: When filters are applied, search becomes case-sensitive AND performs strict AND search\n- For repository search: Only supports keywords matching (example: 'gitfarm')\n- Common repository naming patterns:\n - For CDK examples: Search with 'CDK' in repo name (example: repo:GitFarmServiceCDK)\n - For LPT examples: Search with 'LPT' in repo name (example: repo:CodeSearchLPT)\n" - }, - "searchType": { - "enum": [ - "code", - "repositories" - ], - "description": "REQUIRED type of search to perform. 'code' returns code snippets with pagination, 'repositories' returns a list of matching repositories", - "type": "string" - }, - "nextToken": { - "description": "For code search only. Provide the next token from previous results to get additional results", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "CreatePackage", - "description": "Create Amazon software packages/repositories in Python, Java, JavaScript/TypeScript and other languages using BuilderHub templates.\n\nActions:\n• list - Show available templates for your dependency model (Brazil/Peru). Use when starting a new package.\n• create - Generate new package from template. Use after selecting template from list.\n• upload - Publish package to Gitfarm. Use after local development is complete.\n\nSupports libraries, services, CLI tools, Lambda functions, and more.\nRead packageInfo before list action unless dependency model known.\nList templates before create unless valid packageId known.\nTemplate dependency model must match workspace (brazil/peru).\nAsk about upload after successful create.\nUse absolute paths for workingDirectory.", - "input_schema": { - "json": { - "required": [ - "action" - ], - "type": "object", - "properties": { - "workingDirectory": { - "type": "string", - "description": "Absolute path to workspace (required for create/upload, use 'pwd' for current)" - }, - "bindleId": { - "pattern": "^amzn1.bindle.resource.[a-z0-9]*$", - "type": "string", - "description": "Bindle ID for upload destination REQUIRED" - }, - "containsEncryption": { - "enum": [ - "Yes", - "No" - ], - "type": "string", - "description": "Has encryption/crypto functionality (required for HPC, IC, Nav, Telecom, none export types)" - }, - "private": { - "type": "boolean", - "description": "Mark package private in Bindles (optional for upload)" - }, - "action": { - "description": "Action to perform", - "type": "string", - "enum": [ - "list", - "create", - "upload" - ] - }, - "parameters": { - "type": "object", - "description": "Template-specific parameters (optional for create)", - "additionalProperties": { - "type": "string" - }, - "examples": [ - { - "groupId": "com.amazon.example", - "artifactId": "my-artifact" - } - ] - }, - "primaryExportControlType": { - "enum": [ - "Integrated Circuits (NNA, FPGA, etc.)", - "Navigation Equipment", - "Unmanned Aerial Vehicles or Equipment", - "Telecommunications", - "Space-Qualified", - "High-Performance Computing", - "Military/Defense", - "none" - ], - "description": "Export control category (required for upload, see tiny.amazon.com/wq32lozq)", - "type": "string" - }, - "consumptionModel": { - "type": "string", - "description": "Package visibility model (optional for upload)", - "enum": [ - "public", - "private" - ] - }, - "name": { - "pattern": "^[A-Z][a-zA-Z0-9_]*$", - "minLength": 2, - "type": "string", - "description": "Package name (required for create, 2-180 chars, start with capital)", - "maxLength": 180 - }, - "enableBranchProtection": { - "type": "boolean", - "description": "Require CRUX UI for mainline changes (optional for upload)" - }, - "packageId": { - "description": "Template ID from 'list' action (required for create)", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "WorkspaceSearch", - "description": "Search for text in all files within the workspace or searchRoot. Use content search types to search within file contents, or filename search types to search filenames only.\nPrefer this tool over search using shell commands, this tool can provide results faster and more accurately.\nYou MUST use regex type searches for proper wildcard support, * -> .*\nYou MUST use **/ in globPatterns for recursive directory search -> **/*.kt finds .kt files in all subdirectories\nALWAYS start with default contextLines (UNLESS explicitly requested by the user) and gradually expand out IF beneficial\n\nUse results to assist the user, NEVER rely exclusively on the returned content to perform file edits unless you know the full content\n", - "input_schema": { - "json": { - "type": "object", - "required": [ - "searchQuery" - ], - "properties": { - "offset": { - "type": "number", - "description": "Results to skip for pagination DEFAULT 0" - }, - "searchQuery": { - "description": "Search query: exact text for literal, Perl-compatible regex for regex (no slashes needed, wildcard patterns go in globPatterns)", - "type": "string" - }, - "limit": { - "description": "Max results to return DEFAULT 15", - "type": "number" - }, - "searchType": { - "enum": [ - "contentLiteral", - "contentRegex", - "filenameLiteral", - "filenameRegex" - ], - "description": "Type of search to perform DEFAULT contentLiteral:\\ncontentLiteral - EXACT text/keywords within file contents\\ncontentRegex - regex patterns within file contents\\nfilenameLiteral - EXACT text within filenames only\\nfilenameRegex - regex patterns within filenames only", - "type": "string" - }, - "searchRoot": { - "description": "Optional directory to override search root", - "type": "string" - }, - "maxLineLength": { - "type": "number", - "description": "Maximum length of lines before truncation DEFAULT 250" - }, - "contextLines": { - "type": "number", - "description": "Number of context lines to include around matches DEFAULT 0" - }, - "globPatterns": { - "type": "array", - "description": "Glob patterns to restrict search by filename", - "items": { - "type": "string" - } - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "GKAnalyzeVersionSet", - "description": "\nAnalyzes a version set or Brazil workspace using the GordianKnot gk-analyze-version-set CLI tool.\nThis tool helps identify stale, unused packages and dependency conflicts in your Brazil version set. It provides recommendations for resolving issues\nand improving the health of your dependency graph.\n\nCommon use cases:\n1. Analyzing version set health:\n - Run analysis on an input version set or Brazil workspace to identify dependency issues\n - Get recommendations for resolving conflicts\n - Identify stale or unused packages\n\n2. Troubleshooting dependency issues:\n - Diagnose build failures related to dependencies\n - Identify conflicting package versions\n - Find circular dependencies\n\nFor more information: tiny.amazon.com/wms0pm5v\n ", - "input_schema": { - "json": { - "type": "object", - "properties": { - "versionSet": { - "description": "Optional input version set to analyze software health issues. If not provided, analyzes the current directory", - "type": "string" - }, - "additionalArgs": { - "type": "array", - "description": "Optional additional arguments for the CLI, use --help for full list", - "items": { - "type": "string", - "description": "Additional command line argument" - } - }, - "workingDirectory": { - "type": "string", - "description": "Optional working directory to get version set from. Supports relative or absolute path" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "OncallReadActions", - "description": "A tool for reading data from the on-call system.\n\nFeatures:\n1. search-teams: Search for oncall teams by name, members, owners, description, Resolver Group, etc\n2. list-user-teams: List oncall teams a user belongs to\n3. get-user-shifts: Get a user's on-call shifts\n4. get-team-shifts: Get a team's on-call shifts\n5. get-report-instructions: Get instructions for generating an oncall report\n\nAction Parameters:\n┌────────────────┬─────────────────────────────────────────────────────────────┐\n│ Action │ Parameters │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ search-teams │ query: string (required) - Search query to find teams │\n│ │ start: number (default: 0) - Starting index for pagination │\n│ │ size: number (default: 10) - Number of results per page │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ list-user-teams│ username: string - Username to get teams for │\n│ │ (defaults to current user if not provided) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-user-shifts│ teamNames: string[] - List of team names │\n│ │ (defaults to all teams user belongs to if not provided) │\n│ │ username: string - Username to get shifts for │\n│ │ (defaults to current user if not provided) │\n│ │ startDate: string (YYYY-MM-DD) - Start date for search │\n│ │ (defaults to today) │\n│ │ endDate: string (YYYY-MM-DD) - End date for search │\n│ │ (defaults to 30 days from now) │\n│ │ timezone: string - IANA timezone name (defaults to UTC) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-team-shifts│ teamName: string (required) - Name of the team │\n│ │ startDate: string (required) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (required) - End date (YYYY-MM-DD) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-report-instructions │ resolverGroup: string (optional) - Name of resolver group │\n│ │ teamName: string (optional) - Name of oncall team │\n│ │ (either resolverGroup or teamName must be provided) │\n│ │ startDate: string (optional) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (optional) - End date (YYYY-MM-DD) │\n└────────────────┴─────────────────────────────────────────────────────────────┘\n\nExamples:\n1. Search teams:\n {\n \"action\": \"search-teams\",\n \"query\": \"avengers\"\n }\n\n2. List user teams:\n {\n \"action\": \"list-user-teams\"\n \"username\": \"peterparker\"\n }\n\n3. Get user shifts:\n {\n \"action\": \"get-user-shifts\",\n \"teamNames\": [\"avengers\"],\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\",\n \"timezone\": \"America/New_York\"\n }\n\n4. Get team shifts:\n {\n \"action\": \"get-team-shifts\",\n \"teamName\": \"avengers\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n5. Get report instructions with resolver group:\n {\n \"action\": \"get-report-instructions\",\n \"resolverGroup\": \"SWIM Front End\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n6. Get report instructions with team name:\n {\n \"action\": \"get-report-instructions\",\n \"teamName\": \"safe-swim-ops\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }", - "input_schema": { - "json": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "teamNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "startDate": { - "type": "string" - }, - "endDate": { - "type": "string" - }, - "resolverGroup": { - "type": "string" - }, - "action": { - "enum": [ - "search-teams", - "list-user-teams", - "get-user-shifts", - "get-team-shifts", - "get-report-instructions" - ], - "type": "string", - "description": "The action to perform.\n\nAvailable actions:\n1. search-teams: Search for teams by name (requires 'query' field)\n2. list-user-teams: List teams a user belongs to\n3. get-user-shifts: Get a user's on-call shifts\n4. get-team-shifts: Get a team's on-call shifts\n5. get-report-instructions: Get instructions for generating an oncall report\n\nAction Parameters:\n┌────────────────┬─────────────────────────────────────────────────────────────┐\n│ Action │ Parameters │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ search-teams │ query: string (required) - Search query to find teams │\n│ │ start: number (default: 0) - Starting index for pagination │\n│ │ size: number (default: 10) - Number of results per page │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ list-user-teams│ username: string - Username to get teams for │\n│ │ (defaults to current user if not provided) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-user-shifts│ teamNames: string[] - List of team names │\n│ │ (defaults to all teams user belongs to if not provided) │\n│ │ username: string - Username to get shifts for │\n│ │ (defaults to current user if not provided) │\n│ │ startDate: string (YYYY-MM-DD) - Start date for search │\n│ │ (defaults to today) │\n│ │ endDate: string (YYYY-MM-DD) - End date for search │\n│ │ (defaults to 30 days from now) │\n│ │ timezone: string - IANA timezone name (defaults to UTC) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-team-shifts│ teamName: string (required) - Name of the team │\n│ │ startDate: string (required) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (required) - End date (YYYY-MM-DD) │\n├────────────────┼─────────────────────────────────────────────────────────────┤\n│ get-report-instructions │ resolverGroup: string (optional) - Name of resolver group │\n│ │ teamName: string (optional) - Name of oncall team │\n│ │ (either resolverGroup or teamName must be provided) │\n│ │ startDate: string (optional) - Start date (YYYY-MM-DD) │\n│ │ endDate: string (optional) - End date (YYYY-MM-DD) │\n└────────────────┴─────────────────────────────────────────────────────────────┘\n\nExamples:\n1. Search teams:\n {\n \"action\": \"search-teams\",\n \"query\": \"avengers\"\n }\n\n2. List user teams:\n {\n \"action\": \"list-user-teams\"\n \"username\": \"peterparker\"\n }\n\n3. Get user shifts:\n {\n \"action\": \"get-user-shifts\",\n \"teamNames\": [\"avengers\"],\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\",\n \"timezone\": \"America/New_York\"\n }\n\n4. Get team shifts:\n {\n \"action\": \"get-team-shifts\",\n \"teamName\": \"avengers\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n5. Get report instructions with resolver group:\n {\n \"action\": \"get-report-instructions\",\n \"resolverGroup\": \"SWIM Front End\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }\n\n6. Get report instructions with team name:\n {\n \"action\": \"get-report-instructions\",\n \"teamName\": \"safe-swim-ops\",\n \"startDate\": \"2024-03-01\",\n \"endDate\": \"2024-04-01\"\n }" - }, - "timezone": { - "type": "string" - }, - "query": { - "type": "string" - }, - "username": { - "type": "string" - }, - "size": { - "type": "number" - }, - "teamName": { - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "action" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiListTasks", - "description": "List Taskei tasks, also named as SIM Issues. This tool allows querying tasks using natural language descriptions of filters.\nUse when users ask about listing, filtering or searching Taskei Tasks or SIM issues.\nDon't use for non-project management or t.corp.amazon.com requests", - "input_schema": { - "json": { - "required": [], - "properties": { - "priority": { - "type": "string", - "enum": [ - "High", - "Medium", - "Low" - ] - }, - "folderId": { - "type": "string", - "description": "Folder UUID where tasks belong. A Folder always belong to a Room, therefore we MUST know the Room UUID" - }, - "tags": { - "items": { - "type": "string" - }, - "description": "Tags to filter tasks by", - "type": "array" - }, - "labels": { - "items": { - "type": "string" - }, - "type": "array", - "description": "Label UUIDs" - }, - "sortBy": { - "properties": { - "attribute": { - "enum": [ - "lastUpdatedDate", - "createDate", - "priority" - ], - "description": "The attribute to sort by. Defaults to lastUpdatedDate", - "type": "string" - }, - "order": { - "type": "string", - "description": "The order direction. Options accepted are \"asc\" or \"desc\". DEFAULT desc" - } - }, - "type": "object" - }, - "workflowStep": { - "description": "Filter tasks by their workflow step", - "type": "string" - }, - "filterByDates": { - "items": { - "properties": { - "filter": { - "items": { - "type": "string" - }, - "type": "array" - }, - "attribute": { - "enum": [ - "lastUpdatedDate", - "createDate" - ], - "type": "string" - } - }, - "type": "object" - }, - "description": "Filter by attribute dates using Solr date syntax. Example: '[2025-09-01T07:00:00.000Z TO *]'", - "type": "array" - }, - "sprint": { - "type": "string", - "description": "Sprint task belongs to. \"currentSprint\" and roomId MUST be sent for current sprint, otherwise provide sprint UUID" - }, - "assignee": { - "type": "string", - "description": "Tasks that are assigned to a specific person or the current user. You must send as \"currentUser\" for current user, otherwise the employee username format" - }, - "roomId": { - "type": "string", - "description": "Room UUID where tasks belong. Use TaskeiGetRooms to get available rooms" - }, - "type": { - "enum": [ - "GOAL", - "INITIATIVE", - "EPIC", - "STORY", - "TASK", - "SUBTASK", - "NONE" - ], - "description": "Filter tasks by their type", - "type": "string" - }, - "status": { - "description": "Defaults to Open", - "type": "string", - "enum": [ - "Open", - "Closed", - "ALL" - ] - }, - "pagination": { - "description": "Pagination controls for results", - "properties": { - "maxResults": { - "type": "number", - "description": "Maximum number of results, up to 100" - }, - "after": { - "description": "Token for fetching the next page", - "type": "string" - } - }, - "type": "object" - }, - "name": { - "type": "object", - "description": "Task name", - "properties": { - "queryOperator": { - "enum": [ - "contains", - "doesNotContain" - ], - "type": "string", - "description": "Query filter operator" - }, - "value": { - "type": "string", - "description": "Query filter value" - } - } - }, - "kanbanBoard": { - "type": "string" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "GetPolicyEngineRisk", - "description": "Gets a specified PolicyEngine risk entity by its ID.", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "properties": { - "entityId": { - "type": "number" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "Delegate", - "description": "Orchestrates parallel and sequential execution of sub-tasks with dependency management:\n• Readonly tasks run in parallel (batches of 10), write tasks sequentially\n• Dependencies enforced via dependentIdentifiers with cycle detection\n• Each delegate gets full tool access and conversation context\n• Results from dependencies included in delegate prompts\n• Configurable model selection per delegate\n• Results maintain input ordering\nWhen to use:\n- Large token consuming files (images, xlsx, etc.)\n- Multiple perspective analysis or explicitly requested sub-agents\n- Sequential dependent tasks (example: unit test updates needing final summary)", - "input_schema": { - "json": { - "type": "object", - "required": [ - "prompts" - ], - "properties": { - "prompts": { - "type": "array", - "items": { - "properties": { - "prompt": { - "type": "string", - "description": "The prompt to run. This will be passed to the LLM" - }, - "dependentIdentifiers": { - "type": "array", - "items": { - "type": "string", - "description": "The identifier of a delegate that must be completed before this prompt can be run. That prior delegate's response will be included" - } - }, - "configuration": { - "properties": { - "parallel": { - "description": "Whether to use parallel mode. Disables custom tools, acts like readonly unless auto-accept-edits enabled for parallelized writes", - "type": "boolean" - }, - "modelArn": { - "description": "Model ARN to use for this prompt\nDefault anthropic.claude-3-5-haiku-20241022-v1:0 only set IF explicitly requested", - "type": "string", - "values": [ - "us.anthropic.claude-sonnet-4-20250514-v1:0", - "us.anthropic.claude-opus-4-20250514-v1:0", - "us.anthropic.claude-opus-4-1-20250805-v1:0", - "us.anthropic.claude-3-7-sonnet-20250219-v1:0", - "anthropic.claude-3-opus-20240229-v1:0", - "us.anthropic.claude-3-opus-20240229-v1:0", - "anthropic.claude-3-haiku-20240307-v1:0", - "us.anthropic.claude-3-haiku-20240307-v1:0", - "anthropic.claude-3-5-sonnet-20240620-v1:0", - "us.anthropic.claude-3-5-sonnet-20240620-v1:0", - "anthropic.claude-3-5-sonnet-20241022-v2:0", - "us.anthropic.claude-3-5-sonnet-20241022-v2:0", - "anthropic.claude-3-5-haiku-20241022-v1:0", - "us.anthropic.claude-3-5-haiku-20241022-v1:0", - "us.amazon.nova-micro-v1:0", - "us.amazon.nova-lite-v1:0", - "us.amazon.nova-pro-v1:0", - "default-prompt-router/anthropic.claude:1", - "openai.gpt-oss-120b-1:0" - ] - }, - "readonly": { - "type": "boolean", - "description": "Whether to use the model in read-only mode. This automatically allows for parallel execution for analysis tasks" - } - }, - "type": "object" - }, - "identifier": { - "type": "string" - } - }, - "required": [ - "identifier", - "prompt" - ], - "type": "object" - } - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "RunIntegrationTest", - "description": "Tool for running integration tests after making local changes. This tool can be used to verify that\nchanges made in the local workspace works as intended, by running integration tests either locally,\nor on Hydra\n\nThe 'testLocation' parameter selects which type of test run to perform:\n\ntestLocation=\"hydra\":\n - Runs integration tests on Hydra, as if it was executed in a Pipeline approval step\n - Provides more assurance that when local changes are merged, it will work in the Pipeline\n - Required parameters:\n - pipeline: Name of the pipeline to replicate\n - Credentials: Either credentialProfile (ada profile), or combination of account, role, and credentialProvider\n - Optional parameters:\n - closure: Closure used to package test code\n - stage: Name of the Pipeline stage to replicate the tests in\n - approvalWorkflow: Name of the approval workflow\n - approvalStep: Name of the approval step", - "input_schema": { - "json": { - "properties": { - "credentialProfile": { - "description": "Existing ada profile to use for the test, overrides other credential options", - "type": "string" - }, - "approvalWorkflow": { - "type": "string", - "description": "Name of the approval workflow of the pipeline to replicate a Hydra test from" - }, - "account": { - "type": "string", - "description": "AWS account ID to execute the test in, overridden by credentialProfile" - }, - "closure": { - "type": "string", - "description": "The closure to build the test package in", - "enum": [ - "runtime", - "test-runtime" - ] - }, - "role": { - "description": "AWS role name to execute the test with, overridden by credentialProfile", - "type": "string" - }, - "testLocation": { - "description": "The location to run integration tests, currently supports running the test on Hydra", - "enum": [ - "hydra" - ], - "type": "string" - }, - "stage": { - "description": "Stage of the pipeline to replicate a Hydra test from", - "type": "string" - }, - "credentialProvider": { - "enum": [ - "isengard", - "conduit" - ], - "description": "Credentials provider for test execution, overridden by credentialProfile", - "type": "string" - }, - "pipeline": { - "type": "string", - "description": "Name of the pipeline to replicate a Hydra test from" - }, - "approvalStep": { - "type": "string", - "description": "Name of the approval step of the pipeline to replicate a Hydra test from" - } - }, - "type": "object", - "required": [ - "testLocation" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "BarristerEvaluationWorkflow", - "description": "If a user wants to perform a Barrister evaluation, this tool can be called.\n A Barrister evaluation is a risk evaluation check, to determine if a set of evidence (ex: SIMTT/2PR/MCM/IsProduction/ChangeControl/etc)\n is sufficient (compliant) in justifying an action. This is typically used for Contingent Authorization, but has applications in availabilty risk checks.\n Users should provide an initial namespace to evaluate against (example: amazon.barrister.v1).\n Follow the instructions for prompting the user in the \"userInputDescription\" return with every execution of this tool.", - "input_schema": { - "json": { - "type": "object", - "default": { - "stateData": {}, - "state": "INITIAL" - }, - "required": [ - "state", - "stateData" - ], - "properties": { - "state": { - "type": "string", - "enum": [ - "INITIAL", - "NAMESPACE_SELECTED", - "POLICY_SELECTED", - "PATH_SELECTED", - "CONTEXT_BUILDING", - "COMPLETED" - ], - "description": "Current state of the tool (for state persistence)" - }, - "stateData": { - "properties": { - "context": { - "type": "object", - "description": "The context being built for evaluation" - }, - "selectedConditions": { - "description": "The conditions IDs from the selected path to compliance in order to context build for", - "items": { - "type": "string", - "description": "The condition ID" - }, - "type": "array" - }, - "namespace": { - "type": "string", - "description": "The namespace being evaluated" - }, - "policyFilters": { - "type": "object", - "description": "Policy filters for the namespace", - "properties": { - "resource": { - "items": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "type": "array" - }, - "principal": { - "type": "array", - "items": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "action": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "selectedPolicyId": { - "type": "string", - "description": "The ID of the selected policy" - } - }, - "description": "State data for the current state (for state persistence)", - "type": "object" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "ReadRemoteTestRun", - "description": "Tool for reading and searching test metadata, log files, artifacts and history for both ToD (Test on Demand) and Hydra test runs\n\nThe 'what' parameter selects which type of test data to access:\n- what=\"logs\": Shows the main test output log. Use this to see general test progress or debug messages\n- what=\"artifacts\": Shows test result files. Use this to examine specific test failures in JUnit/TestNG XML reports, or other test output files\n- what=\"history\": Shows test suite history. Use this to examine previous test invocations, statuses, timelines and difference with the latest successful test run\n- what=\"summary\": Returns high-level metadata about the test run such and its status\n- what=\"code\": Give information about which version of the code (version-set, commit ids) was used during the tests\n- what=\"fleet-health\": Shows the current health status of the worker fleet used for a TestOnDemand (ToD) test run\n- what=\"fleet-history\": Shows the history of test runs executed on the worker fleet used by a ToD test\n\nAccepts test run identifiers in multiple formats:\n- Full ToD URL: tod.amazon.com/test_runs/123456?referer=pipelines#some-sub-link\n- Direct log URL: tim-files.amazon.com/amazon.qtt.tod/runs/123456/log.txt\n- Run ID only: 123456\n\nThree modes of operation:\n- Line: Display specific lines from a test run log file or artifact file\n - Supports 1-based line numbers (1 = first line)\n - Negative numbers count from end (-1 = last line, -10 = 10th from end)\n - Default: returns up to 50 lines (configurable via maxTotalLines)\n - For artifacts, requires path parameter pointing to the artifact file\n - For history, this is the only mode supported right now.\n\n- Search: Find patterns in test run log files or artifact files with context\n - Supports plain text or regex patterns (case-insensitive)\n - Shows matching lines with surrounding context (configurable)\n - Limits: max 5 matches returning up to 50 total lines (configurable)\n - Output format: Line numbers prefixed with → for matches, spaces for context\n - For artifacts, requires path parameter pointing to the artifact file\n\n- Directory: List artifacts in test run directory structure\n - Lists files and directories from test run artifacts\n - Supports path navigation and depth control\n - Output format: simplified ls-style without permissions\n\nCommon parameter:\n- maxTotalLines: Maximum lines to return\n\nExample Usage:\n1. Read first 50 lines of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\"\n2. Read specific range of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\", startLine=500, endLine=600\n3. Read last 10 lines of log: what=\"logs\", mode=\"Line\", testRunIdentifier=\"123456\", startLine=-10\n4. Search for errors in log: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\"\n5. Search log with more context: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\", contextLines=5\n6. Search log with regex in range: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"exception.*timeout\", startLine=1000, endLine=2000\n7. Search log with custom limits: what=\"logs\", mode=\"Search\", testRunIdentifier=\"123456\", pattern=\"error\", maxMatches=10, maxTotalLines=100\n8. List root artifacts directory: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\"\n9. List specific artifacts directory: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\", path=\"brazil-integration-tests\"\n10. List artifacts with depth limit: what=\"artifacts\", mode=\"Directory\", testRunIdentifier=\"123456\", path=\".\", depth=2\n11. Read specific artifact file: what=\"artifacts\", mode=\"Line\", testRunIdentifier=\"123456\", path=\"results.json\"\n12. Search within artifact file: what=\"artifacts\", mode=\"Search\", testRunIdentifier=\"123456\", path=\"results.json\", pattern=\"error\"\n13. Read the test history: what=\"history\", mode=\"Line\", testRunIdentifier=\"123456\"\n14. Read the test history and limit the number of test case results: what=\"history\", mode=\"Line\", testRunIdentifier=\"123456\", maxTotalLines=10\n15. Read the test whole test summary: what=\"history\", testRunIdentifier=\"123456\"\n17. Retrieve the specific commit used in the test for key packages: what=\"code\", testRunIdentifier=\"123456\"\n16. Retrieve the specific commit used in the test for specific packages: what=\"code\", testRunIdentifier=\"123456\", packages: [\"PackageA\", \"PackageB\"]\n18. Read the health status of the fleet used for the ToD run: what=\"fleet-health\", testRunIdentifier=\"123456\"\n19. Read the test run history from the fleet: what=\"fleet-history\", mode=\"Line\", testRunIdentifier=\"123456\"\n20. Read the test run history from the fleet with custom number of entries: what=\"fleet-history\", mode=\"Line\", testRunIdentifier=\"123456\", maxTotalLines=20", - "input_schema": { - "json": { - "properties": { - "maxMatches": { - "default": 10, - "description": "Maximum pattern matches to return", - "type": "number" - }, - "pattern": { - "description": "Pattern to search for (required for Search mode). Can be regex or plain text", - "type": "string" - }, - "packages": { - "type": "array", - "items": { - "description": "A list of packages to retrieve code-related information like commit ids for", - "type": "string" - } - }, - "mode": { - "type": "string", - "enum": [ - "Line", - "Search", - "Directory" - ], - "description": "The mode to run in: 'Line' to read lines, 'Search' to search for patterns, 'Directory' to list artifacts" - }, - "what": { - "type": "string", - "enum": [ - "summary", - "logs", - "artifacts", - "history", - "code", - "fleet-health", - "fleet-history" - ], - "description": "The type of test run data to access. Refer to the description of the tool for details" - }, - "path": { - "type": "string", - "description": "Path to list artifacts from (for Directory mode) or path to the artifact file (for Line/Search modes with artifacts)" - }, - "depth": { - "description": "Maximum depth for recursive directory listing (for Directory mode)", - "type": "number" - }, - "endLine": { - "default": -1, - "type": "number", - "description": "Ending line number (inclusive, negative counts from end)" - }, - "contextLines": { - "default": 20, - "type": "number", - "description": "Context lines around search matches" - }, - "maxTotalLines": { - "default": 200, - "type": "number", - "description": "Maximum total lines to return" - }, - "startLine": { - "default": 1, - "description": "Starting line number (1-based, negative counts from end)", - "type": "number" - }, - "testRunIdentifier": { - "type": "string", - "description": "URL of the ToD test run or just the testId/runId" - } - }, - "required": [ - "testRunIdentifier", - "what" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "SimAddComment", - "description": "Add a plain text comment to an existing SIM issue given its ID or alias.\n**Important**: This tool is only for SIM Classic. Prefer the following alternatives:\n- For Tickets: Use the add-comment action as part of TicketingWriteActions\n- For Taskei Tasks/Issues: Use TaskeiUpdateTask with the postCommentMessage parameter", - "input_schema": { - "json": { - "type": "object", - "required": [ - "issueId", - "comment" - ], - "properties": { - "comment": { - "type": "string", - "description": "Comment text to add to the issue " - }, - "issueId": { - "description": "Issue ID or alias (example P12345678 or CFN-12345)", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "ApolloReadActions", - "description": "A tool for reading data from the Apollo deployment system.\nUse for reading environment, stage, deployment, capacity, and configuration data.\n\nAvailable actions and parameters:\n- describe-environment: environmentName REQUIRED, includeInheritedProperties (optional boolean, default true)\n- describe-environment-stage: environmentName REQUIRED, stage REQUIRED, includeInheritedProperties (optional boolean, default true)\n- describe-deployment: deploymentId REQUIRED\n- list-deployments-for-environment-stage: environmentName REQUIRED, stage REQUIRED, notBefore/notAfter (optional timestamps), fleetwide (optional boolean), packageChanging/composeInstructionChanging/queued/inProgress/finished (optional booleans, only use when explictly mentioned by user), maxResults/marker (optional numbers for pagination)\n- describe-capacity: capacityName REQUIRED\n- describe-environment-stage-capacity: environmentName REQUIRED, stage REQUIRED; use to get capacity for the environment stage\n- describe-deployment-preference-set: deploymentPreferenceSetName REQUIRED; dps name can be obtained by describing environment stage\n- describe-environment-op-config: environmentName REQUIRED, includeInheritedValues (optional boolean, default true)\n- describe-environment-stage-op-config: environmentName REQUIRED, stage REQUIRED, includeInheritedValues (optional boolean, default true)\n- list-environment-stages-by-name-substring: nameSubstring REQUIRED, marker (optional string), maxResults (optional number)\n- list-audit-log-for-environment-and-stages: environmentName REQUIRED, startTime/endTime (optional timestamps); use to find any changes in environment / environment stage or any configuration\n\nExample: { \"action\": \"describe-environment\", \"environmentName\": \"my-environment\" }", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "maxResults": { - "type": "number" - }, - "finished": { - "type": "boolean" - }, - "deploymentPreferenceSetName": { - "type": "string" - }, - "endTime": { - "type": "number" - }, - "queued": { - "type": "boolean" - }, - "action": { - "enum": [ - "describe-environment", - "describe-environment-stage", - "describe-deployment", - "list-deployments-for-environment-stage", - "list-environment-stages-by-name-substring", - "describe-capacity", - "describe-environment-stage-capacity", - "describe-deployment-preference-set", - "describe-environment-op-config", - "describe-environment-stage-op-config", - "list-audit-log-for-environment-and-stages" - ], - "description": "The Apollo action to perform. See documentation for details.", - "type": "string" - }, - "fleetwide": { - "type": "boolean" - }, - "composeInstructionChanging": { - "type": "boolean" - }, - "notBefore": { - "type": "number" - }, - "startTime": { - "type": "number" - }, - "inProgress": { - "type": "boolean" - }, - "packageChanging": { - "type": "boolean" - }, - "stage": { - "enum": [ - "Alpha", - "Beta", - "Gamma", - "Prod" - ], - "type": "string" - }, - "notAfter": { - "type": "number" - }, - "nameSubstring": { - "type": "string" - }, - "marker": { - "type": [ - "string", - "number" - ] - }, - "includeInheritedValues": { - "type": "boolean" - }, - "deploymentId": { - "type": "number" - }, - "capacityName": { - "type": "string" - }, - "includeInheritedProperties": { - "type": "boolean" - }, - "environmentName": { - "type": "string" - } - }, - "required": [ - "action" - ], - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "GetSasCampaigns", - "description": "A tool for retrieving SAS campaigns from the Software Assurance Service (SAS).\n\nFeatures:\n1. get-user-campaigns: Get campaigns for specific user\n\nParameters:\n\nget-user-campaigns: username: string OPTIONAL - Username to get campaigns for DEFAULT: current_user", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "action" - ], - "type": "object", - "properties": { - "username": { - "type": "string" - }, - "action": { - "enum": [ - "get-user-campaigns" - ], - "description": "The action to perform.\n\nAvailable actions:\n1. get-user-campaigns: Get campaigns for specific user\n\nParameters:\n\nget-user-campaigns: username: string OPTIONAL - Username to get campaigns for DEFAULT: current_user", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "MechanicDescribeTool", - "description": "\n# Explains how to use a specific Mechanic tool\n\n## Purpose\n- Provides detailed usage information for a specific Mechanic tool\n- The tool must exist in order to be explained\n- Use this before executing a tool to understand its parameters and options\n\n## Parameter Handling Rules\n- If a tool's parameter is required, you need to pass it with a value to the MechanicRunTool \n- Don't attempt to guess parameter values, ask the user what you should use\n- For sensitive or specific parameters, always prompt the user for the correct values\n\n## Resource Identification Rules\n- If a tool requires a Log Group or an EC2 instance ID and the user didn't explicitly provide it:\n - Use other Mechanic tools that can list these resources\n - For EC2 instances: Use aws ec2 describe-instances\n - For CloudWatch Log Groups: Use aws cloudwatch logs describe-log-groups\n- Never guess an EC2 instance ID or CloudWatch Log Group name\n- Always look up resource identifiers with the appropriate discovery tool\n\n## Workflow Integration\n- After explaining a tool, suggest using MechanicRunTool with the proper parameters\n- Include examples of how to use the tool with common parameter combinations\n", - "input_schema": { - "json": { - "required": [ - "namespace" - ], - "properties": { - "namespace": { - "description": "namespace of tool to describe", - "type": "string", - "examples": [ - [ - "host", - "aws" - ] - ] - }, - "toolPath": { - "type": "string", - "description": "toolPath of tool to describe", - "examples": [ - "cloudwatch logs query-logs" - ] - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "GetDogmaClassification", - "description": "Fetch Dogma classification of a given pipeline\nDogma is a website that lets engineers and managers configure their Release Excellence rules. It allows the customer to: \n- View risks that apply to the pipelines they own\n- Dive into details for each risk\n- Request exemptions from rules that should not have reported a risk\n- Manage pipeline classification and override values\n- opt into new rules at the organization, team, or pipeline scope.\nDogma classification is a key feature in Dogma that automatically categorizes pipelines based on what is being deployed through them. This classification determines which policies and rules apply to each pipeline.\nThe classification structure includes:\n- Inferred classification: Automatically determined by DogmaClassifier\n- Classification overrides: Manual corrections to the inferred values when needed\n- Custom classifications: Flexible key-value pairs for campaign targeting\nThe top-level fields represent the effective classification values that are currently active for the pipeline, taking into account both inferred data and any applied overrides.\nMore classification definition details are defined in the wiki: tiny.amazon.com/1e4sgmu23", - "input_schema": { - "json": { - "additionalProperties": false, - "properties": { - "pipelineName": { - "description": "Pipeline name", - "type": "string" - } - }, - "required": [ - "pipelineName" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "MechanicSetUserInput", - "description": "This tool is for helping you send user input to a running Mechanic execution.\nYou provide the parameters to help identify the User Input request, and the response value, and this sends it to Mechanic and then continues executing the tool.\nAfter running this tool, you will either get another user input request, or the execution will finish and output will be returned.", - "input_schema": { - "json": { - "type": "object", - "properties": { - "response": { - "type": "string", - "examples": [ - "Yes", - "No" - ], - "description": "User input response to the request. Must be \"Yes\" or \"No\"" - }, - "executionId": { - "type": "string", - "description": "The ID for the execution to send user input to, do not make up this value. You MUST use a real execution ID", - "examples": [ - "ex-T739a1f08-cf34-4e28-ada3-cc61d27c57f0" - ] - }, - "requestId": { - "description": "The ID for the user input request, do not make up this value. You MUST use a real user input request ID", - "type": "string", - "examples": [ - "ui-abf4682f-6326-47da-928a-1f17b330e790" - ] - } - }, - "required": [ - "executionId", - "requestId", - "response" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "GetPipelinesRelevantToUser", - "description": "\n Retrieves pipelines relevant to the current user or a specific user.\n \n This includes all pipelines the user has permissions on, including their favorites, and all pipelines grouped by team.\n \n The response includes:\n - Pipelines the user has marked as 'Favorite'\n - Pipelines the user has permissions on, grouped by team\n ", - "input_schema": { - "json": { - "properties": { - "user": { - "type": "string", - "description": "Optional user alias to get pipelines for. If not provided, defaults to the current user" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "CheckFilepathForCAZ", - "description": "Checks if a filepath is protected by Contingent Authorization (CAZ), specifically whether it has customer data risk or security metadata risk. ", - "input_schema": { - "json": { - "required": [ - "filepath" - ], - "additionalProperties": false, - "properties": { - "hostclass": { - "type": "string", - "description": "Optional Apollo hostclass name. If provided, AWS resource parameters are ignored" - }, - "default_directives": { - "description": "Default directives to apply (default: 'MECHANIC_SAFE_PATHS')", - "enum": [ - "MECHANIC_SAFE_PATHS" - ], - "type": "string", - "default": "MECHANIC_SAFE_PATHS" - }, - "filepath": { - "items": { - "type": "string" - }, - "type": "array", - "description": "The file path to check for CAZ protection" - }, - "aws_resource": { - "properties": { - "resource_type": { - "enum": [ - "ACCOUNT", - "EC2_INSTANCE", - "ECS_TASK", - "S3_BUCKET" - ], - "default": "EC2_INSTANCE", - "type": "string", - "description": "Resource type to check against (default: 'EC2_INSTANCE')" - }, - "partition": { - "type": "string", - "default": "aws", - "description": "AWS partition for the resource (default: 'aws')" - }, - "account_id": { - "type": "string", - "description": "AWS account ID for the resource" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "account_id" - ] - }, - "namespace": { - "default": "default", - "description": "CAMS namespace to use (default: 'default')", - "enum": [ - "default" - ], - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "TicketingReadActions", - "description": "A tool for reading data from the ticketing system.\n\nFeatures:\n1. Search for tickets with various filters\n2. Get the details of a single ticket\n3. Get list of resolver groups user belongs to\n4. Get details for a specific resolver group\n5. Get comprehensive instructions for using the ticketing search functionality\n\n\n# Ticketing Tools\n\nThese tools provide access to the ticketing system.\n\n## How to Use\n\nAll actions require a JSON payload with the following structure:\n```json\n{\n \"action\": \"\",\n \"input\": {\n // Action-specific parameters go here\n }\n}\n```\n\n⚠️ Important: All parameters must be inside the `input` object. Parameters at the root level will not be processed correctly.\n\n## Available Actions\n\n### Ticket Search and Retrieval\n\n#### search-tickets\nSearch for tickets based on various criteria.\n\nParameters:\n- query: Raw Solr query string for custom searches. Example: 'extensions.tt.status:(Open OR \"In Progress\") AND extensions.tt.assignedGroup:\"SWIM Front End\"'\n- status: Array of ticket statuses to filter by. By default, only open status tickets are returned.\n- assignedGroup: Array of resolver group names to filter by. Example: ['MY TEAM', 'super-cool-team']\n- fullText: Full text search term to search across ticket content. Example: 'error in production'\n- createDate: Filter by creation date using Solr date syntax. Example: '[2024-01-01T00:00:00Z TO NOW]'\n- lastResolvedDate: Filter by last resolved date using Solr date syntax.\n- lastUpdatedDate: Filter by last updated date using Solr date syntax.\n- currentSeverity: Array of severity levels to filter by. High severity is 1-2, 2.5 for business hours high severity, low severity is 3-5.\n- minimumSeverity: A single number representing the minimum numeric ticket severity\n- sort: Sort parameter for results. Example: 'lastUpdatedDate desc'\n- rows: Maximum number of tickets to return (default: 50, max: 100)\n- start: Starting index for pagination\n- startToken: Token for cursor-based pagination\n- responseFields: Array of fields to include in the response\n\nFor comprehensive search instructions and field descriptions, use the get-search-instructions action.\n\nExample:\n```\n{\n \"action\": \"search-tickets\",\n \"input\": {\n \"status\": [\"Assigned\", \"Researching\", \"Work In Progress\", \"Pending\", \"Resolved\"],\n \"assignedGroup\": [\"IT Support\"],\n \"currentSeverity\": [\"1\", \"2\", \"2.5\"],\n \"minimumSeverity\": 2,\n \"createDate\": \"[2024-01-01T00:00:00Z TO NOW]\",\n \"sort\": \"lastUpdatedDate desc\",\n \"rows\": 50,\n \"responseFields\": [\n \"id\",\n \"title\",\n \"status\",\n \"extensions.tt.assignedGroup\",\n \"extensions.tt.impact\",\n \"createDate\",\n \"lastUpdatedDate\",\n \"description\"\n ]\n }\n}\n```\n\nNote: Some fields are nested under `extensions.tt` and must be referenced using dot notation (e.g., `extensions.tt.assignedGroup`). For a complete list of available fields, use the get-search-instructions action.\n\n#### get-ticket\nRetrieve a single ticket for a specified ID\n\nParameters:\n- ticketId: The ID of the ticket\n\nResponse includes:\n- Ticket details with the most recent announcement and 100 comments\n\nExample:\n```json\n{\n \"action\": \"get-ticket\",\n \"input\": {\n \"ticketId\": \"ABC123\"\n }\n}\n```\n\n### Resolver Group Management\n\n#### get-my-resolver-groups\nGet the resolver groups that the current user is a member of.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-my-resolver-groups\"\n}\n```\n\n#### get-resolver-group-details\nGet operational details about a specific resolver group, including its configuration, members, and settings.\n\nParameters:\n- groupName: The name of the resolver group to get details for\n\nResponse includes:\n- Basic group information and group details\n- Ownership information\n- Business hours and days configuration\n- Management structure, group preferences and settings\n- Notification configurations\n- Labels and templates\n\nExample:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"input\": {\n \"groupName\": \"example-group\"\n }\n}\n```\n\n⚠️ Common Mistake: Do not put parameters at the root level. This will not work:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"groupName\": \"example-group\" // ❌ Wrong: parameter at root level\n}\n```\n\n### Documentation and Instructions\n\n#### get-search-instructions\nGet comprehensive instructions for using the ticketing search functionality, including field descriptions, examples, and best practices.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-search-instructions\"\n}\n```\n\nThe response includes detailed information about:\n- Available search fields and their properties\n- Search syntax and examples\n- Best practices for constructing queries\n", - "input_schema": { - "json": { - "type": "object", - "required": [ - "action" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "input": { - "type": "object", - "additionalProperties": {} - }, - "action": { - "description": "The action to perform.\n\nAvailable actions:\n1. search-tickets: Search for tickets with various filters\n2. get-ticket: Get the details of a single ticket\n3. get-my-resolver-groups: Get list of resolver groups user belongs to\n4. get-resolver-group-details: Get details for a specific resolver group\n5. get-search-instructions: Get comprehensive instructions for using the ticketing search functionality\n\n\n# Ticketing Tools\n\nThese tools provide access to the ticketing system.\n\n## How to Use\n\nAll actions require a JSON payload with the following structure:\n```json\n{\n \"action\": \"\",\n \"input\": {\n // Action-specific parameters go here\n }\n}\n```\n\n⚠️ Important: All parameters must be inside the `input` object. Parameters at the root level will not be processed correctly.\n\n## Available Actions\n\n### Ticket Search and Retrieval\n\n#### search-tickets\nSearch for tickets based on various criteria.\n\nParameters:\n- query: Raw Solr query string for custom searches. Example: 'extensions.tt.status:(Open OR \"In Progress\") AND extensions.tt.assignedGroup:\"SWIM Front End\"'\n- status: Array of ticket statuses to filter by. By default, only open status tickets are returned.\n- assignedGroup: Array of resolver group names to filter by. Example: ['MY TEAM', 'super-cool-team']\n- fullText: Full text search term to search across ticket content. Example: 'error in production'\n- createDate: Filter by creation date using Solr date syntax. Example: '[2024-01-01T00:00:00Z TO NOW]'\n- lastResolvedDate: Filter by last resolved date using Solr date syntax.\n- lastUpdatedDate: Filter by last updated date using Solr date syntax.\n- currentSeverity: Array of severity levels to filter by. High severity is 1-2, 2.5 for business hours high severity, low severity is 3-5.\n- minimumSeverity: A single number representing the minimum numeric ticket severity\n- sort: Sort parameter for results. Example: 'lastUpdatedDate desc'\n- rows: Maximum number of tickets to return (default: 50, max: 100)\n- start: Starting index for pagination\n- startToken: Token for cursor-based pagination\n- responseFields: Array of fields to include in the response\n\nFor comprehensive search instructions and field descriptions, use the get-search-instructions action.\n\nExample:\n```\n{\n \"action\": \"search-tickets\",\n \"input\": {\n \"status\": [\"Assigned\", \"Researching\", \"Work In Progress\", \"Pending\", \"Resolved\"],\n \"assignedGroup\": [\"IT Support\"],\n \"currentSeverity\": [\"1\", \"2\", \"2.5\"],\n \"minimumSeverity\": 2,\n \"createDate\": \"[2024-01-01T00:00:00Z TO NOW]\",\n \"sort\": \"lastUpdatedDate desc\",\n \"rows\": 50,\n \"responseFields\": [\n \"id\",\n \"title\",\n \"status\",\n \"extensions.tt.assignedGroup\",\n \"extensions.tt.impact\",\n \"createDate\",\n \"lastUpdatedDate\",\n \"description\"\n ]\n }\n}\n```\n\nNote: Some fields are nested under `extensions.tt` and must be referenced using dot notation (e.g., `extensions.tt.assignedGroup`). For a complete list of available fields, use the get-search-instructions action.\n\n#### get-ticket\nRetrieve a single ticket for a specified ID\n\nParameters:\n- ticketId: The ID of the ticket\n\nResponse includes:\n- Ticket details with the most recent announcement and 100 comments\n\nExample:\n```json\n{\n \"action\": \"get-ticket\",\n \"input\": {\n \"ticketId\": \"ABC123\"\n }\n}\n```\n\n### Resolver Group Management\n\n#### get-my-resolver-groups\nGet the resolver groups that the current user is a member of.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-my-resolver-groups\"\n}\n```\n\n#### get-resolver-group-details\nGet operational details about a specific resolver group, including its configuration, members, and settings.\n\nParameters:\n- groupName: The name of the resolver group to get details for\n\nResponse includes:\n- Basic group information and group details\n- Ownership information\n- Business hours and days configuration\n- Management structure, group preferences and settings\n- Notification configurations\n- Labels and templates\n\nExample:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"input\": {\n \"groupName\": \"example-group\"\n }\n}\n```\n\n⚠️ Common Mistake: Do not put parameters at the root level. This will not work:\n```json\n{\n \"action\": \"get-resolver-group-details\",\n \"groupName\": \"example-group\" // ❌ Wrong: parameter at root level\n}\n```\n\n### Documentation and Instructions\n\n#### get-search-instructions\nGet comprehensive instructions for using the ticketing search functionality, including field descriptions, examples, and best practices.\n\nParameters: None\n\nExample:\n```\n{\n \"action\": \"get-search-instructions\"\n}\n```\n\nThe response includes detailed information about:\n- Available search fields and their properties\n- Search syntax and examples\n- Best practices for constructing queries\n", - "type": "string", - "enum": [ - "search-tickets", - "get-ticket", - "get-my-resolver-groups", - "get-resolver-group-details", - "get-search-instructions" - ] - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "TaskeiGetRooms", - "description": "Fetch user's Rooms for the Taskei application, also known as SIM folders.\nA room represents a work process for a team and contains all tasks and policies owned by that team.\nThis tool retrieves detailed information about the Taskei Rooms the user has write permissions.\nUse this tool when the user asks to fetch their rooms in a Task Management context (or using the app names Taskei or SIM).\nAll the tasks in Taskei and SIM belong to a room, so if you need to do other actions where the room is needed as input param, you can obtain them from this tool.\nDo not use this tool for other project management tools different than Taskei, and for other context besides project and task management", - "input_schema": { - "json": { - "type": "object", - "required": [], - "properties": { - "nameContains": { - "description": "Search query string that filters results to only include Rooms where the name contains this text. Case-insensitive matching is applied to find partial or complete matches within Room names", - "type": "string" - }, - "maxResults": { - "default": 25, - "description": "The maximum number of results that we want to fetch. The lesser the best, as the query will be faster. (default: 25)", - "type": "number" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "ThirdPartyAnalysisGateway", - "description": "\n Third Party Analysis Gateway (3PAG) performs composition analysis on Third Party software\n artifacts, which detects vulnerabilities/CVE and software licenses used.\n \n ## Disclaimer\n The data returned from 3PAG is informational. For license data, you should reach out to\n OSPO for approval.\n \n ## Important\n - Contact OSPO for confirmation for license approval: tiny.amazon.com/181c7x2f6\n - When using this tool you MUST include a disclaimer and avoid strong language on results\n \n More information for 3PAG can be found in: tiny.amazon.com/ouzvlq96\n ", - "input_schema": { - "json": { - "required": [ - "action", - "identity", - "toolType" - ], - "type": "object", - "properties": { - "identity": { - "type": "string", - "minLength": 1 - }, - "action": { - "enum": [ - "GetPolicyCheckResult" - ], - "description": "The action to perform.\n\nAvailable actions:\n1. GetPolicyCheckResult: fetch the analysis result from 3PAG", - "type": "string" - }, - "toolType": { - "type": "string", - "enum": [ - "NPM", - "BrazilGo", - "BTPT" - ] - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "GetPipelineDetails", - "description": "\n Retrieves a detailed summary of a pipeline's current state, including:\n - Name, ID, description, enabled status\n - Health metrics including failed builds, deployments, tests, and pending approvals\n - Stage count by prod/non-prod and type\n - Target count by type and approval status\n - Promotion count by type and status\n - Latest events for targets in the pipeline\n - Active Administrative disables\n\n Definitions:\n - Badge indicates the automation level of the pipeline (gold: fully automated; silver: mostly automated; bronze: partially automated; no badge: not automated)\n - Promotions needing synchronization indicate a newer artifact is ready to be deployed to the next target in the pipeline\n\n This tool can retrieve information about any existing pipeline, not only those in the list of pipelines relevant to a user.\n ", - "input_schema": { - "json": { - "properties": { - "pipelineName": { - "description": "Name of the pipeline to get an overview summary for", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "pipelineName" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "GetPipelineHealth", - "description": "\n Retrieves the current status and health metrics for a list of pipelines.\n\n This tool can ONLY retrieve pipelines which the current user has permissions on.\n \n The response includes:\n - Whether the pipeline is enabled\n - The fitness badge (gold, silver, bronze)\n - Health metrics like failed builds, deployments, and tests\n - Pending approvals and workflow steps\n - Basic pipeline information\n\n Health metrics definitions:\n - failedBuilds: total failing source code builds\n - failedDeployments: total failing deployments\n - failedProdDeployments: total failing deployments to Production fleets\n - failedTests: total failing automated tests\n - failedProdTests: total failing automated tests on Production fleets\n - pendingManualApprovals: total manual approvals waiting for input\n - pendingProdManualApprovals: total manual approvals gating Production deployments waiting for input\n - pendingManualWFSteps: total workflow steps requiring manual approval waiting for input\n - pendingProdManualWFSteps: total workflow steps requiring manual approval and gating Production deployments waiting for input\n - disabledPromotions: number of disabled promotions\n - pipelineDisabled: whether pipeline is admin disabled 0 = false, 1 = true\n\n If any of these health metrics is non-zero or if the pipeline is disabled then the pipeline is Blocked, meaning it requires operator intervention to continue promoting changes automatically.\n \n Use the optional 'onlyBlocked' parameter to filter results to only include pipelines that are blocked (either disabled or have health metric issues). Prefer this option over manually identifying blocked pipelines, as it is more efficient.\n ", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "required": [ - "pipelineNames" - ], - "properties": { - "pipelineNames": { - "items": { - "type": "string" - }, - "description": "List of pipeline names to query", - "type": "array" - }, - "onlyBlocked": { - "type": "boolean", - "description": "Optional boolean which if set limits results to pipelines which are blocked" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "WorkspaceGitDetails", - "description": "\n Returns the git repositories, statuses, and git diffs for packages in a given workspace.\n This tool DOES NOT create or push any git commits.\n\n An expected workflow for this tool would be:\n 1. Code changes are made to one or more package(s) in a workspace.\n 2. The agent is prompted to create git commits for these packages.\n 3. This tool will respond with the top-level repository structure of the the packages in a workspace,\n and the git changes for each repository.\n\n Response structure in JSON would be:\n {\n \"message\": \"Local git repository details retrieved successfully\",\n \"gitRepositories\": [\n {\n \"repositoryName\": \"repo1\",\n \"repositoryPath\": \"/workspace/repo1\",\n \"gitStatus\": \"On branch main. Your branch is up to date with 'origin/main'.\n Changes not staged for commit:\n (use \"git add ...\" to update what will be committed)\n (use \"git restore ...\" to discard changes in working directory)\n modified: src/index.ts\n modified: package.json\",\n \"gitDiff\": \"diff --git a/src/index.ts b/src/index.ts\n index 1234567..89abcdef 100644\n --- a/src/index.ts\n +++ b/src/index.ts\n @@ -1,3 +1,4 @@\n export function hello() {\n - return \"world\";\n + // Added a comment\n + return \"hello world\";\n }\"\n }\n ]\n }\n ", - "input_schema": { - "json": { - "required": [ - "workingDirectory" - ], - "properties": { - "workingDirectory": { - "type": "string", - "description": "Working directory of the workspace that has git repositories" - } - }, - "type": "object" - } - } - } - } - ], - "native___": [ - { - "ToolSpecification": { - "name": "fs_write", - "description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `str_replace` command:\n * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n * The `new_str` parameter should contain the edited lines that should replace the `old_str`.", - "input_schema": { - "json": { - "type": "object", - "required": [ - "command", - "path" - ], - "properties": { - "new_str": { - "description": "Required parameter of `str_replace` command containing the new string. Required parameter of `insert` command containing the string to insert. Required parameter of `append` command containing the content to append to the file.", - "type": "string" - }, - "summary": { - "type": "string", - "description": "A brief explanation of what the file change does or why it's being made." - }, - "command": { - "type": "string", - "enum": [ - "create", - "str_replace", - "insert", - "append" - ], - "description": "The commands to run. Allowed options are: `create`, `str_replace`, `insert`, `append`." - }, - "old_str": { - "description": "Required parameter of `str_replace` command containing the string in `path` to replace.", - "type": "string" - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "insert_line": { - "description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.", - "type": "integer" - }, - "file_text": { - "description": "Required parameter of `create` command, with the content of the file to be created.", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "dummy", - "description": "This is a dummy tool. If you are seeing this that means the tool associated with this tool call is not in the list of available tools. This could be because a wrong tool name was supplied or the list of tools has changed since the conversation has started. Do not show this when user asks you to list tools.", - "input_schema": { - "json": { - "required": [], - "properties": {}, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "report_issue", - "description": "Opens the browser to a pre-filled gh (GitHub) issue template to report chat issues, bugs, or feature requests. Pre-filled information includes the conversation transcript, chat context, and chat request IDs from the service.", - "input_schema": { - "json": { - "properties": { - "steps_to_reproduce": { - "description": "Optional: Previous user chat requests or steps that were taken that may have resulted in the issue or error response.", - "type": "string" - }, - "expected_behavior": { - "description": "Optional: The expected chat behavior or action that did not happen.", - "type": "string" - }, - "title": { - "type": "string", - "description": "The title of the GitHub issue." - }, - "actual_behavior": { - "description": "Optional: The actual chat behavior that happened and demonstrates the issue or lack of a feature.", - "type": "string" - } - }, - "required": [ - "title" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "introspect", - "description": "ALWAYS use this tool when users ask ANY question about Q CLI itself, its capabilities, features, commands, or functionality. This includes questions like 'Can you...', 'Do you have...', 'How do I...', 'What can you do...', or any question about Q's abilities. When mentioning commands in your response, always prefix them with '/' (e.g., '/save', '/load', '/context'). CRITICAL: Only provide information explicitly documented in Q CLI documentation. If details about any tool, feature, or command are not documented, clearly state the information is not available rather than generating assumptions.", - "input_schema": { - "json": { - "type": "object", - "properties": { - "query": { - "description": "The user's question about Q CLI usage, features, or capabilities", - "type": "string" - } - }, - "required": [] - } - } - } - }, - { - "ToolSpecification": { - "name": "use_aws", - "description": "Make an AWS CLI api call with the specified service, operation, and parameters. All arguments MUST conform to the AWS CLI specification. Should the output of the invocation indicate a malformed command, invoke help to obtain the the correct command.", - "input_schema": { - "json": { - "required": [ - "region", - "service_name", - "operation_name", - "label" - ], - "type": "object", - "properties": { - "parameters": { - "description": "The parameters for the operation. The parameter keys MUST conform to the AWS CLI specification. You should prefer to use JSON Syntax over shorthand syntax wherever possible. For parameters that are booleans, prioritize using flags with no value. Denote these flags with flag names as key and an empty string as their value. You should also prefer kebab case.", - "type": "object" - }, - "region": { - "type": "string", - "description": "Region name for calling the operation on AWS." - }, - "operation_name": { - "type": "string", - "description": "The name of the operation to perform." - }, - "service_name": { - "description": "The name of the AWS service. If you want to query s3, you should use s3api if possible.", - "type": "string" - }, - "profile_name": { - "type": "string", - "description": "Optional: AWS profile name to use from ~/.aws/credentials. Defaults to default profile if not specified." - }, - "label": { - "type": "string", - "description": "Human readable description of the api that is being called." - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "execute_bash", - "description": "Execute the specified bash command.", - "input_schema": { - "json": { - "required": [ - "command" - ], - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "summary": { - "type": "string", - "description": "A brief explanation of what the command does" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "fs_read", - "description": "Tool for reading files, directories and images. Always provide an 'operations' array.\n\nFor single operation: provide array with one element.\nFor batch operations: provide array with multiple elements.\n\nAvailable modes:\n- Line: Read lines from a file\n- Directory: List directory contents\n- Search: Search for patterns in files\n- Image: Read and process images\n\nExamples:\n1. Single: {\"operations\": [{\"mode\": \"Line\", \"path\": \"/file.txt\"}]}\n2. Batch: {\"operations\": [{\"mode\": \"Line\", \"path\": \"/file1.txt\"}, {\"mode\": \"Search\", \"path\": \"/file2.txt\", \"pattern\": \"test\"}]}", - "input_schema": { - "json": { - "properties": { - "operations": { - "items": { - "type": "object", - "required": [ - "mode" - ], - "properties": { - "mode": { - "type": "string", - "enum": [ - "Line", - "Directory", - "Search", - "Image" - ], - "description": "The operation mode to run in: `Line`, `Directory`, `Search`. `Line` and `Search` are only for text files, and `Directory` is only for directories. `Image` is for image files, in this mode `image_paths` is required." - }, - "image_paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of paths to the images. This is currently supported by the Image mode." - }, - "start_line": { - "default": 1, - "type": "integer", - "description": "Starting line number (optional, for Line mode). A negative index represents a line number starting from the end of the file." - }, - "context_lines": { - "default": 2, - "type": "integer", - "description": "Number of context lines around search results (optional, for Search mode)" - }, - "end_line": { - "description": "Ending line number (optional, for Line mode). A negative index represents a line number starting from the end of the file.", - "default": -1, - "type": "integer" - }, - "path": { - "type": "string", - "description": "Path to the file or directory. The path should be absolute, or otherwise start with ~ for the user's home (required for Line, Directory, Search modes)." - }, - "pattern": { - "description": "Pattern to search for (required, for Search mode). Case insensitive. The pattern matching is performed per line.", - "type": "string" - }, - "depth": { - "default": 0, - "type": "integer", - "description": "Depth of a recursive directory listing (optional, for Directory mode)" - } - } - }, - "minItems": 1, - "type": "array", - "description": "Array of operations to execute. Provide one element for single operation, multiple for batch." - }, - "summary": { - "description": "Optional description of the purpose of this batch operation (mainly useful for multiple operations)", - "type": "string" - } - }, - "type": "object", - "required": [ - "operations" - ] - } - } - } - } - ], - "amazon-internal-mcp-server": [ - { - "ToolSpecification": { - "name": "remove_tag_work_contribution", - "description": "Remove a tag from a work contribution in AtoZ.\n\nThis tool allows you to remove a tag (such as a leadership principle tag) from an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- tagKey: The key of the tag to remove (e.g., 'CUSTOMER_OBSESSION', 'EARN_TRUST')\n- tagType: The type of tag (e.g., 'LEADERSHIP_PRINCIPLE')\n- ownerLogin or ownerPersonId: The owner of the work contribution", - "input_schema": { - "json": { - "required": [ - "workContributionId", - "tagKey", - "tagType" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "workContributionId": { - "type": "string", - "description": "ID of the work contribution" - }, - "ownerLogin": { - "description": "Login/alias of the work contribution owner", - "type": "string" - }, - "tagKey": { - "type": "string", - "description": "Key of the tag to remove (e.g., 'CUSTOMER_OBSESSION', 'EARN_TRUST')" - }, - "ownerPersonId": { - "type": "string", - "description": "Person ID of the work contribution owner" - }, - "tagType": { - "description": "Type of tag to remove", - "type": "string", - "enum": [ - "LEADERSHIP_PRINCIPLE", - "ROLE_GUIDELINE" - ] - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_create_question", - "description": "Create a new question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to post new questions to Sage through the MCP interface.\nQuestions require at least one tag or packageTag to categorize them properly.\nThe question content supports Markdown formatting for rich text, code blocks, and links.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Asking technical questions about Amazon internal tools and services\n- Seeking help with troubleshooting issues\n- Requesting best practices or guidance\n\nExample usage:\n{ \"title\": \"How to resolve Brazil dependency conflicts?\", \"contents\": \"I'm getting the following error when building my package:\\n\\n```\\nCannot resolve dependency X\\n```\\n\\nHow can I fix this?\", \"tags\": [\"brazil\", \"build-system\"] }", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "tags": { - "description": "Tags to categorize the question (at least one tag or packageTag is required)", - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string", - "description": "Title of the question" - }, - "contents": { - "type": "string", - "description": "Content of the question in Markdown format" - }, - "packageTags": { - "items": { - "type": "string" - }, - "description": "Package tags to categorize the question (at least one tag or packageTag is required)", - "type": "array" - } - }, - "required": [ - "title", - "contents" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "update_work_contribution", - "description": "Update an existing work contribution in AtoZ.\n\nThis tool allows you to modify the details of an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution to update\n- title: The updated title of the work contribution\n- editStatus: The updated status of the contribution\n- ownerLogin or ownerPersonId: The owner of the work contribution\n\nOptional parameters include:\n- summary: An updated summary of the contribution\n- startDate: An updated start date (YYYY-MM-DD)\n- endDate: An updated end date (YYYY-MM-DD)", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "workContributionId", - "title", - "editStatus" - ], - "properties": { - "summary": { - "description": "Updated summary of the work contribution", - "type": "string" - }, - "title": { - "type": "string", - "description": "Updated title of the work contribution" - }, - "startDate": { - "type": "string", - "description": "Updated start date in YYYY-MM-DD format" - }, - "endDate": { - "description": "Updated end date in YYYY-MM-DD format", - "type": "string" - }, - "ownerPersonId": { - "description": "Person ID of the employee who owns the contribution", - "type": "string" - }, - "workContributionId": { - "type": "string", - "description": "ID of the work contribution to update" - }, - "editStatus": { - "type": "string", - "enum": [ - "IN_PROGRESS", - "COMPLETE", - "DRAFT", - "READY_FOR_REVIEW", - "APPROVED", - "PENDING_CHANGES", - "DRAFT_MANAGER" - ], - "description": "Updated edit status of the work contribution" - }, - "ownerLogin": { - "description": "Login/alias of the employee who owns the contribution", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "write_internal_website", - "description": "Write to Amazon internal websites.\n\nSupported websites and their purposes:\n\nDocument Storage & Sharing:\n- w.amazon.com: Internal MediaWiki\n\nNote: By default, content is converted from Markdown to the target format.\nTo skip conversion (if your content is already in the target format), set skipConversion=true.", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "properties": { - "operation": { - "description": "Operation to perform", - "enum": [ - "update", - "append", - "prepend", - "create" - ], - "type": "string" - }, - "versionSummary": { - "type": "string", - "description": "Summary message for the version history" - }, - "url": { - "type": "string", - "format": "uri", - "description": "Website URL to write to" - }, - "content": { - "description": "Content to write in Markdown format", - "type": "string" - }, - "format": { - "default": "XWiki", - "description": "Format to write in", - "type": "string", - "enum": [ - "Markdown", - "XWiki", - "XHTML", - "HTML", - "Plain", - "MediaWiki" - ] - }, - "skipConversion": { - "default": false, - "description": "Skip content format conversion", - "type": "boolean" - }, - "title": { - "type": "string", - "description": "Title for the page (required for create operations)" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "url", - "content", - "operation" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_sa_activity", - "description": "This tool is logging/creating, reading, updating or deleting SA Activities on AWS SFDC AKA AWSentral. You must have either account id or opportunity id to create", - "input_schema": { - "json": { - "type": "object", - "properties": { - "opportunity_id": { - "type": "string", - "description": "the SFDC id of the opportunity, use the sfdc_opportunity_lookup tool to retrieve before submitting." - }, - "operation": { - "description": "The operation to perform: create, read, update, or delete (always read before deleting, confirm with the user)", - "type": "string", - "enum": [ - "create", - "read", - "update", - "delete" - ] - }, - "activity_subject": { - "type": "string", - "description": "The title of the activity, keep it short" - }, - "activity_description": { - "description": "A description of the activity, around 1 paragraph, rewrite the user's input to be more descriptive and professional, unless the user says not to.", - "type": "string" - }, - "activity_id": { - "type": "string", - "description": "The ID of the SA Activity (required for read, update, and delete operations)" - }, - "activity_status": { - "default": "Completed", - "enum": [ - "Not Started", - "In Progress", - "Completed", - "Waiting on someone else", - "Deferred", - "Unresponsive", - "Disqualified", - "Cancelled", - "Completed with Global Support", - "Sales handoff to BDM completed", - "Completed with sales handoff to BDM", - "Completed with funding program handoff to ATP Mgr" - ], - "type": "string", - "description": "The activity Status. Default status is Completed." - }, - "activity_type": { - "description": "The type of activity, one of Account Planning, Meeting, Architecture Review, Demo, Partner, or Workshop", - "type": "string" - }, - "date": { - "description": "the date in MM-DD-YYYY, if left empty will be today's date, if you are unsure about today's date, leave this blank", - "type": "string" - }, - "activity_assigned_to": { - "type": "string", - "description": "The name of the user to which the activity should be assigned." - }, - "account_id": { - "description": "the SFDC id of the account, use the sfdc_account_lookup tool to retrieve before submitting.", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "operation" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_get_knowledge_structure", - "description": "Map the hierarchical organization of your knowledge repository by generating a complete directory structure. This tool provides a navigable overview of how folders and documents are organized, with configurable depth settings to control detail level. Essential for understanding knowledge base architecture and relationships between document collections.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "depth": { - "description": "How many levels deep to traverse", - "type": "number" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "overleaf_write_file", - "description": "Write a file to an Overleaf project with automatic commit and push.\n\nThis tool writes content to the specified file in an Overleaf project.\nBefore writing, it ensures the project is cloned locally and synchronized.\nAfter writing, it automatically commits the changes with a descriptive message\nand pushes them to the remote repository.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"main.tex\",\n \"content\": \"\\\\documentclass{article}\\n\\\\begin{document}\\nHello World\\n\\\\end{document}\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file within the project" - }, - "project_id": { - "type": "string", - "description": "Project ID to write to" - }, - "content": { - "type": "string", - "description": "File content to write" - } - }, - "additionalProperties": false, - "required": [ - "project_id", - "file_path", - "content" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "list_leadership_principles", - "description": "List all Amazon Leadership Principles that can be used as tags on work contributions.\n\nThis tool retrieves a list of all available Amazon Leadership Principles that can be\napplied as tags to work contributions in AtoZ.\n\nLimitations:\nYou can only access your own work contributions\n\nThe response includes:\n- Leadership principle keys (used for adding tags)\n- Display names of the leadership principles\n\nUse this information when adding leadership principle tags to work contributions\nwith the add_tag_work_contribution tool.", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": {}, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_get_artifact_comments", - "description": "Retrieves comments for a Pippin design artifact, organized by thread and status (open vs resolved). Use this tool to review feedback, track discussion threads, and understand the current state of comments on design artifacts. Comments are grouped by parent-child relationships (threads) and categorized by their resolution status. This tool automatically handles pagination to retrieve all comments for the specified artifact.", - "input_schema": { - "json": { - "properties": { - "designId": { - "type": "string", - "description": "Design artifact ID within the project (string identifier, e.g., 'design-1'). Obtain this from pippin_list_artifacts or the Pippin web interface." - }, - "projectId": { - "type": "string", - "description": "Pippin project ID (UUID format, e.g., 'dee44368f3f7'). Obtain this from pippin_list_projects or the Pippin web interface." - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "projectId", - "designId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "search_products", - "description": "Search for products on Amazon.com (US marketplace only) and extract structured product information including titles, prices, ratings, and images", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "query" - ], - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query string for the products you want to find" - }, - "filters": { - "additionalProperties": false, - "description": "Optional filters to narrow down search results", - "type": "object", - "properties": { - "index": { - "type": "string", - "description": "Department to search in. Available options include: 'all' (default), 'books', 'electronics', 'computers', 'clothing', 'home', 'beauty', 'toys', 'grocery', 'sports', 'automotive', 'pets', 'baby', 'health', 'industrial', 'movies', 'music', 'video-games', 'tools', 'office-products', and more" - }, - "sortBy": { - "type": "string", - "description": "Sort order for results. Available options include:\n- 'relevanceblender' (default): Sort by relevance\n- 'price-asc-rank': Price low to high\n- 'price-desc-rank': Price high to low\n- 'review-rank': Average customer review\n- 'date-desc-rank': Newest arrivals\n- 'exact-aware-popularity-rank': Popularity\n- 'get-it-fast-rank': Fastest delivery\n- 'low-prices-rank': Lowest price with ranking factors\n- 'most-purchased-rank': Most purchased\n- 'top-brands-rank': Top brands" - }, - "minPrice": { - "type": "number", - "description": "Minimum price filter in dollars (e.g., 25 for $25)" - }, - "maxPrice": { - "type": "number", - "description": "Maximum price filter in dollars (e.g., 100 for $100)" - } - } - }, - "maxResults": { - "type": "number", - "description": "Maximum number of products to return (default: 10, max recommended: 50)" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_list_records", - "description": "Get the records (also called config values) for a given feature (also called configuration) name from Amazon Config Store.\nIf the specified format of the returned records is PARSED, it will be returned in a human-readable format. If the format is STRINGIFIED, it will be returned in the original ion format.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "featureName" - ], - "properties": { - "stage": { - "description": "Stage to query", - "default": "PROD", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "type": "string" - }, - "format": { - "description": "Specifies the format of the records returned, either PARSED (human-readable format) or STRINGIFIED (original ion format)", - "default": "PARSED", - "type": "string", - "enum": [ - "PARSED", - "STRINGIFIED" - ] - }, - "featureName": { - "type": "string", - "description": "Feature name to retrieve records for" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "prompt_farm_search_prompts", - "description": "A specialized search tool designed to efficiently discover and retrieve tested prompt templates from Amazon internal PromptFarm, enabling developers to leverage community-vetted prompts for reducing LLM hallucinations and optimizing AI outputs. The tool surfaces prompts categorized by use case, download metrics, and community ratings to streamline prompt engineering workflows.", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "properties": { - "searchQuery": { - "type": "string", - "description": "The search query for PromptFarm" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "searchQuery" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_get", - "description": "Gets data from a G2S2 table with specified parameters", - "input_schema": { - "json": { - "properties": { - "parentStageVersion": { - "description": "The parent stage version for the stage version", - "type": "string" - }, - "kwargs": { - "additionalProperties": {}, - "description": "Additional key-value parameters for the query", - "type": "object" - }, - "tableName": { - "type": "string", - "description": "The table name to query" - } - }, - "required": [ - "tableName", - "parentStageVersion" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "edit_quip", - "description": "Edit Quip documents\n\nThis tool allows you to make targeted edits to specific sections of a Quip document\nusing section IDs obtained from the read_quip tool when using HTML format.\n\nWorkflow:\n1. Use read_quip with HTML format to get the document with section IDs\n2. Identify the section ID you want to modify (e.g., 'temp:C:SAf3351f25e51434479864cf71ce')\n3. Use edit_quip with the section ID and appropriate location parameter\n\nLocations:\n0: APPEND - Add to end of document (default)\n1: PREPEND - Add to beginning of document\n2: AFTER_SECTION - Insert after section_id\n3: BEFORE_SECTION - Insert before section_id\n4: REPLACE_SECTION - Replace section_id content\n5: DELETE_SECTION - Delete section_id\n6: AFTER_DOCUMENT_RANGE - Insert after document_range\n7: BEFORE_DOCUMENT_RANGE - Insert before document_range\n8: REPLACE_DOCUMENT_RANGE - Replace document_range content\n9: DELETE_DOCUMENT_RANGE - Delete document_range\n\nExamples:\n1. Append to document:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\"\n}\n```\n\n2. Prepend to document:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 1\n}\n```\n\n3. Insert after section:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 2,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n4. Replace section content:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"### New heading\",\n \"format\": \"markdown\",\n \"location\": 4,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n5. Delete section:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"\",\n \"format\": \"markdown\",\n \"location\": 5,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\"\n}\n```\n\n6. Edit with concise response:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"New content\",\n \"format\": \"markdown\",\n \"location\": 4,\n \"sectionId\": \"temp:C:SAf3351f25e51434479864cf71ce\",\n \"returnFullDocument\": false\n}\n```", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "format": { - "enum": [ - "html", - "markdown" - ], - "default": "markdown", - "description": "The format of the content", - "type": "string" - }, - "sectionId": { - "description": "Section ID for section operations", - "type": "string" - }, - "returnFullDocument": { - "description": "Whether to return the full document content after editing (default: false)", - "type": "boolean" - }, - "content": { - "type": "string", - "description": "The new content to write to the document" - }, - "documentRange": { - "description": "Document range for range operations", - "type": "string" - }, - "location": { - "type": "number", - "description": "Location for content insertion", - "minimum": 0, - "maximum": 9 - }, - "documentId": { - "description": "The Quip document URL or ID to edit", - "type": "string" - } - }, - "required": [ - "documentId" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_create_feature", - "description": "Creates a new feature (also called configuration) in Amazon Config Store.\nThis tool allows creating a feature with the specified name, schema, owners, and other attributes.\nThe name of the feature should be unique, and the contextual parameters used should be existing in the specified stage.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "cti": { - "type": "object", - "required": [ - "category", - "type", - "item" - ], - "additionalProperties": false, - "properties": { - "category": { - "type": "string", - "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." - }, - "type": { - "description": "CTI type. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "item": { - "type": "string", - "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." - } - }, - "description": "CTI information. Do NOT assume this info, you MUST ask the user about it." - }, - "teamName": { - "type": "string", - "description": "Team name responsible for the feature. Do NOT assume this info, you MUST ask the user about it." - }, - "crFeatureEnabled": { - "type": "boolean", - "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", - "default": true - }, - "owners": { - "items": { - "required": [ - "type", - "name" - ], - "type": "object", - "properties": { - "type": { - "enum": [ - "BINDLE", - "TEAM", - "POSIX_GROUP", - "AAA" - ], - "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "name": { - "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - } - }, - "additionalProperties": false - }, - "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", - "minItems": 1, - "type": "array" - }, - "schema": { - "type": "object", - "required": [ - "name", - "attributes", - "contextualParameters", - "types", - "metadata" - ], - "properties": { - "attributes": { - "minItems": 1, - "items": { - "properties": { - "name": { - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "type": "string", - "description": "Name of the attribute in snake_case" - }, - "description": { - "description": "Description for the attribute", - "type": "string" - }, - "type": { - "type": "string", - "description": "Type of the attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" - } - }, - "additionalProperties": false, - "required": [ - "name", - "type" - ], - "type": "object" - }, - "type": "array", - "description": "Attributes of the feature. Attributes are the fields of your config table, the value of these attributes can vary depending on contextual parameter values." - }, - "contextualParameters": { - "items": { - "type": "string" - }, - "minItems": 0, - "type": "array", - "description": "Contextual Parameters of the feature. Contextual parameters are the keys of the configuration, you can have zero, one, or many. Order is important as it is used to determine the priority in query resolution, meaning least specific CP must come first and the most specific comes last (e.g. country, state, city). The contextual parameter must exist in the specified stage before using it in a feature. If it does not exist, the contextual parameter needs to be created in Sandbox first then promoted to other stages. You can use acs_get_contextual_parameter tool to confirm that the name of the contextual parameter is existing in the specified stage, or you can use acs_search_resources tool with resourceType as CONTEXTUAL_PARAMETER to recommend contextual parameters to use as guidance to the customers if it was not provided or contextual parameter does not exist in the given stage." - }, - "metadata": { - "properties": { - "clients": { - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "packageName": { - "type": "string", - "description": "Custom package name for the client. The Java client will be generated in the specified package. When provided, use search_internal_code tool to make sure the package exists. Before executing the command, you MUST ask the user to MAKE SURE the bindle has these required permissions: 1. Can read Gitfarm Repository 2. Can write Gitfarm Repository 3. Can write to protected branches Gitfarm Repository" - }, - "bindleId": { - "type": "string", - "description": "Bindle ID for the client. It needs to be in the format of amzn1.bindle.resource.* and NOT the bindle name. bindleId is required if packageName is not specified. ACS will auto-generate a package under this bindle. Do NOT assume this info, you MUST ask the user about it.Before executing the command, you MUST ask the user to MAKE SURE the bindle has these required permissions: 1. Can read Gitfarm Repository 2. Can write Gitfarm Repository 3. Can write to protected branches Gitfarm Repository" - } - } - }, - "minItems": 1, - "description": "The generated Java client to consume the configuration. You MUST ASK the user whether they want to use an existing package by providing a packageName, or generate a new package by providing a bindleId.", - "maxItems": 1, - "type": "array" - } - }, - "type": "object", - "required": [ - "clients" - ], - "additionalProperties": false, - "description": "Metadata of the clients of the feature" - }, - "types": { - "items": { - "properties": { - "name": { - "description": "Name of the custom type", - "type": "string" - }, - "type": { - "anyOf": [ - { - "required": [ - "kind", - "values" - ], - "additionalProperties": false, - "properties": { - "kind": { - "type": "string", - "description": "Enum type used as a type for an attribute.", - "const": "Enum" - }, - "values": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" - }, - "minItems": 1, - "description": "Enum values. Values must be in snake_case." - } - }, - "type": "object" - }, - { - "type": "object", - "properties": { - "attributes": { - "description": "Struct attributes", - "type": "array", - "minItems": 1, - "items": { - "required": [ - "name", - "type" - ], - "additionalProperties": false, - "properties": { - "name": { - "description": "Name of the struct attribute. It should be in snake_case.", - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "type": "string" - }, - "type": { - "type": "string", - "description": "Type of the struct attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" - }, - "description": { - "type": "string", - "description": "Description of the struct attribute." - } - }, - "type": "object" - } - }, - "kind": { - "type": "string", - "description": "Struct type used as a type for an attribute.", - "const": "Struct" - } - }, - "additionalProperties": false, - "required": [ - "kind", - "attributes" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { - "const": "List", - "type": "string", - "description": "List type used as a type for an attribute." - }, - "element": { - "type": "string", - "description": "Type of the list element: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" - } - }, - "required": [ - "kind", - "element" - ] - } - ], - "description": "Type definition" - }, - "description": { - "description": "Description for the custom type", - "type": "string" - } - }, - "type": "object", - "required": [ - "name", - "type" - ], - "additionalProperties": false - }, - "minItems": 0, - "type": "array", - "description": "Custom types for the feature that can be used as schema attribute type, struct attribute type, or list element type" - }, - "validations": { - "minItems": 0, - "description": "Validations for the feature attributes", - "type": "array", - "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "kind": { - "type": "string", - "description": "For string attributes, which validates attributes against a predefined regex.", - "const": "Pattern" - }, - "regex": { - "description": "Regex pattern", - "type": "string" - }, - "targetAttributes": { - "items": { - "type": "string" - }, - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", - "type": "array" - }, - "description": { - "type": "string", - "description": "Contains the ACS customer explanation for a given validation." - } - }, - "type": "object", - "required": [ - "kind", - "targetAttributes", - "regex" - ] - }, - { - "properties": { - "min": { - "type": "string", - "description": "Range minimum value (inclusive)" - }, - "kind": { - "const": "Range", - "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values).", - "type": "string" - }, - "max": { - "type": "string", - "description": "Range maximum value (inclusive)" - }, - "targetAttributes": { - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", - "items": { - "type": "string" - }, - "type": "array" - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - } - }, - "type": "object", - "required": [ - "kind", - "targetAttributes" - ], - "additionalProperties": false - }, - { - "additionalProperties": false, - "type": "object", - "properties": { - "kind": { - "type": "string", - "const": "NonNull", - "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes." - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - }, - "targetAttributes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - } - }, - "required": [ - "kind", - "targetAttributes" - ] - }, - { - "type": "object", - "required": [ - "kind", - "targetAttributes", - "arn" - ], - "additionalProperties": false, - "properties": { - "description": { - "description": "Description of what the Lambda validates.", - "type": "string" - }, - "arn": { - "type": "string", - "description": "Lambda ARN" - }, - "targetAttributes": { - "items": { - "type": "string" - }, - "type": "array", - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - }, - "kind": { - "description": "Lambda validation allows you to use your own custom logic to validate record values.", - "type": "string", - "const": "Lambda" - } - } - } - ] - } - }, - "name": { - "type": "string", - "minLength": 1, - "description": "Name of the feature to create. It should be in PascalCase.", - "pattern": "^(?:[A-Z][a-z0-9]*)+$" - } - }, - "additionalProperties": false, - "description": "Schema definition for the feature. The schema defines the attributes, contextual parameters, types, validations, and clients of a feature." - }, - "approvers": { - "items": { - "required": [ - "type", - "name" - ], - "additionalProperties": false, - "properties": { - "name": { - "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "type": { - "type": "string", - "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", - "enum": [ - "USER", - "LDAP", - "POSIXG", - "TEAM", - "SNS" - ] - }, - "requiredCount": { - "type": "number", - "description": "Required count of approvers", - "exclusiveMinimum": 0 - } - }, - "type": "object" - }, - "type": "array", - "minItems": 1, - "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it." - }, - "stage": { - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "description": "Stage to query", - "type": "string" - }, - "description": { - "description": "Description of the feature", - "type": "string", - "minLength": 1 - }, - "teamWikiLink": { - "description": "Team wiki link. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "configSnapshotEnabled": { - "default": false, - "description": "Whether config snapshot is enabled. Config snapshot allows the user to use deployable cache and dynamic refresher for example. Know more about deployable cache from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/OnBoarding/Cache/#HDeployablecache and dynamic refresher from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/DynamicRefresher.", - "type": "boolean" - } - }, - "required": [ - "description", - "schema", - "owners", - "cti", - "teamName", - "teamWikiLink", - "stage" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "rtla_fetch_logs", - "description": "Fetch logs from RTLA (Real-Time Log Analysis) API. This tool allows you to retrieve log entries based on organization, affected type, time range, and filter expressions. The maximum time range supported is 12 hours from the start time. Useful for troubleshooting system issues, analyzing error patterns, and monitoring application health.", - "input_schema": { - "json": { - "properties": { - "affectedType": { - "type": "string", - "description": "Type of affected logs to retrieve (e.g., \"FATAL\", \"NONFATAL\")" - }, - "identifyAdditionalOrgs": { - "default": true, - "description": "Whether to identify additional organizations", - "type": "boolean" - }, - "org": { - "type": "string", - "description": "Organization identifier (e.g., \"CWCBCCECMPROD\")" - }, - "searchField": { - "type": "string", - "default": "org", - "description": "Search field type (default: \"org\")" - }, - "timeZone": { - "description": "Time zone (e.g., \"US/Pacific\")", - "type": "string", - "default": "GMT&customTimeZoneOffset" - }, - "startTime": { - "type": "string", - "description": "Start time in ISO 8601 format with timezone (e.g., 2025-05-11T11:31:16-04:00)" - }, - "anchor": { - "type": "string", - "description": "Anchor position (e.g., \"Ending\", \"Beginning\")", - "default": "Ending" - }, - "filterExpression": { - "type": "string", - "description": "Filter expression for log filtering (e.g., \"(pageType eq 'uscbcc-ecm-paybill')\")" - }, - "endTime": { - "description": "End time in ISO 8601 format with timezone (e.g., 2025-05-11T12:31:16-04:00)", - "type": "string" - } - }, - "required": [ - "org", - "affectedType", - "startTime", - "endTime" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "orca_get_latest_error_details", - "description": "Get detailed error information from an Orca workflow run URL.\n\nThis tool extracts error details including stack traces from Orca Studio execution pages.\n\nExample:\n```json\n{ \"url\": \"https://us-east-1.studio.orca.amazon.dev/#/clients/MyClient/execution/12345\", \"workflowName\": \"TestWorkflow\", \"objectId\": \"TestObjectId, \"runId\": \"TestRunId, \"clientId\": \"MyOrcaClient\"}\n```\nExample with custom region:\n```json\n{ \"url\": \"https://us-east-1.studio.orca.amazon.dev/#/clients/MyClient/execution/12345\", \"workflowName\": \"TestWorkflow\", \"objectId\": \"TestObjectId, \"runId\": \"TestRunId, \"clientId\": \"MyOrcaClient, \"region\": \"us-west-2\"}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "workflowName": { - "type": "string", - "description": "The type of workflow to extract error details from" - }, - "url": { - "type": "string", - "description": "The Orca Studio URL of the execution to analyze" - }, - "objectId": { - "description": "The objectId of the particular workflow to extract error details from", - "type": "string" - }, - "runId": { - "description": "The runId of the execution to extract error details from", - "type": "string" - }, - "clientId": { - "type": "string", - "description": "The clientId of the execution to extract error details from" - }, - "region": { - "type": "string", - "description": "AWS region (defaults to us-east-1). Common regions include us-west-2, eu-west-1, etc." - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "required": [ - "url", - "workflowName", - "objectId", - "runId", - "clientId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_agent_script_search", - "description": "Perform comprehensive keyword searches across the entire agentic script library, examining script names, content bodies, and metadata fields simultaneously. This tool returns contextually-rich results with relevant text snippets surrounding each match, highlighting where and how search terms appear within scripts. Results include file locations, match types (filename, content, or description matches), and properly handles duplicate scripts with consolidated results. Perfect for discovering scripts based on functionality, implementation details, or descriptive elements rather than exact names.", - "input_schema": { - "json": { - "required": [ - "query" - ], - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query to find matching scripts" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "delete_work_contribution", - "description": "Delete a work contribution from AtoZ.\n\nLimitations:\nYou can only access your own work contributions\n\nThis tool allows you to remove an existing work contribution.\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution to delete\n- ownerLogin or ownerPersonId: The owner of the work contribution", - "input_schema": { - "json": { - "type": "object", - "properties": { - "ownerLogin": { - "description": "Login/alias of the employee who owns the contribution", - "type": "string" - }, - "ownerPersonId": { - "description": "Person ID of the employee who owns the contribution", - "type": "string" - }, - "workContributionId": { - "description": "ID of the work contribution to delete", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "workContributionId" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "mosaic_list_risks", - "description": "\nThe AWS Risk Library is an extensible reference library that contains potential risk events\nthat may impact AWS and/or its customers and the risk scenarios that could trigger them. The\nlibrary contains high-level risk categories (Level 1), (e.g., availability, security, third\nparty, etc.); sub-categories of risk events (Level 2) for each level 1 risk (e.g., network\nfailure, service failure, infrastructure failure); and plausible risk causes (Level 3) that\ncan result in a risk event (e.g., inadequate capacity planning, lack of governance oversight,\npower outages, etc.). The level 2 risk events are the central element of the risk library.\n\nThis tool returns the risks that are part of the AWS Risk Library.", - "input_schema": { - "json": { - "type": "object", - "properties": {} - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_update_contextual_parameter", - "description": "Updates a contextual parameter (also called config key, or CP) in Amazon Config Store.\nThis tool allows updating a contextual parameter by only giving it the parameters required to be updated, other parameters that are not provided will remain as is.\nIf any of the required parameters are not provided, do NOT assume them, just leave them empty.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "properties": { - "description": { - "type": "string", - "description": "Description of the contextual parameter" - }, - "stage": { - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "description": "Stage to query", - "type": "string" - }, - "crFeatureEnabled": { - "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", - "type": "boolean" - }, - "cti": { - "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", - "type": "object", - "required": [ - "category", - "type", - "item" - ], - "properties": { - "type": { - "description": "CTI type. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "item": { - "type": "string", - "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." - }, - "category": { - "type": "string", - "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." - } - }, - "additionalProperties": false - }, - "approvers": { - "type": "array", - "minItems": 1, - "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "name" - ], - "properties": { - "requiredCount": { - "type": "number", - "description": "Required count of approvers", - "exclusiveMinimum": 0 - }, - "type": { - "enum": [ - "USER", - "LDAP", - "POSIXG", - "TEAM", - "SNS" - ], - "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "name": { - "type": "string", - "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." - } - } - } - }, - "owners": { - "items": { - "type": "object", - "properties": { - "type": { - "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", - "type": "string", - "enum": [ - "BINDLE", - "TEAM", - "POSIX_GROUP", - "AAA" - ] - }, - "name": { - "type": "string", - "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it." - } - }, - "required": [ - "type", - "name" - ], - "additionalProperties": false - }, - "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", - "type": "array" - }, - "validations": { - "items": { - "anyOf": [ - { - "properties": { - "targetAttributes": { - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "const": "Pattern", - "description": "For string attributes, which validates attributes against a predefined regex.", - "type": "string" - }, - "description": { - "type": "string", - "description": "Contains the ACS customer explanation for a given validation." - }, - "regex": { - "description": "Regex pattern", - "type": "string" - } - }, - "required": [ - "kind", - "targetAttributes", - "regex" - ], - "type": "object", - "additionalProperties": false - }, - { - "properties": { - "kind": { - "const": "Range", - "type": "string", - "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values)." - }, - "targetAttributes": { - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", - "items": { - "type": "string" - }, - "type": "array" - }, - "min": { - "description": "Range minimum value (inclusive)", - "type": "string" - }, - "max": { - "type": "string", - "description": "Range maximum value (inclusive)" - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "kind", - "targetAttributes" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "kind": { - "const": "NonNull", - "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes.", - "type": "string" - }, - "targetAttributes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - }, - "description": { - "type": "string", - "description": "Contains the ACS customer explanation for a given validation." - } - }, - "type": "object", - "required": [ - "kind", - "targetAttributes" - ] - }, - { - "required": [ - "kind", - "targetAttributes", - "arn" - ], - "properties": { - "description": { - "type": "string", - "description": "Description of what the Lambda validates." - }, - "arn": { - "type": "string", - "description": "Lambda ARN" - }, - "kind": { - "const": "Lambda", - "description": "Lambda validation allows you to use your own custom logic to validate record values.", - "type": "string" - }, - "targetAttributes": { - "items": { - "type": "string" - }, - "type": "array", - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - } - }, - "additionalProperties": false, - "type": "object" - } - ] - }, - "description": "Validations for the contextual parameter records", - "type": "array" - }, - "name": { - "minLength": 1, - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "description": "Name of the contextual parameter to update. It should be in snake_case." - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "name", - "stage" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_update_artifact", - "description": "Updates an existing artifact within a Pippin project", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "projectId": { - "type": "string", - "description": "Project ID" - }, - "name": { - "description": "Updated artifact name", - "type": "string" - }, - "content": { - "type": "string", - "description": "Updated artifact content (provide this OR contentPath)" - }, - "designId": { - "type": "string", - "description": "Artifact ID" - }, - "contentPath": { - "type": "string", - "description": "Path to a file containing the artifact content (provide this OR content)" - }, - "description": { - "type": "string", - "description": "Updated artifact description" - } - }, - "required": [ - "projectId", - "designId" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_freeze_stage_version", - "description": "Freezes a specified G2S2 stage version", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "stageVersion": { - "type": "string", - "description": "The stage version to freeze" - } - }, - "additionalProperties": false, - "required": [ - "stageVersion" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "edit_quip_link_sharing", - "description": "Edit link sharing settings for an existing Quip document\n\nThis tool allows you to enable, disable, or change the link sharing mode\nfor an existing Quip document without modifying its content.\n\nParameters:\n- documentId: The Quip document URL or ID\n- mode: Link sharing mode ('view', 'edit', or 'none')\n\nExamples:\n1. Enable view-only link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"view\"\n}\n```\n\n2. Enable edit link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"edit\"\n}\n```\n\n3. Disable link sharing:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"mode\": \"none\"\n}\n```", - "input_schema": { - "json": { - "properties": { - "documentId": { - "description": "The Quip document URL or ID", - "type": "string" - }, - "mode": { - "enum": [ - "view", - "edit", - "none" - ], - "description": "Link sharing mode: 'view' for view-only, 'edit' for edit access, 'none' to disable sharing", - "type": "string" - } - }, - "type": "object", - "additionalProperties": false, - "required": [ - "documentId", - "mode" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "search_quip_commented_by_current_user", - "description": "Get all documents where the current user has left comments\n\nThis tool retrieves all Quip documents where the current user has posted comments.\nYou can optionally filter the results by date range to get documents with comments within a specific time period.\n\nThe tool checks all user-accessible threads for comments made by the current user,\nwith optional date range filtering for more targeted results.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents with user comments:\n```json\n{\n}\n```\n\n2. Get documents with comments within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```", - "input_schema": { - "json": { - "properties": { - "startDate": { - "type": "string", - "description": "Start date for filtering comments (YYYY-MM-DD format)" - }, - "endDate": { - "description": "End date for filtering comments (YYYY-MM-DD format)", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "read_quip_from_urls", - "description": "Extract and retrieve the full HTML content of Quip documents using their URLs\n\nThis tool reads multiple Quip documents simultaneously using their URLs.\nIt extracts document IDs from the provided links and retrieves the content\nfor all documents in a single operation.\n\nThe tool accepts an array of Quip document URLs and returns structured\ninformation including document ID, title, content, and the original link\nfor each document.\n\nExamples:\n1. Read multiple documents:\n```json\n{\n \"links\": [\n \"https://quip-amazon.com/abc/Document1\",\n \"https://quip-amazon.com/def/Document2\"\n ]\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "links": { - "type": "array", - "description": "Array of Quip document urls to read", - "items": { - "type": "string" - } - } - }, - "required": [ - "links" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "mermaid", - "description": "Create and decode Mermaid diagrams using Amazon's internal Mermaid editor.\nMermaid allows creating flowcharts, sequence diagrams, and more using text descriptions.\n\nSupported operations:\n- encode: Convert Mermaid text to an encoded URL\n- decode: Extract Mermaid text from an encoded URL", - "input_schema": { - "json": { - "required": [ - "operation" - ], - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "operation": { - "type": "string", - "enum": [ - "encode", - "decode" - ], - "description": "The operation to perform" - }, - "url": { - "description": "Mermaid URL for decode operation", - "format": "uri", - "type": "string" - }, - "content": { - "type": "string", - "description": "Mermaid content for encode operation" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "prompt_farm_prompt_content", - "description": "A tool designed to fetch prompt content directly by specifying the repository name. This tool leverages repository identifiers to locate, extract, and deliver prompt templates or prompt from PromptFarm prompt repositories. It simplifies accessing prompt definitions without manual browsing, enabling users to quickly integrate or customize prompts by referencing the exact repository source.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "repositoryName" - ], - "type": "object", - "properties": { - "repositoryName": { - "description": "The name of the PromptFarm repository to retrieve the prompt from", - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_create_artifact", - "description": "Creates a new artifact within an existing Pippin project", - "input_schema": { - "json": { - "additionalProperties": false, - "required": [ - "projectId", - "name", - "content" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "projectId": { - "type": "string", - "description": "Project ID" - }, - "name": { - "type": "string", - "description": "Artifact name" - }, - "description": { - "type": "string", - "description": "Artifact description" - }, - "content": { - "type": "string", - "description": "Artifact content" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "create_quip", - "description": "Create a new Quip document or spreadsheet\n\nThis tool creates a new document or spreadsheet in Quip with the specified content.\n\nRequired parameters:\n- content: The HTML or Markdown content of the new document (max 1MB)\n\nOptional parameters:\n- format: Format of the content ('html' or 'markdown', default is 'html')\n- title: Title of the new document (max 10KB)\n- member_ids: Comma-separated list of folder IDs or user IDs for access\n- type: Type of document to create ('document' or 'spreadsheet', default is 'document')\n- mode: Link sharing mode ('view', 'edit', or 'none' to disable sharing)\n\nNotes:\n- If title is not specified, it will be inferred from the first content\n- If member_ids is not specified, the document will be created in the user's Private folder\n- For spreadsheets, content must be surrounded by HTML
tags\n- If mode is not specified, document uses default sharing settings\n\nExamples:\n1. Create a simple document:\n```json\n{\n \"content\": \"# My New Document\\n\\nThis is a test document.\",\n \"format\": \"markdown\"\n}\n```\n\n2. Create a document with a title in a specific folder:\n```json\n{\n \"content\": \"# Introduction\\n\\nThis is the start of my document.\",\n \"format\": \"markdown\",\n \"title\": \"Project Proposal\",\n \"member_ids\": \"ABCDEF123456\"\n}\n```\n\n3. Create a document with internal link sharing:\n```json\n{\n \"content\": \"# Shared Document\\n\\nThis document has link sharing enabled.\",\n \"format\": \"markdown\",\n \"mode\": \"view\"\n}\n```\n\n4. Create a document with sharing disabled:\n```json\n{\n \"content\": \"# Private Document\\n\\nThis document has no link sharing.\",\n \"format\": \"markdown\",\n \"mode\": \"none\"\n}\n```", - "input_schema": { - "json": { - "required": [ - "content" - ], - "additionalProperties": false, - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": [ - "view", - "edit", - "none" - ], - "description": "Link sharing mode: 'view' for view-only, 'edit' for edit access, 'none' to disable sharing" - }, - "format": { - "default": "markdown", - "enum": [ - "html", - "markdown" - ], - "type": "string", - "description": "The format of the content" - }, - "member_ids": { - "description": "Comma-separated list of folder IDs or user IDs for access", - "type": "string" - }, - "title": { - "type": "string", - "description": "Title of the new document" - }, - "type": { - "default": "document", - "type": "string", - "description": "Type of document to create", - "enum": [ - "document", - "spreadsheet" - ] - }, - "content": { - "description": "The HTML or Markdown content of the new document", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_create_label", - "description": "Creates a new G2S2 label with the specified parent label", - "input_schema": { - "json": { - "type": "object", - "properties": { - "labelName": { - "type": "string", - "description": "The label name to create" - }, - "stageVersion": { - "description": "The stage version for the new label", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "labelName", - "stageVersion" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "search_quip_mentioned_current_user", - "description": "Get all documents where the current user was mentioned\n\nThis tool retrieves all Quip documents where the current user was mentioned by name or email.\nYou can optionally filter the results by date range to get documents with mentions within a specific time period.\n\nThe tool searches for documents containing the user's name, email, or username,\nwith optional date range filtering based on document update time.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents with user mentions:\n```json\n{\n}\n```\n\n2. Get documents with mentions within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```", - "input_schema": { - "json": { - "properties": { - "startDate": { - "description": "Start date for filtering mentions (YYYY-MM-DD format)", - "type": "string" - }, - "endDate": { - "type": "string", - "description": "End date for filtering mentions (YYYY-MM-DD format)" - } - }, - "additionalProperties": false, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "create_work_contribution", - "description": "Create a new work contribution in AtoZ.\n\nThis tool allows you to create a new work contribution with specified details.\nAfter successful creation, you will need to navigate to the AtoZ portal\nat https://atoz.amazon.work/profile/your-growth to upload any artifacts.\nYou must use list_leadership_principles tool to get the uri and definition of all principles\nYou can use add_tag_work_contribution to tag leadership principles\nYou must provide ownerLogin amazon alias as ownerLogin\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- title: The title of the work contribution\n- editStatus: The status of the contribution (IN_PROGRESS or DRAFT)\n- ownerLogin or ownerPersonId: The owner of the work contribution\n\nOptional parameters include:\n- summary: A detailed summary of the contribution\n- startDate: The start date of the contribution (YYYY-MM-DD)\n- endDate: The end date of the contribution (YYYY-MM-DD)", - "input_schema": { - "json": { - "type": "object", - "properties": { - "editStatus": { - "enum": [ - "IN_PROGRESS", - "COMPLETE", - "DRAFT", - "READY_FOR_REVIEW", - "APPROVED", - "PENDING_CHANGES", - "DRAFT_MANAGER" - ], - "description": "Edit status of the work contribution", - "type": "string" - }, - "summary": { - "type": "string", - "description": "Summary of the work contribution" - }, - "startDate": { - "description": "Start date in YYYY-MM-DD format", - "type": "string" - }, - "ownerLogin": { - "type": "string", - "description": "Login/alias of the employee who owns the contribution" - }, - "ownerPersonId": { - "description": "Person ID of the employee who owns the contribution", - "type": "string" - }, - "endDate": { - "type": "string", - "description": "End date in YYYY-MM-DD format" - }, - "title": { - "type": "string", - "description": "Title of the work contribution" - } - }, - "required": [ - "title", - "editStatus" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "mosaic_list_controls", - "description": "\nThe AWS Control Library is the authoritative source of controls that AWS \nuses to manage operational risk. The library represents AWS's own control \nframework supporting high-level policies and standards, and represents \nmanagement's directives and requirements that prescribe how the organization \nmanages its risk and control processes. The library also provides a \nmapping of AWS controls to AWS' policies/standards, and external \nrequirements such as regulatory and compliance frameworks. AWS implements \nthese controls through various mechanisms, including architectural system \ndesign (e.g., region isolation), system enforced guardrails (e.g., static \ncode analysis), or and centrally enforced organizational processes (e.g., \napplication security reviews). Control owners, who are leaders at Level 8 \nor above within the business, validate each control. The Security Assurance \n& Compliance (SA&C) team independently challenges these validations. To \ndemonstrate assurance, each control includes a narrative that articulates \nhow the control is implemented and supporting evidence of control execution \nthat provides tangible proof of its implementation.\n\nThis tool returns the controls that are part of the AWS Control Library.", - "input_schema": { - "json": { - "type": "object", - "properties": {} - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_agent_script_list", - "description": "Discover and browse the complete collection of available agentic scripts with customizable filtering options. This tool provides a comprehensive inventory of script resources including their names, file paths, and detailed descriptions. Results are organized to help quickly identify relevant scripts for specific tasks, with automatic handling of duplicate scripts across different directories. Ideal for exploring the script library or finding scripts based on filename patterns. Returns script names, paths, and descriptions to help users discover relevant scripts for their tasks. Categorize the scripts based on description.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "filter": { - "type": "string", - "description": "Filter to apply to script list" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "isengard", - "description": "Access Amazon's internal Isengard service for AWS account management.\\nThis tool is designed for builders including developers, support teams, and field teams (SAs and TAMs) \\nto easily access their Isengard-managed AWS accounts, typically non-production accounts used for building and testing.\\n\\n## When to use:\\n- When you need to list AWS accounts you own or have access to through POSIX group membership\\n- When you need detailed information about a specific Isengard-managed AWS account\\n- When you need temporary AWS credentials for testing or development work\\n\\n## Limitations:\\n- Only works with Isengard-managed AWS accounts\\n- Requires appropriate permissions to the target AWS accounts, managed by Midway\\n- Credential access requires valid IAM role names already created for the AWS Account.\\n- This tool does not yet support alternative partitions such as GovCloud or China.\\n- listOwnedAWSAccounts supports pagination with maxResultsPerPage (1-100, default: 30) and maxPages (default: 1) parameters.\\n\\n## Supported operations:\\n- listOwnedAWSAccounts: List all ACTIVE accounts you own with optional primary owner filtering and pagination\\n- getAWSAccount: Get detailed information about a specific AWS account\\n- getAssumeRoleCredentials: Get temporary AWS credentials for a specific account and IAM role\\n\\n## Examples\\nList owned AWS accounts: isengard listOwnedAWSAccounts\\nGet AWS account details: isengard getAWSAccount --accountId 123456789012\\nGet AWS credentials for IAM role: isengard getAssumeRoleCredentials --accountId 123456789012 --roleName MyRole", - "input_schema": { - "json": { - "required": [ - "operation" - ], - "properties": { - "maxResultsPerPage": { - "type": "number", - "description": "Number of results per page (1-100, default: 30)", - "minimum": 1, - "maximum": 100 - }, - "maxPages": { - "description": "Maximum number of pages to retrieve (default: 1)", - "type": "number", - "minimum": 1 - }, - "roleName": { - "type": "string", - "description": "IAM Role Name for getAssumeRoleCredentials operation." - }, - "operation": { - "type": "string", - "description": "The operation to perform", - "enum": [ - "listOwnedAWSAccounts", - "getAWSAccount", - "getAssumeRoleCredentials" - ] - }, - "ownerType": { - "enum": [ - "primary" - ], - "type": "string", - "description": "Filter for listOwnedAWSAccounts operation narrows down results to only those that the user is primary owner of. The only valid value is 'primary' otherwise leave it ommited to return all AWS Accounts the user is considered an owner of." - }, - "accountId": { - "type": "string", - "description": "AWS Account ID for getAWSAccount or getAssumeRoleCredentials operation" - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_create_issue", - "description": "Create a new JIRA issue", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "required": [ - "projectKey", - "issueType", - "summary" - ], - "type": "object", - "properties": { - "projectKey": { - "type": "string", - "minLength": 1, - "description": "The key of the project where the issue will be created" - }, - "summary": { - "type": "string", - "minLength": 1, - "description": "The summary of the issue" - }, - "issueType": { - "description": "The type of the issue (e.g., Bug, Task, Story)", - "minLength": 1, - "type": "string" - }, - "additionalFields": { - "type": "object", - "description": "Additional fields to include in the issue", - "additionalProperties": {} - }, - "description": { - "type": "string", - "description": "The description of the issue" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "add_comment_quip", - "description": "Add a comment to a Quip document\n\nThis tool allows you to add a comment to a specified Quip document or thread.\nComments appear in the thread's conversation panel and are visible to all document collaborators.\nThe comment will be attributed to the owner of the API token.\n\nParameters:\n- threadIdOrUrl: (Required) The Quip document/thread ID or URL to add a comment to\n- content: (Required) The comment message text to add\n- section_id: ID of a document section to comment on\n\nNotes:\n- Plain text only, no formatting or HTML is supported\n- Comments cannot be edited or deleted through the Quip API: These operations are not supported\n- Maximum length is 1MB (though practical messages are typically much shorter)\n- Only one of section_id or annotation_id can be provided\n- annotation_id is retrieved as a response of the get_recent_messages_quip tool\n- Manually creating a link to a quip section gives a response like : https://quip-amazon.com/bpVtAZ8LB0b4/Quip-Commenting-Capabilities-Test#fND9CAsTr5B\n- Where bpVtAZ8LB0b4 is the threadId, and fND9CAsTr5B is the section_id.\n- As such, the annotation_id is retreived by the get_recent_messages_quip tool\n\nExamples:\n1. Add a simple comment:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"Great document! I have a few suggestions.\"\n}\n```\n\n2. Add a comment to a specific section:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"This section needs more detail.\",\n \"section_id\": \"SAf3351f25e51434479864cf71ce\"\n}\n```\n\n3. Reply to an existing comment:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"content\": \"I agree with your comment.\",\n \"annotation_id\": \"fND9CAeEYiG\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "required": [ - "threadIdOrUrl", - "content" - ], - "properties": { - "annotation_id": { - "type": "string", - "description": "ID of a document comment to reply to" - }, - "threadIdOrUrl": { - "type": "string", - "description": "The thread ID or Quip URL to add a comment to" - }, - "section_id": { - "type": "string", - "description": "ID of a document section to comment on" - }, - "content": { - "type": "string", - "description": "The comment message content to add to the thread" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "read_permissions", - "description": "Read team information from Amazon's internal permissions system.\n\nThis tool allows you to retrieve detailed information about team memberships,\noverrides, and rules from permissions.amazon.com team pages.\n\nYou MUST specify which tables OR rule sections to include in the response.\nAt least one of these parameters must be provided with at least one option selected.\nThe tool will only retrieve the specified tables and rule sections.\n\nAvailable tables:\n- additional_overrides: Additional Members overrides table\n- deny_overrides: Denied Members overrides table\n- team_membership: Team Membership table (large table, slow to retrieve)\n- team_audit: Team Audit log table (very large table, very slow to retrieve)\n\nAvailable rule sections:\n- rule_membership: Membership rules section\n- rule_additional_overrides: Additional Members overrides rules section\n\nFor large tables (especially team_membership and team_audit), you can use the\nmaxPages parameter to limit the number of pages processed and prevent timeouts.\nYou can also use tableFilters to narrow down the results.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "maxPages": { - "type": "integer", - "description": "Maximum number of pages to process per table. Use for very large tables to prevent timeouts.", - "exclusiveMinimum": 0 - }, - "ruleSections": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "rule_membership", - "rule_additional_overrides" - ] - }, - "description": "List of specific rule sections to include. At least one table or rule section must be specified." - }, - "tables": { - "description": "List of specific tables to include. At least one table or rule section must be specified.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "additional_overrides", - "deny_overrides", - "team_membership", - "team_audit" - ] - } - }, - "tableFilters": { - "propertyNames": { - "enum": [ - "additional_overrides", - "deny_overrides", - "team_membership", - "team_audit" - ] - }, - "description": "Filters to apply to specific tables. Each filter contains a query string or array of query strings and optional threshold.", - "additionalProperties": { - "required": [ - "query" - ], - "properties": { - "query": { - "anyOf": [ - { - "type": "string", - "description": "Text to search for in the table rows" - }, - { - "items": { - "type": "string" - }, - "description": "Multiple terms to search for in the table rows (combined with OR logic)", - "type": "array" - } - ], - "description": "Text or array of texts to search for in the table rows" - }, - "threshold": { - "default": 0.3, - "description": "Fuzzy match threshold (0-1). Lower = stricter match. Default is 0.3", - "minimum": 0, - "maximum": 1, - "type": "number" - } - }, - "type": "object", - "additionalProperties": false - }, - "type": "object" - }, - "teamUrl": { - "type": "string", - "description": "URL of the permissions team page to read", - "format": "uri" - } - }, - "required": [ - "teamUrl" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_add_comment", - "description": "Add a comment to a JIRA issue", - "input_schema": { - "json": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "The body of the comment", - "minLength": 1 - }, - "issueIdOrKey": { - "minLength": 1, - "type": "string", - "description": "The ID or key of the issue" - } - }, - "required": [ - "issueIdOrKey", - "body" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "get_thread_folders_quip", - "description": "Get folders containing a Quip thread (V2 API)\n\nThis tool retrieves information about folders that contain a specific thread.\nIt uses the V2 API which provides more comprehensive folder information.\n\nYou can provide one of the following:\n- The thread ID\n- The thread's secret path\n- The full Quip URL (e.g., https://quip-amazon.com/abc/Doc)\n\nThe secret path can be found in the URL of a thread.\nFor example, in 'https://quip.com/3fs7B2leat8/TrackingDocument', the secret path is '3fs7B2leat8'.\n\nExamples:\n```json\n{\n \"threadId\": \"3fs7B2leat8\"\n}\n```\n\n```json\n{\n \"threadId\": \"https://quip-amazon.com/abc/Doc\"\n}\n```", - "input_schema": { - "json": { - "properties": { - "threadId": { - "description": "The thread ID, secret path, or full Quip URL", - "type": "string" - } - }, - "type": "object", - "required": [ - "threadId" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_list_artifacts", - "description": "Lists all artifacts for a specific Pippin project", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "nextToken": { - "type": "string", - "description": "Pagination token" - }, - "projectId": { - "description": "Project ID", - "type": "string" - }, - "maxResults": { - "type": "number", - "description": "Maximum number of results to return" - } - }, - "required": [ - "projectId" - ], - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_accept_answer", - "description": "Accept an answer to a question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to mark an answer as accepted for a question.\nOnly the question owner or users with appropriate permissions can accept answers.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Marking the most helpful answer to your question\n- Indicating which solution resolved your issue\n- Helping others find the correct answer quickly\n\nExample usage:\n{ \"answerId\": 7654321 }", - "input_schema": { - "json": { - "properties": { - "answerId": { - "type": "number", - "description": "ID of the answer to accept" - } - }, - "type": "object", - "required": [ - "answerId" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_agent_script_get", - "description": "Access the complete content and metadata of specific agentic scripts using either file paths or script names. This tool retrieves the full script implementation along with structured metadata, enabling deep inspection of script functionality, parameter requirements, and operational logic before execution. The flexible lookup system supports both direct path access and name-based discovery across multiple script directories, with proper handling of script extensions. Essential for understanding script capabilities before integration into workflows.", - "input_schema": { - "json": { - "type": "object", - "properties": { - "name": { - "description": "Name of the script (with or without .script.md extension)", - "type": "string" - }, - "path": { - "type": "string", - "description": "Path to the script file" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_create_stage_version", - "description": "Creates a new stage version in G2S2 with the specified parent stage version", - "input_schema": { - "json": { - "type": "object", - "properties": { - "stageVersion": { - "type": "string", - "description": "The stage version to create" - }, - "parentStageVersion": { - "type": "string", - "description": "The parent stage version for the stage version" - } - }, - "additionalProperties": false, - "required": [ - "stageVersion", - "parentStageVersion" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_list_stage_version", - "description": "Lists contents of a specified G2S2 stage version", - "input_schema": { - "json": { - "required": [ - "stageVersion" - ], - "type": "object", - "properties": { - "stageVersion": { - "type": "string", - "description": "The stage version to list" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_add_comment", - "description": "Add a comment to a post on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to comment on questions or answers on Sage through the MCP interface.\nComments are useful for requesting clarification, providing additional context, or suggesting improvements.\nComments use plain text format (no Markdown support).\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Asking for clarification on a question or answer\n- Providing additional context or information\n- Suggesting improvements or alternatives\n\nExample usage:\n{ \"postId\": 1234567, \"contents\": \"Could you also explain how this works with custom dependencies?\" }", - "input_schema": { - "json": { - "required": [ - "postId", - "contents" - ], - "properties": { - "contents": { - "type": "string", - "description": "Content of the comment in plain text" - }, - "postId": { - "type": "number", - "description": "ID of the post (question or answer) to comment on" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_sync_project_to_remote", - "description": "Synchronizes local files to a Pippin project as artifacts", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "projectId", - "inputDirectory" - ], - "properties": { - "inputDirectory": { - "type": "string", - "description": "Local directory containing files to upload" - }, - "createMissing": { - "type": "boolean", - "default": true, - "description": "Create artifacts if they don't exist" - }, - "projectId": { - "type": "string", - "description": "Project ID" - }, - "nameFormat": { - "description": "How to name artifacts", - "enum": [ - "use_filename", - "use_id" - ], - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "marshal_get_report", - "description": "Retrieve Marshal Report.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", - "input_schema": { - "json": { - "required": [ - "reportId" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "reportId": { - "pattern": "^\\d+$", - "description": "The ID of the Marshal Report (numeric ID only, not the full URL)", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "marshal_search_insights", - "description": "Search Marshal Insights.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "properties": { - "relativeDateRangeMs": { - "type": "string", - "description": "Relative date range for search (e.g. last 1 hour, last 1 week) in milliseconds", - "pattern": "^\\d+$" - }, - "absoluteDateRangeStartDate": { - "pattern": "^\\d+$", - "type": "string", - "description": "Absolute date range for search start date in milliseconds since 1/1/1970" - }, - "managerAlias": { - "type": "string", - "description": "Manager Alias - returns all employees below" - }, - "absoluteDateRangeEndDate": { - "pattern": "^\\d+$", - "description": "Absolute date range for search end date in milliseconds since 1/1/1970", - "type": "string" - }, - "category": { - "type": "string", - "description": "Insight Category" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "slack_send_message", - "description": "Send a message to a specified Slack channel with optional thread support", - "input_schema": { - "json": { - "properties": { - "channelId": { - "type": "string", - "minLength": 1 - }, - "message": { - "type": "string", - "minLength": 1 - }, - "thread_ts": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "channelId", - "message" - ], - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_user_lookup", - "description": "This tool is for looking up users on the AWS Salesforce AKA AWSentral", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "user_name": { - "type": "string", - "description": "the name of the user" - }, - "user_id": { - "type": "string", - "description": "the id of the user" - }, - "email": { - "type": "string", - "description": "the email address of the user" - }, - "alias": { - "type": "string", - "description": "the alias of the user" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_import_stage_version", - "description": "Imports ion file into a specified G2S2 stage version", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "stageVersion": { - "type": "string", - "description": "The stage version to import into" - }, - "filepath": { - "type": "string", - "description": "The ion file path to import" - } - }, - "required": [ - "stageVersion", - "filepath" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_transition_issue", - "description": "Transition a JIRA issue to a new status", - "input_schema": { - "json": { - "required": [ - "issueIdOrKey", - "transitionId" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "comment": { - "type": "string", - "description": "Optional comment to add during transition" - }, - "transitionId": { - "minLength": 1, - "type": "string", - "description": "The ID of the transition" - }, - "fields": { - "additionalProperties": {}, - "description": "Optional fields to update during transition", - "type": "object" - }, - "issueIdOrKey": { - "description": "The ID or key of the issue", - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "list_work_contributions", - "description": "List work contributions from AtoZ PortfolioWidgetService.\n\nThis tool retrieves work contributions for a specific employee from AtoZ.\nYou must provide either ownerLogin or ownerPersonId to identify the employee.\n\nLimitations:\nYou can only access your own work contributions\n\nThe response includes work contributions with their details such as:\n- Title and summary\n- Edit status\n- Start and end dates\n- Associated artifacts\n- Stakeholders\n\nFor paginated results, you can use the nextToken parameter to retrieve subsequent pages.", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "ownerPersonId": { - "type": "string", - "description": "Person ID of the employee to get work contributions for" - }, - "ownerLogin": { - "description": "Login/alias of the employee to get work contributions for", - "type": "string" - }, - "nextToken": { - "description": "Token for pagination", - "type": "string" - }, - "maxResults": { - "type": "number", - "description": "Maximum number of results to return (default: 100)" - }, - "sortDirection": { - "description": "Sort direction (ASC or DESC, default: DESC)", - "type": "string", - "enum": [ - "ASC", - "DESC" - ] - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_list_knowledge", - "description": "Generate organized inventories of documents stored in the knowledge repository. This tool can list all documents or focus on specific folders, with options for recursive directory traversal and depth control. Returns document paths and titles, enabling systematic navigation of the knowledge structure.", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "properties": { - "folder": { - "type": "string", - "description": "The folder path to list documents from" - }, - "depth": { - "default": 5, - "type": "number", - "description": "How many levels deep to traverse" - }, - "recursive": { - "description": "Whether to include documents in subfolders", - "type": "boolean" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_get_issue", - "description": "Get a JIRA issue by ID or key", - "input_schema": { - "json": { - "required": [ - "issueIdOrKey" - ], - "properties": { - "issueIdOrKey": { - "minLength": 1, - "description": "The ID or key of the issue", - "type": "string" - }, - "expand": { - "description": "The additional information to include in the response", - "type": "string" - }, - "fields": { - "type": "array", - "description": "The list of fields to return", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "search_ags_confluence_website", - "description": "Search for Amazon Games Confluence pages\n\nThis tool allows you to search for content in the Amazon Games Confluence instance.\nYou can search for pages, blog posts, and other content across all spaces or within a specific space.\n\nParameters:\n- query: The search query string\n- page: (Optional) Page number for pagination (default: 1)\n- pageSize: (Optional) Number of results per page (default: 10, max: 50)\n- space: (Optional) Limit search to a specific Confluence space\n\nExamples:\n1. Basic search:\n { \"query\": \"game server architecture\" }\n\n2. Search with pagination:\n { \"query\": \"matchmaking\", \"page\": 2, \"pageSize\": 20 }\n\n3. Search in a specific space:\n { \"query\": \"deployment guide\", \"space\": \"GAMETECH\" }\n\nTips:\n- Use specific technical terms for more precise results\n- For recent content, sort by modification date\n- When looking for documentation, include terms like 'guide', 'documentation', or 'how-to'\n- For architecture documents, include terms like 'architecture', 'design', or 'diagram'\n- If you know the space key, use it to narrow down results", - "input_schema": { - "json": { - "properties": { - "query": { - "description": "Search query string", - "type": "string" - }, - "page": { - "description": "Page number for pagination (default: 1)", - "type": "number" - }, - "pageSize": { - "type": "number", - "description": "Number of results per page (default: 10, max: 50)" - }, - "space": { - "description": "Limit search to a specific Confluence space", - "type": "string" - } - }, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "required": [ - "query" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "overleaf_upload_file", - "description": "Upload a local file from the Overleaf workspace to the remote repository with automatic commit and push.\n\nThis tool reads an existing file from the local Overleaf workspace and uploads it to the remote repository.\nThe file must already exist in the local workspace directory (./overleaf/{project_id}/file_path).\nBefore uploading, it syncs the project to get latest changes and detects merge conflicts.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"figures/diagram.png\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "properties": { - "file_path": { - "description": "Path to the file within the project workspace", - "type": "string" - }, - "project_id": { - "description": "Project ID to upload to", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "project_id", - "file_path" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "search_quip", - "description": "Search for Quip threads\n\nThis tool allows you to search for Quip threads using keywords.\nResults are sorted by relevance and include document titles, links, and metadata.\n\nExamples:\n1. Basic search:\n```json\n{\n \"query\": \"expense report\"\n}\n```\n\n2. Search with limit:\n```json\n{\n \"query\": \"expense report\",\n \"count\": 5\n}\n```\n\n3. Search only in titles:\n```json\n{\n \"query\": \"expense report\",\n \"onlyMatchTitles\": true\n}\n```", - "input_schema": { - "json": { - "required": [ - "query" - ], - "properties": { - "count": { - "description": "Maximum number of results to return (default: 10, max: 50)", - "type": "number" - }, - "query": { - "type": "string", - "description": "Search query to find matching Quip threads" - }, - "onlyMatchTitles": { - "description": "If true, only search in document titles (default: false)", - "type": "boolean" - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_move_label", - "description": "Moves a stage version to a specified testing label", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "labelName", - "stageVersion" - ], - "properties": { - "stageVersion": { - "description": "The stage version from a parent label", - "type": "string" - }, - "labelName": { - "type": "string", - "description": "The label name of a testing label" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_change_records", - "description": "Modify records (also called config values) for a given feature (also called configuration) in Amazon Config Store.\nAllows adding, deleting, or modifying records with proper change tracking.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "properties": { - "changeSummary": { - "type": "string", - "description": "Summary of the changes being made" - }, - "records": { - "description": "Record changes to apply", - "properties": { - "recordChanges": { - "type": "array", - "items": { - "additionalProperties": false, - "properties": { - "attribute": { - "type": "string", - "description": "Name of the attribute being modified, if you are not sure what are the valid attributes for this feature, you can use acs_get_feature tool" - }, - "weblabRules": { - "description": "Optional weblab rules", - "items": { - "properties": { - "operands": { - "items": { - "description": "Weblab treatment identifier", - "type": "string" - }, - "type": "array" - }, - "use": { - "description": "Value for the attribute as stringified json", - "type": "string" - }, - "operator": { - "description": "Weblab rule operator", - "type": "string", - "enum": [ - "AND", - "OR" - ] - } - }, - "type": "object", - "required": [ - "operator", - "operands", - "use" - ], - "additionalProperties": false - }, - "type": "array" - }, - "contextualParameters": { - "additionalProperties": { - "description": "Config key value", - "type": "string" - }, - "description": "Each contextual parameter present in the feature schema must be included, if you are not sure what are the contextual parameters for this feature, you can use acs_get_feature tool", - "type": "object" - }, - "operationType": { - "enum": [ - "Upsert", - "Delete" - ], - "type": "string", - "description": "Operation type for the record change" - }, - "value": { - "type": "string", - "description": "New value for the attribute (required for Upsert operations) as stringified json, if you are not sure what is the expected value type for this feature, you can use acs_get_feature tool" - } - }, - "type": "object", - "required": [ - "operationType", - "contextualParameters", - "attribute" - ] - } - } - }, - "required": [ - "recordChanges" - ], - "type": "object", - "additionalProperties": false - }, - "crId": { - "type": "string", - "description": "Optional CR id to raise a new revision rather than making a new CR" - }, - "stage": { - "type": "string", - "description": "Stage to query", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ] - }, - "ticketLink": { - "type": "string", - "description": "Optional link to a ticket related to this change" - }, - "featureName": { - "type": "string", - "description": "Feature name to modify records for" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "featureName", - "records", - "changeSummary", - "stage" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "read_quip", - "description": "Read Quip document content\n\nThis tool retrieves the content of a Quip document in either HTML or Markdown format:\n\n- HTML format: More verbose but contains section IDs and additional metadata.\n These unique section IDs (for h1, h2, h3, p, etc.) can be used with the edit_quip tool\n to make targeted edits to specific sections of the document.\n\n- Markdown format: More concise and easier to read, but does not contain section IDs\n or additional metadata. Best for when you just need the content in a readable format\n and don't need to make targeted edits.\n\nWorkflow:\n1. Use read_quip to get the document content\n2. Identify the section ID you want to modify (when using HTML format)\n3. Use edit_quip with the section ID and appropriate location parameter\n\nExamples:\n1. Read document in HTML format (default):\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\"\n}\n```\n\n2. Read document in Markdown format:\n```json\n{\n \"documentId\": \"https://quip-amazon.com/abc/Doc\",\n \"format\": \"markdown\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "format": { - "description": "Format to return the content in (html or markdown)", - "type": "string", - "enum": [ - "html", - "markdown" - ] - }, - "documentId": { - "type": "string", - "description": "The Quip document URL or ID to read" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "documentId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_get_feature", - "description": "Get detailed information about a specific feature (also called configuration) from Amazon Config Store.\nRetrieves full details of a feature including schema, owners, clients, and more, but not the records (config values), use acs_list_records for that.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "name" - ], - "type": "object", - "additionalProperties": false, - "properties": { - "stage": { - "type": "string", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "default": "PROD", - "description": "Stage to query" - }, - "name": { - "description": "Feature name to retrieve", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_list_cp_records", - "description": "Get the records for a given contextual parameter (also called config key, or CP) from Amazon Config Store.\nRetrieves all records associated with a contextual parameter.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "properties": { - "stage": { - "description": "Stage to query", - "default": "PROD", - "type": "string", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ] - }, - "inRevision": { - "description": "Optional revision to retrieve records from", - "type": "string" - }, - "fromRevision": { - "type": "string", - "description": "Optional starting revision for retrieving records" - }, - "name": { - "type": "string", - "description": "Contextual parameter name to retrieve records for" - } - }, - "additionalProperties": false, - "required": [ - "name" - ], - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_get_contextual_parameter", - "description": "Get detailed information about a specific contextual parameter (also called config key, or CP) from Amazon Config Store.\nRetrieves full details of a contextual parameter including type, owners, status, and more, but not the records (config key values), use acs_list_contextual_parameter_records for that.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "name" - ], - "properties": { - "stage": { - "default": "PROD", - "type": "string", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "description": "Stage to query" - }, - "name": { - "type": "string", - "description": "Contextual parameter name to retrieve" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "search_fleet_credit_score", - "description": "Retrieve operational credit scores for fleets managed by a specific Amazon manager, identified by their alias.\n This tool should be used when you want to evaluate the operational performance and creditworthiness of all fleets under a given manager. \n The credit score here refers specifically to Amazon's internal fleet operational scoring system, **not** to a financial or consumer credit score.\n \n Each fleet's ID and associated operational credit score will be returned. \n These scores help in identifying at-risk fleets and evaluating performance for compliance, reliability, and delivery operations.\n\n ### Use Cases:\n • \"What are the credit scores of fleets managed by alias 'samishra@'?\"\n • \"Give me all fleet IDs and their scores under the manager 'samishra@'.\"\n \n ### When NOT to Use:\n • DO NOT use this tool to get personal or financial credit scores.\n • DO NOT use this tool if you don't have the manager alias.\n • NOT suitable for querying single fleet score (use a more targeted tool if available).\n \n ### Caveats:\n • Only works for Amazon-internal operational fleet credit score system.\n • The data may have a short refresh delay (up to 24 hours).\n • You must have permission to view data under the provided manager alias.", - "input_schema": { - "json": { - "properties": { - "alias": { - "description": "Manager alias to fetch credit scores for", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "alias" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_contact_lookup", - "description": "This tool is for looking up contacts on the AWS Salesforce AKA AWSentral", - "input_schema": { - "json": { - "type": "object", - "properties": { - "phone": { - "description": "the phone number of the contact", - "type": "string" - }, - "account_name": { - "type": "string", - "description": "the name of the account associated with the contact" - }, - "contact_name": { - "type": "string", - "description": "the name of the contact" - }, - "contact_id": { - "type": "string", - "description": "the id of the contact" - }, - "email": { - "description": "the email address of the contact", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "plantuml", - "description": "Create and decode PlantUML diagrams using Amazon's internal PlantUML server.\nPlantUML allows creating UML diagrams from text descriptions.\n\nSupported operations:\n- encode: Convert PlantUML text to an encoded URL\n- decode: Extract PlantUML text from an encoded URL", - "input_schema": { - "json": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "PlantUML content for encode operation" - }, - "operation": { - "enum": [ - "encode", - "decode" - ], - "description": "The operation to perform", - "type": "string" - }, - "url": { - "description": "PlantUML URL for decode operation", - "format": "uri", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "operation" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "create_folder_quip", - "description": "Create a new Quip folder\n\nThis tool creates a new folder in Quip.\nYou can optionally specify a parent folder to create a subfolder.\n\nExamples:\n1. Create a root-level folder:\n```json\n{\n \"title\": \"New Project Folder\"\n}\n```\n\n2. Create a subfolder:\n```json\n{\n \"title\": \"Documentation\",\n \"parentFolderId\": \"ABCDEF123456\"\n}\n```\n", - "input_schema": { - "json": { - "required": [ - "title" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "title": { - "type": "string", - "description": "Title of the new folder" - }, - "parentFolderId": { - "description": "ID of parent folder (if not provided, creates at root level)", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "list_katal_components", - "description": "List all available Katal components\n\nThis tool returns a list of all available components in the Katal library,\norganized by category with basic information about each component.\n\nExample usage:\n```json\n{}\n```", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": {}, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_create_project", - "description": "Creates a new Pippin design project with specified details", - "input_schema": { - "json": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "requirements": { - "type": "string", - "description": "Project requirements" - }, - "name": { - "description": "Project name", - "type": "string" - }, - "bindleId": { - "description": "Bindle ID", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_search_resources", - "description": "Search for resources in Amazon Config Store based on a query string.\nReturns matching features, contextual parameters, tags, or attributes based on the search criteria.\nThis retrieves only the metadata of the resource and not the full details.\nYou can optionally filter by resource types: FEATURE, CONTEXTUAL_PARAMETER, TAG, ATTRIBUTE.\nYou must specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "type": "object", - "properties": { - "queryString": { - "minLength": 1, - "description": "Search query string to find matching resources", - "type": "string" - }, - "stage": { - "type": "string", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "description": "Stage to query", - "default": "PROD" - }, - "resourceTypes": { - "description": "Optional filter for resource types to search", - "type": "array", - "items": { - "enum": [ - "FEATURE", - "CONTEXTUAL_PARAMETER", - "TAG", - "ATTRIBUTE" - ], - "type": "string" - } - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "queryString" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_config_helper", - "description": "Get help configuring JIRA tools for Q CLI", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "token": { - "description": "Your JIRA token (optional - for validation)", - "type": "string" - }, - "jira_url": { - "type": "string", - "description": "Your JIRA instance URL (optional - for validation)" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "search_quip_created_by_current_user", - "description": "Get all documents created by the current user\n\nThis tool retrieves all Quip documents that were created by the current user.\nYou can optionally filter the results by date range to get documents created within a specific time period.\n\nThe tool fetches all user threads, then filters them to show only documents authored by the current user,\nwith optional date range filtering for more targeted results.\n\nDate format: Use ISO 8601 format (YYYY-MM-DD) for date parameters.\n\nExamples:\n1. Get all documents created by current user:\n```json\n{\n}\n```\n\n2. Get documents created within a date range:\n```json\n{\n \"startDate\": \"2024-01-01\",\n \"endDate\": \"2024-12-31\"\n}\n```\n\n3. Get documents created after a specific date:\n```json\n{\n \"startDate\": \"2024-06-01\"\n}\n```\n\n4. Get documents created before a specific date:\n```json\n{\n \"endDate\": \"2024-06-30\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "startDate": { - "description": "Start date for filtering documents (YYYY-MM-DD format)", - "type": "string" - }, - "endDate": { - "type": "string", - "description": "End date for filtering documents (YYYY-MM-DD format)" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_read_knowledge", - "description": "Access and retrieve the full content of knowledge documents using either a file path or document title. This tool enables direct retrieval of stored knowledge resources from the configured knowledge base, supporting both absolute and relative paths. Returns the document content along with path and title metadata.", - "input_schema": { - "json": { - "properties": { - "title": { - "type": "string", - "description": "The title of the document to find" - }, - "path": { - "type": "string", - "description": "The path to the document file" - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "orca_get_execution_data", - "description": "Get execution data for a specific run in Orca Studio.\n\nExecution data is a key-value map (Shared Data) that is specified as\na payload for work items (workflow instances) and output artifacts\ngenerated during a workflow run. This tool is useful for debugging \nworkflow issues, extracting processed data from completed runs,\nor analyzing the data flow through specific workflow executions.\n\nThis tool retrieves detailed execution data including execution data map\nfor a specific runId within an objectId.\n\nLimitations:\n- If the Execution data is large it could cause performance issues\n- Supported classification of data is until orange\n- Large datasets may experience timeout issues (default 60s timeout)\n\nParameters:\n- objectId: (required) The object ID\n- workflowName: (required) The workflow name\n- runId: (required) The specific run ID to get data for\n- clientId: (required) The Orca client ID\n- region: (optional) AWS region (defaults to us-east-1)\n\nExample:\n```json\n{ \"objectId\": \"d7f71182-d7b8-4886-8d07-15c404a82583\", \"workflowName\": \"GenerateReportForNCA-beta\", \"runId\": \"b9d9c02a-d3f0-4da8-9601-1740f1aaaeae\", \"clientId\": \"SafrReportingSILServiceBeta\" }\n```", - "input_schema": { - "json": { - "properties": { - "objectId": { - "description": "The object ID", - "type": "string" - }, - "workflowName": { - "description": "The workflow name", - "type": "string" - }, - "runId": { - "type": "string", - "description": "The specific run ID to get data for" - }, - "clientId": { - "type": "string", - "description": "The Orca client ID" - }, - "region": { - "type": "string", - "description": "AWS region (defaults to us-east-1)" - } - }, - "type": "object", - "additionalProperties": false, - "required": [ - "objectId", - "workflowName", - "runId", - "clientId" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_create_contextual_parameter", - "description": "Creates a new contextual parameter (also called config key, or CP) in Amazon Config Store.\nThis tool allows creating a contextual parameter with the specified name, owners, approvers, and other attributes.\nThe name of the contextual parameter should be unique.\nThe contextual parameter will be created in SANDBOX stage, it can then be promoted to other stages from the UI after it is approved by ACS team.\nIf any of the required parameters are not provided, you MUST ASK the user for them.", - "input_schema": { - "json": { - "required": [ - "name", - "owners", - "cti", - "description", - "approximateMaxRecordsCount", - "exampleValues" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "owners": { - "minItems": 1, - "type": "array", - "items": { - "additionalProperties": false, - "type": "object", - "properties": { - "type": { - "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it.", - "enum": [ - "BINDLE", - "TEAM", - "POSIX_GROUP", - "AAA" - ], - "type": "string" - }, - "name": { - "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - } - }, - "required": [ - "type", - "name" - ] - }, - "description": "List of owners. Do NOT assume this info, you MUST ask the user about it." - }, - "cti": { - "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", - "required": [ - "category", - "type", - "item" - ], - "type": "object", - "additionalProperties": false, - "properties": { - "category": { - "description": "CTI category. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - }, - "type": { - "type": "string", - "description": "CTI type. Do NOT assume this info, you MUST ask the user about it." - }, - "item": { - "description": "CTI item. Do NOT assume this info, you MUST ask the user about it.", - "type": "string" - } - } - }, - "validations": { - "items": { - "anyOf": [ - { - "required": [ - "kind", - "targetAttributes", - "regex" - ], - "properties": { - "kind": { - "description": "For string attributes, which validates attributes against a predefined regex.", - "type": "string", - "const": "Pattern" - }, - "targetAttributes": { - "items": { - "type": "string" - }, - "type": "array", - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes must be of type String. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - }, - "regex": { - "description": "Regex pattern", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - }, - { - "properties": { - "kind": { - "description": "For integer and long attributes, which validates that the attributes fall within a predefined range (defined by min and max values).", - "type": "string", - "const": "Range" - }, - "targetAttributes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "list of strings, each of them specifies the path to an attribute.All of the target attributes should be of type Integer or Long. The validation will be applied to all specified target attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - }, - "min": { - "description": "Range minimum value (inclusive)", - "type": "string" - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - }, - "max": { - "type": "string", - "description": "Range maximum value (inclusive)" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "kind", - "targetAttributes" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "targetAttributes": { - "type": "array", - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. The target attribute needs to be a descendant of a struct attribute. Non Null validation only applies to struct attributes. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/", - "items": { - "type": "string" - } - }, - "kind": { - "const": "NonNull", - "type": "string", - "description": "For struct attributes, which validate that child attributes of a struct are non-null. This validation only applies to struct attributes." - }, - "description": { - "description": "Contains the ACS customer explanation for a given validation.", - "type": "string" - } - }, - "required": [ - "kind", - "targetAttributes" - ] - }, - { - "required": [ - "kind", - "targetAttributes", - "arn" - ], - "additionalProperties": false, - "properties": { - "kind": { - "const": "Lambda", - "type": "string", - "description": "Lambda validation allows you to use your own custom logic to validate record values." - }, - "targetAttributes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "list of strings, each of them specifies the path to an attribute.All target attributes should be previously defined in the schema. Your Lambda function will receive as input the record or sub-record defined by the target attribute. Refer to our wiki for guidance: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/SchemaValidation/" - }, - "arn": { - "description": "Lambda ARN", - "type": "string" - }, - "description": { - "description": "Description of what the Lambda validates.", - "type": "string" - } - }, - "type": "object" - } - ] - }, - "minItems": 0, - "description": "Validations for the contextual parameter records", - "type": "array" - }, - "exampleValues": { - "type": "string", - "minLength": 1, - "description": "Example of values this contextual parameters is going to hold. Comma separated strings: example_1, example_2... and so on." - }, - "description": { - "description": "Description of the contextual parameter", - "type": "string", - "minLength": 1 - }, - "approvers": { - "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." - }, - "type": { - "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it.", - "type": "string", - "enum": [ - "USER", - "LDAP", - "POSIXG", - "TEAM", - "SNS" - ] - }, - "requiredCount": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Required count of approvers" - } - }, - "required": [ - "type", - "name" - ] - } - }, - "approximateMaxRecordsCount": { - "type": "integer", - "description": "Cardinality of the contextual parameter, how many records this contextual parameter is going to hold on the long term.", - "minimum": 1 - }, - "crFeatureEnabled": { - "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true.", - "default": true, - "type": "boolean" - }, - "parentKeys": { - "description": "Parent contextual parameters for this contextual parameter. This is only needed when you are creating a composite contextual parameter", - "type": "string" - }, - "name": { - "description": "Name of the contextual parameter to create. It should be in snake_case.", - "minLength": 1, - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "get_work_contribution", - "description": "Get a specific work contribution by ID from AtoZ PortfolioWidgetService.\n\nLimitations:\nYou can only access your own work contributions\n\nThis tool retrieves detailed information about a work contribution, including:\n- Title and summary\n- Edit status\n- Start and end dates\n- Associated artifacts\n- Stakeholders\n\nYou must provide the work contribution ID to retrieve the details.", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "workContributionId": { - "type": "string", - "description": "The ID of the work contribution to retrieve" - } - }, - "required": [ - "workContributionId" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_list_projects", - "description": "Lists all available Pippin design projects", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "properties": { - "maxResults": { - "description": "Maximum number of results to return", - "type": "number" - }, - "nextToken": { - "type": "string", - "description": "Pagination token" - }, - "statuses": { - "type": "string", - "description": "Project statuses to filter by" - }, - "user": { - "description": "User to filter by", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "mox_console", - "description": "Access the MOX console to fetch order data from MORSE service", - "input_schema": { - "json": { - "type": "object", - "properties": { - "merchantCustomerId": { - "type": [ - "string", - "number" - ], - "description": "The merchant customer ID (e.g., 994273326)" - }, - "retrievePromotions": { - "description": "Whether to retrieve promotions", - "type": "boolean", - "default": true - }, - "orderIds": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ], - "description": "The order ID(s) to retrieve. Can be a single order ID or an array of order IDs." - }, - "operation": { - "enum": [ - "getOrderDetailsNonUCI" - ], - "description": "The operation to perform. Available operations: getOrderDetailsNonUCI", - "type": "string" - }, - "retrieveOrderReportData": { - "type": "boolean", - "description": "Whether to retrieve order report data", - "default": true - }, - "hostname": { - "type": "string", - "description": "Optional custom hostname for the API endpoint" - }, - "region": { - "default": "USAmazon", - "description": "The region to use for the API endpoint (USAmazon, EUAmazon, JPAmazon)", - "type": "string", - "enum": [ - "USAmazon", - "EUAmazon", - "JPAmazon" - ] - }, - "retrieveExtendedItemFields": { - "type": "boolean", - "description": "Whether to retrieve extended item fields", - "default": true - } - }, - "additionalProperties": false, - "required": [ - "operation", - "merchantCustomerId", - "orderIds" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "add_work_contribution_stakeholder", - "description": "Add a stakeholder to a work contribution in AtoZ.\n\nThis tool allows you to add a stakeholder (collaborator) to an existing work contribution.\n\nLimitations:\nYou can only access your own work contributions\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- stakeholderLogin or stakeholderPersonId: The stakeholder to add\n- ownerLogin or ownerPersonId: The owner of the work contribution", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "ownerPersonId": { - "type": "string", - "description": "Person ID of the work contribution owner" - }, - "stakeholderLogin": { - "type": "string", - "description": "Login/alias of the stakeholder to add" - }, - "workContributionId": { - "type": "string", - "description": "ID of the work contribution" - }, - "stakeholderPersonId": { - "type": "string", - "description": "Person ID of the stakeholder to add" - }, - "ownerLogin": { - "description": "Login/alias of the work contribution owner", - "type": "string" - } - }, - "required": [ - "workContributionId" - ], - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "g2s2_create_cr", - "description": "Creates a code review for a specified G2S2 stage version", - "input_schema": { - "json": { - "required": [ - "stageVersion", - "description" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "crId": { - "type": "string", - "description": "Existing CR ID to update (optional)" - }, - "description": { - "description": "A CR description to add", - "type": "string" - }, - "stageVersion": { - "description": "The stage version to create a code review for", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "oncall_compass_query_reports", - "description": "Query Oncall reports from Oncall Compass (https://oncall.ai.amazon.dev/). Currently it will return most recently generated reports by the user. The user's authentication token (~/.midway/cookie) will be used for identifying the user.", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": {} - } - } - } - }, - { - "ToolSpecification": { - "name": "policy_engine_get_risk", - "description": "Access Amazon Policy Engine risk information for a specific entity. This tool allows you to retrieve detailed information about a specific risk or violation from Policy Engine.", - "input_schema": { - "json": { - "type": "object", - "required": [ - "entityId" - ], - "properties": { - "entityId": { - "description": "Entity ID of the risk/violation to view details for", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_get_artifact", - "description": "Retrieves a specific Pippin artifact by its ID", - "input_schema": { - "json": { - "additionalProperties": false, - "properties": { - "projectId": { - "type": "string", - "description": "Project ID" - }, - "designId": { - "type": "string", - "description": "Artifact ID" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "projectId", - "designId" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "get_katal_component", - "description": "Get detailed information about a specific Katal component\n\nThis tool retrieves comprehensive documentation and usage information for a given Katal component,\nincluding properties, methods, examples, guidelines, and accessibility information.\n\nExamples:\n1. Get Button component info:\n```json\n{\n \"name\": \"Button\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the Katal component to get information about" - } - }, - "required": [ - "name" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_update_feature", - "description": "Updates a feature (also called configuration) in Amazon Config Store.\nThis tool allows updating a feature by only giving it the parameters required to be updated, other parameters that are not provided will remain as is.\nIf any of the required parameters are not provided, do NOT assume them, just leave them empty.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "type": "object", - "required": [ - "name", - "stage" - ], - "properties": { - "name": { - "description": "Name of the feature to update. It should be in PascalCase.", - "type": "string", - "minLength": 1 - }, - "teamName": { - "type": "string", - "description": "Team name responsible for the feature. Do NOT assume this info, you MUST ask the user about it." - }, - "teamWikiLink": { - "type": "string", - "description": "Team wiki link. Do NOT assume this info, you MUST ask the user about it." - }, - "configSnapshotEnabled": { - "type": "boolean", - "description": "Whether config snapshot is enabled. Config snapshot allows the user to use deployable cache and dynamic refresher for example. Know more about deployable cache from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/OnBoarding/Cache/#HDeployablecache and dynamic refresher from here: https://w.amazon.com/bin/view/INTech/AmazonConfigStore/DeveloperGuide/DynamicRefresher." - }, - "cti": { - "description": "CTI information. Do NOT assume this info, you MUST ask the user about it.", - "additionalProperties": false, - "properties": { - "category": { - "type": "string", - "description": "CTI category. Do NOT assume this info, you MUST ask the user about it." - }, - "item": { - "type": "string", - "description": "CTI item. Do NOT assume this info, you MUST ask the user about it." - }, - "type": { - "type": "string", - "description": "CTI type. Do NOT assume this info, you MUST ask the user about it." - } - }, - "type": "object", - "required": [ - "category", - "type", - "item" - ] - }, - "approvers": { - "items": { - "required": [ - "type", - "name" - ], - "additionalProperties": false, - "type": "object", - "properties": { - "requiredCount": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Required count of approvers" - }, - "name": { - "type": "string", - "description": "Name of the approver. Do NOT assume this info, you MUST ask the user about it." - }, - "type": { - "type": "string", - "enum": [ - "USER", - "LDAP", - "POSIXG", - "TEAM", - "SNS" - ], - "description": "Type of approver (USER, LDAP, POSIXG, TEAM, SNS). Do NOT assume this info, you MUST ask the user about it." - } - } - }, - "type": "array", - "description": "List of approvers. Required and must not be empty when crFeatureEnabled is true. Do NOT assume this info, you MUST ask the user about it.", - "minItems": 1 - }, - "upsertedAttributes": { - "items": { - "additionalProperties": false, - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "Description for the attribute" - }, - "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "description": "Name of the attribute in snake_case" - }, - "type": { - "description": "Type of the attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"", - "type": "string" - } - }, - "required": [ - "name", - "type" - ] - }, - "type": "array", - "description": "Attributes of the feature. Attributes are the fields of your config table, the value of these attributes can vary depending on contextual parameter values." - }, - "crFeatureEnabled": { - "type": "boolean", - "description": "Whether CR feature is enabled. This will raise CR for each change done to the resource. If this is true, then approvers list must be provided. You MUST ASK the user if they want it to be false or true." - }, - "owners": { - "description": "List of owners. Do NOT assume this info, you MUST ask the user about it.", - "type": "array", - "items": { - "properties": { - "type": { - "type": "string", - "enum": [ - "BINDLE", - "TEAM", - "POSIX_GROUP", - "AAA" - ], - "description": "Type of owner (BINDLE, TEAM, POSIX_GROUP, AAA). Do NOT assume this info, you MUST ask the user about it." - }, - "name": { - "type": "string", - "description": "Name of the owner. Do NOT assume this info, you MUST ask the user about it." - } - }, - "additionalProperties": false, - "required": [ - "type", - "name" - ], - "type": "object" - } - }, - "upsertedNamedTypes": { - "items": { - "properties": { - "type": { - "description": "Type definition", - "anyOf": [ - { - "type": "object", - "properties": { - "values": { - "minItems": 1, - "description": "Enum values. Values must be in snake_case.", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" - } - }, - "kind": { - "description": "Enum type used as a type for an attribute.", - "type": "string", - "const": "Enum" - } - }, - "required": [ - "kind", - "values" - ], - "additionalProperties": false - }, - { - "required": [ - "kind", - "attributes" - ], - "type": "object", - "properties": { - "attributes": { - "description": "Struct attributes", - "minItems": 1, - "type": "array", - "items": { - "required": [ - "name", - "type" - ], - "additionalProperties": false, - "properties": { - "description": { - "description": "Description of the struct attribute.", - "type": "string" - }, - "name": { - "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", - "type": "string", - "description": "Name of the struct attribute. It should be in snake_case." - }, - "type": { - "type": "string", - "description": "Type of the struct attribute: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"" - } - }, - "type": "object" - } - }, - "kind": { - "const": "Struct", - "type": "string", - "description": "Struct type used as a type for an attribute." - } - }, - "additionalProperties": false - }, - { - "additionalProperties": false, - "type": "object", - "required": [ - "kind", - "element" - ], - "properties": { - "kind": { - "description": "List type used as a type for an attribute.", - "const": "List", - "type": "string" - }, - "element": { - "description": "Type of the list element: Boolean, Integer, String, Long, or one of the custom types defined in the schema \"types\"", - "type": "string" - } - } - } - ] - }, - "description": { - "description": "Description for the custom type", - "type": "string" - }, - "name": { - "description": "Name of the custom type", - "type": "string" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "name", - "type" - ] - }, - "description": "Custom types for the feature that can be used as schema attribute type, struct attribute type, or list element type", - "type": "array" - }, - "stage": { - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ], - "description": "Stage to query", - "type": "string" - }, - "description": { - "type": "string", - "description": "Description of the feature" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_opportunity_lookup", - "description": "This tool is for looking up opportunities on the AWS Salesforce AKA AWSentral", - "input_schema": { - "json": { - "properties": { - "opportunity_id": { - "description": "the id of the opportunity - this will only pull the 1 opportunity", - "type": "string" - }, - "account_name": { - "type": "string", - "description": "the name of the account with the opportunities, this will pull all opportunities that may be related to an account, but not directly associated." - }, - "opportunity_name": { - "description": "the name of the opportunity to search for", - "type": "string" - }, - "account_id": { - "type": "string", - "description": "the id of the account associated with the opportunity, this will pull all opportunities on an account, its best to use just the account_id" - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "orca_list_runs_for_objectId", - "description": "List all runs for a specific objectId in Orca Studio.\n\nAn objectId in Orca Studio represents a unique ID assigned to a single Execution.\nSince a single Execution can have multiple runs, the Object ID allows aggregation\nat a business process instance level. Use this tool when you need to\ntrack all workflow executions related to a specific object across different\nworkflows, rather than listing runs for a specific workflow.\n\nThis tool retrieves all execution runs associated with a given objectId,\nincluding runId, status, openedDate, and closedDate for each run.\n\nLimitations:\n- Results are limited to the most recent runs that haven't been deleted by retention policies (typically last 100)\n- Large datasets may experience timeout issues (default 60s timeout)\n\nParameters:\n- objectId: (required) The object ID to query runs for\n- clientId: (required) The Orca client ID\n- region: (optional) AWS region (defaults to us-east-1)\n\nExample:\n```json\n{ \"objectId\": \"d7f71182-d7b8-4886-8d07-15c404a82583\", \"clientId\": \"SafrReportingSILServiceBeta\" }\n```", - "input_schema": { - "json": { - "required": [ - "objectId", - "clientId" - ], - "type": "object", - "additionalProperties": false, - "properties": { - "objectId": { - "type": "string", - "description": "The object ID to query runs for" - }, - "clientId": { - "type": "string", - "description": "The Orca client ID" - }, - "region": { - "description": "AWS region (defaults to us-east-1)", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_search_issues", - "description": "Search for JIRA issues using JQL", - "input_schema": { - "json": { - "properties": { - "jql": { - "type": "string", - "minLength": 1, - "description": "JQL search query" - }, - "expand": { - "type": "string", - "description": "The additional information to include in the response" - }, - "startAt": { - "description": "The index of the first result to return (0-based)", - "type": "integer", - "minimum": 0 - }, - "maxResults": { - "minimum": 1, - "type": "integer", - "maximum": 1000, - "description": "The maximum number of results to return (default: 50)" - }, - "fields": { - "description": "The list of fields to return", - "items": { - "type": "string" - }, - "type": "array" - }, - "validateQuery": { - "description": "Whether to validate the JQL query", - "type": "string" - } - }, - "required": [ - "jql" - ], - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_search_tags", - "description": "Search for tags on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to find appropriate tags for categorizing questions on Sage.\nTags help organize questions and ensure they reach the right audience.\nResults are paginated and sorted by popularity by default.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Finding relevant tags before creating a question\n- Discovering tags related to specific technologies or teams\n- Exploring popular tags in a particular domain\n\nExample usage:\n{ \"nameFilter\": \"brazil\", \"page\": 1, \"pageSize\": 10 }", - "input_schema": { - "json": { - "properties": { - "nameFilter": { - "description": "Optional filter to search for tags by name", - "type": "string" - }, - "pageSize": { - "type": "number", - "description": "Number of results per page (default: 60)" - }, - "page": { - "description": "Page number for pagination (starts at 1)", - "type": "number" - } - }, - "additionalProperties": false, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "search_people", - "description": "Search for Amazon employees with filtering by attributes like job level, location, and Bar Raiser/Manager status. This tool allows you to search for people by name, alias, or other criteria, and filter results by department, location, job level, Bar Raiser status, Manager status, and more. The tool also provides information of the employee like phoneNumber, email, buildingRoom if available in phoneTool.", - "input_schema": { - "json": { - "additionalProperties": false, - "properties": { - "query": { - "type": "string", - "description": "Search query for finding people (name, alias, etc.)" - }, - "filters": { - "type": "object", - "properties": { - "isBarRaiser": { - "description": "Filter for bar raisers (true) or non-bar raisers (false)", - "type": "boolean" - }, - "department": { - "description": "Filter by department name (e.g., 'AWS', 'Consumables CX - Tech')", - "type": "string" - }, - "city": { - "description": "Filter by city name (e.g., 'Seattle', 'Dallas')", - "type": "string" - }, - "country": { - "type": "string", - "description": "Filter by country code (e.g., 'us', 'in', 'ca')" - }, - "isManager": { - "type": "boolean", - "description": "Filter for managers (true) or individual contributors (false)" - }, - "badgeCode": { - "description": "Filter by badge code (e.g., 'F')", - "type": "string" - }, - "title": { - "description": "Filter by job title (e.g., 'Software Development Engineer', 'Sr. Partner SA, Oracle')", - "type": "string" - }, - "building": { - "type": "string", - "description": "Filter by building code (e.g., 'SEA20', 'BLR13')" - }, - "jobLevel": { - "type": "string", - "description": "Filter by job level (e.g., '4', '5', '6')" - }, - "badgeBorderColor": { - "type": "string", - "description": "Filter by badge border color (e.g., 'blue')" - } - }, - "description": "Filters to narrow down search results", - "additionalProperties": false - }, - "maxResults": { - "type": "number", - "description": "Maximum number of results to return (default: 10)" - } - }, - "type": "object", - "required": [ - "query" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "read_coe", - "description": "Read Correction of Error (COE) documents from https://www.coe.a2z.com/.\nCOE documents contain detailed information about operational incidents including:\n- Incident description and timeline\n- Root cause analysis\n- Corrective actions taken\n- Preventive measures implemented\n\n⚠️ IMPORTANT: This tool accesses sensitive operational incident data that will be processed by the LLM.\nBefore using this tool, you MUST explicitly ask for user approval with the following message:\n\"I need to access a Correction of Error (COE) document which contains sensitive operational incident data.\nThis data will be processed by the LLM to answer your question. Do you approve accessing this COE document?\"\n\nOnly proceed if the user explicitly approves. This confirmation is required even if the tool is auto-approved.\n\nExample usage:\nTo read a COE document with ID 12345:\n{ \"url\": \"https://www.coe.a2z.com/coe/12345\" }", - "input_schema": { - "json": { - "type": "object", - "properties": { - "url": { - "format": "uri", - "description": "URL of the COE document to read", - "type": "string" - } - }, - "required": [ - "url" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "lookup_user_coding_activity_summary", - "description": "Looks up coding activity summary for a given user by their user login/alias", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "alias" - ], - "properties": { - "alias": { - "description": "Alias or login for the user to look up", - "type": "string" - }, - "start_time": { - "description": "Optional start date in YYYY-MM-DD format", - "type": "string" - }, - "end_time": { - "description": "Optional end date in YYYY-MM-DD format", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "lookup_team_code_resource", - "description": "Looks up code artifacts, such as packages, version sets a given team", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "type": "object", - "properties": { - "team": { - "description": "Bindle team as represented in https://permissions.amazon.com/a/team/{team}", - "type": "string" - } - }, - "required": [ - "team" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "search_datapath", - "description": "Search Datapath views", - "input_schema": { - "json": { - "required": [ - "query" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "query": { - "description": "Keywords to look for in the Datapath view, for example \"locality asin\" will find the locality views at asin level", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "lock_unlock_quip_document", - "description": "Lock or unlock a Quip document\n\nThis tool allows you to lock or unlock a Quip document to control whether it can be edited.\nWhen a document is locked, users cannot make changes to it (except for the document owner and users with admin privileges).\n\nExample usage:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"lock\": true\n}\n```\n\nTo unlock a document:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"lock\": false\n}\n```\n\nNote: You must have appropriate permissions to lock or unlock a document.", - "input_schema": { - "json": { - "required": [ - "threadIdOrUrl", - "lock" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "threadIdOrUrl": { - "description": "The thread ID or Quip URL of the document to lock or unlock", - "type": "string" - }, - "lock": { - "description": "Tick the checkbox to lock or uncheck to unlock the document", - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "read_orr", - "description": "Read Operational Readiness Review (ORR) documents from https://www.orr.reflect.aws.dev/.\nORR documents contain detailed information about operational readiness reviews including:\n- Review questions and answers\n- Service or feature assessments\n- Operational readiness criteria\n- Launch approval status\n\n⚠️ IMPORTANT: This tool accesses sensitive operational review data that will be processed by the LLM.\nBefore using this tool, you MUST explicitly ask for user approval with the following message:\n\"I need to access an Operational Readiness Review (ORR) document which contains sensitive operational data.\nThis data will be processed by the LLM to answer your question. Do you approve accessing this ORR document?\"\n\nOnly proceed if the user explicitly approves. This confirmation is required even if the tool is auto-approved.\n\nExample usage:\nTo read an ORR document with a specific review ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/review/687e56b9-d3d4-4bd5-b033-379461c96381/questions\" }\n\nTo read an ORR template:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\" }\n\nTo read only a specific section by ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"sectionId\": \"53886aad-5ef9-4450-9da0-de7365ef07cb\" }\n\nTo read only a specific section by title:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"sectionTitle\": \"Axiom 01 - AZ Resilience\" }\n\nTo read only a specific question by ID:\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"questionId\": \"039ee146-7a05-4e4f-b10e-4eebb574f093\" }\n\nTo read only a specific question by prompt text (supports partial matching):\n{ \"url\": \"https://www.orr.reflect.aws.dev/template/787a767f-af3a-4747-97ca-b617d2e4cbe0/content\", \"questionPrompt\": \"AZ failure\" }", - "input_schema": { - "json": { - "type": "object", - "required": [ - "url" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "sectionTitle": { - "type": "string", - "description": "Optional title of a specific section to return" - }, - "sectionId": { - "description": "Optional ID of a specific section to return", - "type": "string" - }, - "questionPrompt": { - "type": "string", - "description": "Optional prompt text to search for in questions (supports partial matching)" - }, - "url": { - "format": "uri", - "description": "URL of the ORR document to read", - "type": "string" - }, - "questionId": { - "type": "string", - "description": "Optional ID of a specific question to return" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "figma_to_code", - "description": "Generate code from Figma mockups and designs via Alchemy API.\nSupports multiple output formats including React, HTML and Storm UI.\nAnalyzes Figma design data to generate production-ready code.", - "input_schema": { - "json": { - "required": [ - "figmaUrl" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "figmaUrl": { - "type": "string", - "format": "uri", - "description": "Figma URL containing the design to convert to code" - }, - "outputFormat": { - "description": "Output code format/framework", - "type": "string", - "enum": [ - "react", - "html", - "storm-ui" - ], - "default": "react" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "rtla_fetch_single_request_logs", - "description": "Fetch detailed logs for a single request from RTLA (Real-Time Log Analysis) API. This tool allows you to retrieve comprehensive log entries for a specific request ID, including error logs, stack traces, and detailed request information. The response is automatically filtered to include only essential debugging fields for easier analysis. Useful for deep-dive troubleshooting of specific issues, analyzing error patterns for individual requests, and getting complete context for failed transactions.", - "input_schema": { - "json": { - "required": [ - "org", - "requestType", - "date", - "requestId" - ], - "properties": { - "org": { - "description": "Organization identifier (e.g., \"CWCBCCECMPROD\")", - "type": "string" - }, - "requestType": { - "type": "string", - "description": "Type of request logs to retrieve (e.g., \"FATAL\", \"NONFATAL\")" - }, - "date": { - "type": "number", - "description": "Date in milliseconds since epoch when the request occurred" - }, - "requestId": { - "type": "string", - "description": "Specific request ID to fetch logs for (e.g., \"GHHJD10YZDJNXT062G2X\")" - }, - "identifyAdditionalOrgs": { - "description": "Whether to identify additional organizations related to this request", - "type": "boolean", - "default": true - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "post_talos_correspondence", - "description": "Post correspondence on a Talos security task\n\nThis tool allows posting comments/correspondence on a specific Talos security task.\nIt uses the Talos API to create new correspondence entries for tasks.\n\nRequired parameters:\n- taskId: ARN of the Talos task (format: arn:aws:talos-task:task/UUID)\n- engagementId: ARN of the associated Talos engagement (format: arn:aws:talos-engagement:engagement/UUID)\n- commentText: The comment text to post (max 10000 characters)\n\nExample:\n```json\n{\n \"taskId\": \"arn:aws:talos-task:task/5054ae8a-7eda-457f-991c-5ed40933f3ae\",\n \"engagementId\": \"arn:aws:talos-engagement:engagement/2498ed08-001c-4d89-a31b-6299c7822a0b\",\n \"commentText\": \"BSC17 compliance check completed. Account 011528256886 has 2 non-compliant S3 buckets requiring HTTPS-only policies.\"\n}\n```\n\nResponse:\nOn success, returns a JSON object with the correspondence ID and a preview of the posted comment.\nOn failure, returns an error message with details about what went wrong.\n\nLimitations and Requirements:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n- Limited to 10 requests per minute per user (rate limit)\n- Comments cannot be edited or deleted through this tool once posted\n- User must have appropriate permissions to access the specified Talos task and engagement\n- Task and engagement must exist and be in a valid state to accept comments\n\nWhen NOT to use this tool:\n- Do not use for posting sensitive or classified information that should not be stored in Talos\n- Do not use for posting large attachments or binary data (use the Talos UI directly instead)\n- Do not use for bulk commenting on multiple tasks (use the Talos UI or API directly for batch operations)\n- Do not use for retrieving task information (use the talos_get_task tool instead)", - "input_schema": { - "json": { - "type": "object", - "properties": { - "engagementId": { - "description": "ARN of the associated Talos engagement", - "pattern": "^arn:aws:talos-engagement:engagement\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", - "type": "string", - "minLength": 1 - }, - "commentText": { - "minLength": 1, - "maxLength": 10000, - "type": "string", - "description": "The comment text to post" - }, - "taskId": { - "minLength": 1, - "description": "ARN of the Talos task to post comment to", - "type": "string", - "pattern": "^arn:aws:talos-task:task\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" - } - }, - "required": [ - "taskId", - "engagementId", - "commentText" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_sync_project_to_local", - "description": "Synchronizes a Pippin project's artifacts to a local directory", - "input_schema": { - "json": { - "type": "object", - "required": [ - "projectId", - "outputDirectory" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "projectId": { - "description": "Project ID", - "type": "string" - }, - "includeMetadata": { - "type": "boolean", - "description": "Include metadata files (.meta.json)" - }, - "outputDirectory": { - "description": "Local directory to save artifacts", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "admiral_instance_timeline", - "description": "Fetch and parse the timeline of an EC2 instance from Admiral.\nAdmiral is an internal Amazon tool that provides information about EC2 instances.\nThis tool is useful for troubleshooting AWS EC2 instances.\n\nParameters:\n- region: (optional) Airport code for AWS region (e.g., iad, pdx, sfo). Defaults to 'iad'.\n- instance_id: (required) EC2 instance ID (e.g., i-0285d2cffe9d1958d).", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "instance_id" - ], - "properties": { - "instance_id": { - "description": "EC2 instance ID (e.g., i-0285d2cffe9d1958d)", - "type": "string" - }, - "region": { - "default": "iad", - "description": "Airport code for AWS region (e.g., iad, pdx, sfo)", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "get_recent_messages_quip", - "description": "Get recent messages from a Quip thread\n\nThis tool retrieves the most recent messages for a given Quip thread.\nYou can filter and sort the messages using various parameters.\n\nExamples:\n1. Get recent messages:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\"\n}\n```\n\n2. Get recent messages with count:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"count\": 10\n}\n```\n\n3. Get recent edit messages:\n```json\n{\n \"threadIdOrUrl\": \"https://quip-amazon.com/abc/Doc\",\n \"messageType\": \"edit\"\n}\n```", - "input_schema": { - "json": { - "required": [ - "threadIdOrUrl" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "updatedSinceUsec": { - "type": "number", - "description": "UNIX timestamp in microseconds for messages updated at and after" - }, - "sortedBy": { - "enum": [ - "ASC", - "DESC" - ], - "description": "Sort order for messages", - "type": "string" - }, - "maxCreatedUsec": { - "type": "number", - "description": "UNIX timestamp in microseconds for messages created at and before" - }, - "sortBy": { - "type": "string", - "description": "Alias for sortedBy", - "enum": [ - "ASC", - "DESC" - ] - }, - "count": { - "type": "number", - "description": "Number of messages to return (1-100, default 25)" - }, - "lastUpdatedSinceUsec": { - "description": "UNIX timestamp in microseconds for messages updated before", - "type": "number" - }, - "threadIdOrUrl": { - "description": "The thread ID or Quip URL to get messages from", - "type": "string" - }, - "messageType": { - "description": "Type of messages to return", - "type": "string", - "enum": [ - "message", - "edit" - ] - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "search_resilience_score", - "description": "Search for resiliency scores for a manager's alias.\n • Required: manager alias\n • Optional: page size, page number, and score version\n • Returns resiliency score data for services under the specified manager", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "alias" - ], - "type": "object", - "properties": { - "pageSize": { - "type": "number", - "description": "Number of results per page (default: 4000)" - }, - "pageNumber": { - "type": "number", - "description": "Page number to fetch (default: 0)" - }, - "scoreVersion": { - "description": "Version of the score to fetch (default: 0.7.0)", - "type": "string" - }, - "alias": { - "type": "string", - "description": "Manager alias to fetch resiliency scores for" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_get_project", - "description": "Retrieves a Pippin design project by its ID", - "input_schema": { - "json": { - "additionalProperties": false, - "required": [ - "projectId" - ], - "type": "object", - "properties": { - "projectId": { - "type": "string", - "description": "Project ID" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_territory_lookup", - "description": "This tool is for looking up territories and retrieving an account list on the AWS Salesforce AKA AWSentral", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "territory_name": { - "type": "string", - "description": "the name of the territory to search for" - }, - "territory_id": { - "description": "the id of the territory to retrieve", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_get_tag_details", - "description": "Get detailed information about a specific tag on Sage (Amazon's internal Q&A platform).\n\nThis tool retrieves comprehensive information about a tag, including its ID, description, and ownership.\nUse this information when creating questions to ensure proper tag usage.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Verifying tag ownership before using it\n- Getting detailed descriptions of tags\n- Finding contact information for tag owners\n\nExample usage:\n{ \"tagName\": \"brazil\" }", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "required": [ - "tagName" - ], - "properties": { - "tagName": { - "type": "string", - "description": "Name of the tag to retrieve details for" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_get_knowledge_metadata", - "description": "Extract comprehensive metadata from knowledge documents, including YAML frontmatter, tags, internal links, tasks, headings, and file attributes. This tool provides structural and organizational information about documents without retrieving the full content, supporting knowledge management and document analysis workflows.", - "input_schema": { - "json": { - "properties": { - "path": { - "description": "The path to the document file", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [ - "path" - ], - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "get_folder_quip", - "description": "Get detailed information about a Quip folder\n\nThis tool retrieves detailed information about a specific folder,\nincluding its title, color, parent folder, and child folders.\n\nExample:\n```json\n{\n \"folderId\": \"ABCDEF123456\"\n}\n```", - "input_schema": { - "json": { - "additionalProperties": false, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "folderId" - ], - "properties": { - "folderId": { - "type": "string", - "description": "The ID of the folder to retrieve information about" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "search_symphony", - "description": "Search for Symphony CREATIVE/PLACEMENT/EVENT/TAG with region id and query, this tool allows you to search Symphony objects by many dimensions, including Symphony creative owner, id, displayName etc.", - "input_schema": { - "json": { - "required": [ - "region", - "type", - "query" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "query": { - "description": "Stringified query and sort key from the Elasticsearch DSL.", - "type": "string" - }, - "pageSize": { - "type": "number", - "description": "minimum: 1, maximum: 50" - }, - "type": { - "type": "string", - "description": "Content Symphony CREATIVE/PLACEMENT/EVENT/TAG" - }, - "region": { - "description": "Symphony region that are going to query data, e.g.: NA, EU, FE, Integ", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "imr_costs_search_fleet", - "description": "Search for fleets based on a query term, matching either fleet name or fleet owner.", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "query": { - "type": "string", - "description": "Query term could be a partial fleet name or one of the owners" - }, - "includeDeleted": { - "type": "boolean", - "description": "Include deleted fleets", - "default": false - } - }, - "additionalProperties": false, - "required": [ - "query" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "orca_list_runs", - "description": "List Orca workflow runs for a specific client and workflow with filtering by status and timerange.\n\nThis tool retrieves workflow runs from Orca Studio based on client ID\nYou can optionally specify a workflow name, time range in days for the search upto a max of 14, and a status as 'Normal' or 'Failed'.\ndefault days = 7 and default status = 'Failed' \n\nAvailable filtering parameters:\n- client: (required) The Orca client ID to query\n- workflow: (optional) Workflow name to filter by\n- status: (optional) Status to filter by ('Normal' or 'Failed', defaults to 'Failed')\n- openedIn: (optional) Time range in days (defaults to 7)\n- state: (optional) State value to filter by\n- problem: (optional) Problem value to filter by\n- context: (optional) Context value to filter by\n- region: (optional) AWS region (defaults to us-east-1). Common regions include us-east-1, us-west-2, eu-west-1, etc.\n\nExample\n```json\n{ \"client\": \"MyOrcaClient\"}\n```\n\nExample with workflow:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\" }\n```\n\nExample with custom time range:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"openedIn\": \"14\" }\n```\nExample with status:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"status\": \"Normal\" }\n```\nExample with status and custom time range:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"status\": \"Normal\", \"openedIn\": \"14\" }\n```\nExample with state filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"workflow\": \"TestWorkflow\", \"state\": \"StateName::Error::Problem\" }\n```\nExample with problem filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"problem\": \"UnknownProblem\" }\n```\nExample with context filtering:\n```json\n{ \"client\": \"MyOrcaClient\", \"context\": \"live\" }\n```\n\nExample with custom region:\n```json\n{ \"client\": \"MyOrcaClient\", \"region\": \"us-west-2\" }\n```", - "input_schema": { - "json": { - "properties": { - "region": { - "description": "AWS region (defaults to us-east-1). Common regions include us-west-2, eu-west-1, etc.", - "type": "string" - }, - "state": { - "type": "string", - "description": "Optional state value to filter by. Representing the current state of the work item. Often follows pattern '[StateName]::[Status]::[Additional Context]'" - }, - "context": { - "type": "string", - "description": "Optional context value to filter by. Representing the environment context the work item was opened in (e.g., 'live', 'beta') or other information (e.g., 'largeorder')" - }, - "problem": { - "type": "string", - "description": "Optional problem value to filter by. Representing classification result for errored work items (e.g., 'UnknownProblem')" - }, - "status": { - "description": "Optional status to filter runs by (defaults to Failed)", - "type": "string", - "enum": [ - "Normal", - "Failed" - ] - }, - "openedIn": { - "type": "string", - "description": "Optional time range in days (defaults to 7)" - }, - "client": { - "description": "The Orca client ID to query", - "type": "string" - }, - "workflow": { - "type": "string", - "description": "Optional workflow name to query (defaults to '')" - } - }, - "type": "object", - "additionalProperties": false, - "required": [ - "client" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "eureka_web_search", - "description": "Web Search using Amazon's internal web-scale search engine - Eureka\n\nGiven a query, this tool will search across the web and return relevant search results.\nThe tool returns top documents with content, url, title, and document_published_at_timestamp.\n\nExample:\n { \"query\": \"recent supreme court ruling\" }", - "input_schema": { - "json": { - "properties": { - "query": { - "type": "string", - "description": "Search query" - } - }, - "additionalProperties": false, - "required": [ - "query" - ], - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_account_lookup", - "description": "This tool is for looking up accounts on the AWS Salesforce AKA AWSentral", - "input_schema": { - "json": { - "properties": { - "account_id": { - "description": "the id of the account", - "type": "string" - }, - "account_name": { - "description": "the name of the account", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "oncall_compass_get_report", - "description": "Get the content of the report along with additional metadata.", - "input_schema": { - "json": { - "type": "object", - "required": [ - "reportId" - ], - "additionalProperties": false, - "properties": { - "reportId": { - "description": "ID of the report to retrieve", - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "overleaf_clone_project", - "description": "Clone an Overleaf project to the local workspace.\n\nThis tool clones the specified Overleaf project to the local workspace directory.\nThe project will be stored in ./overleaf/{project_id}.\nIf the project is already cloned locally, this operation is idempotent and will skip cloning.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\"\n}\n```", - "input_schema": { - "json": { - "type": "object", - "properties": { - "project_id": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$", - "description": "Project ID to clone" - } - }, - "additionalProperties": false, - "required": [ - "project_id" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "jira_get_attachment", - "description": "Download an attachment from a JIRA issue", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "outputPath": { - "description": "Path where to save the downloaded attachment", - "type": "string" - }, - "attachmentUrl": { - "minLength": 1, - "description": "The URL of the attachment to download", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "attachmentUrl" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "sfdc_list_tasks_activity", - "description": "This tool is for listing SA Activities and tasks in AWS Salesforce (AFA AWSentral)", - "input_schema": { - "json": { - "properties": { - "account_id": { - "type": "string", - "description": "The Salesforce Account ID to filter by - this will return all activities/tasks on an account and it's opportunities" - }, - "opportunity_id": { - "type": "string", - "description": "The Salesforce Opportunity ID to filter by - this will return all activities/tasks on a opportunity" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "pippin_update_project", - "description": "Updates an existing Pippin design project's details", - "input_schema": { - "json": { - "type": "object", - "required": [ - "projectId" - ], - "properties": { - "description": { - "type": "string", - "description": "Updated project description" - }, - "requirements": { - "type": "string", - "description": "Updated project requirements" - }, - "projectId": { - "description": "Project ID", - "type": "string" - }, - "status": { - "type": "string", - "description": "Updated project status" - }, - "name": { - "description": "Updated project name", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "add_tag_work_contribution", - "description": "Add a tag to a work contribution in AtoZ.\n\nThis tool allows you to add a tag (such as a leadership principle tag) to an existing work contribution.\nTo get a list of available leadership principles, use the list_leadership_principles tool.\n\nLimitations:\nonly up to three leadership principles can be tagged\n\nRequired parameters include:\n- workContributionId: The ID of the work contribution\n- tagKey: The key of the tag to add (e.g., 'uri_1', 'uri_2')\n- tagType: The type of tag (e.g., 'LEADERSHIP_PRINCIPLE')\n- ownerLogin or ownerPersonId: The owner of the work contribution", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "workContributionId", - "tagKey", - "tagType" - ], - "properties": { - "workContributionId": { - "description": "ID of the work contribution", - "type": "string" - }, - "tagType": { - "type": "string", - "enum": [ - "LEADERSHIP_PRINCIPLE", - "ROLE_GUIDELINE" - ], - "description": "Type of tag to add" - }, - "ownerLogin": { - "description": "Login/alias of the work contribution owner", - "type": "string" - }, - "ownerPersonId": { - "description": "Person ID of the work contribution owner", - "type": "string" - }, - "tagKey": { - "description": "Uri Key of the tag to add (e.g., 'uri_1', 'uri_2')", - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "acs_change_cp_records", - "description": "Modify records (also called config values) for a given contextual parameter (also called config key, or CP) in Amazon Config Store.\nAllows adding, deprecating, or modifying records with proper change tracking.\nDeprecating a contextual parameter value will avoid any new usage of this value. However, existing feature records using this value will remain unaffected.\nIf any of the required parameters are not provided, you MUST ASK the user for them.\nYou can optionally specify the stage (PROD, DEVO, SANDBOX also called BETA) to query.", - "input_schema": { - "json": { - "type": "object", - "properties": { - "ticketLink": { - "type": "string", - "description": "Optional link to a ticket related to this change" - }, - "name": { - "description": "Contextual parameter name to modify records for", - "type": "string" - }, - "recordChanges": { - "minItems": 1, - "description": "Record changes to apply", - "type": "array", - "items": { - "properties": { - "operationType": { - "description": "Operation type for the record change. Either Upsert or Deprecate.", - "enum": [ - "Upsert", - "Deprecate" - ], - "type": "string" - }, - "value": { - "minLength": 1, - "description": "Value for the record to be added or deprecated", - "type": "string" - }, - "parentKeyValueMap": { - "additionalProperties": { - "minLength": 1, - "type": "string" - }, - "type": "object", - "description": "Map from parent contextual parameter keys to their values. Required for composite contextual parameters.", - "propertyNames": { - "minLength": 1 - } - }, - "description": { - "type": "string", - "description": "Description of the changes being made" - }, - "parentValue": { - "description": "Parent value of the contextual parameter value", - "type": "string" - } - }, - "type": "object", - "required": [ - "operationType", - "value" - ], - "additionalProperties": false - } - }, - "crId": { - "type": "string", - "description": "Optional CR id to raise a new revision rather than making a new CR" - }, - "stage": { - "description": "Stage to query", - "type": "string", - "enum": [ - "PROD", - "DEVO", - "SANDBOX" - ] - }, - "changeSummary": { - "type": "string", - "description": "Summary of the changes being made" - } - }, - "required": [ - "name", - "recordChanges", - "changeSummary", - "stage" - ], - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false - } - } - } - }, - { - "ToolSpecification": { - "name": "genai_poweruser_search_knowledge", - "description": "Perform advanced text-based searches across your knowledge repository to find documents matching specific queries. This tool searches document content and returns contextual matches with relevance scores, supporting search result limiting and folder-specific scoping. Ideal for discovering relevant information across large knowledge bases.", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "type": "object", - "required": [ - "query" - ], - "properties": { - "folder": { - "description": "Limit search to a specific folder", - "type": "string" - }, - "query": { - "description": "The search query", - "type": "string" - }, - "limit": { - "type": "number", - "description": "Maximum number of results to return" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "policy_engine_get_user_dashboard", - "description": "Access Amazon Policy Engine dashboard information for a specific user alias. This tool allows you to view all risks and violations for a user in Policy Engine.", - "input_schema": { - "json": { - "properties": { - "username": { - "description": "Username to view dashboard for (e.g., 'jingzhoh')", - "type": "string" - } - }, - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "username" - ], - "type": "object" - } - } - } - }, - { - "ToolSpecification": { - "name": "marshal_get_insight", - "description": "Retrieve Marshal Insights.\nMarshal is an internal AWS application for collecting insights from Solutions Architects (SAs), and other field teams, and facilitating the reporting process for Weekly/Monthly/Quarterly Business Reports (WBR/MBR/QBR).\n", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "insightId": { - "type": "string", - "pattern": "^\\d+$", - "description": "The ID of the Marshal insight (numeric ID only, not the full URL)" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "insightId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "read_kingpin_goal", - "description": "Read a Kingpin goal by ID, retrieving comprehensive details including metadata, description, status comments, and path to green information. Now supports goal history tracking with the includeHistory parameter, showing how status comments and path to green have changed over time. Path to Green represents specific actions needed to get at-risk goals back on track. Use maxVersions parameter to control the amount of history data returned. Kingpin is Amazon's internal source of truth for planning and commitments.", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "includeHistory": { - "default": false, - "type": "boolean", - "description": "Whether to include the goal's history in the response, showing changes to statusComments and pathToGreen fields over time (default: false)" - }, - "goalId": { - "description": "The ID of the Kingpin goal to read (numeric ID only, not the full URL)", - "type": "string" - }, - "maxVersions": { - "type": "number", - "default": 10, - "description": "Maximum number of versions to include in the history, used to limit returned information size for goals with extensive history (default: 10)" - } - }, - "required": [ - "goalId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "imr_costs_get_fleet_summary", - "description": "Presents the internal costs (IMR) for a fleet or AWS account. Retrieves the information from the tool Cerberus and monthly statements api.", - "input_schema": { - "json": { - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "resourceId": { - "type": "string", - "description": "Resource identifier, fleetId or aws account" - }, - "rateCard": { - "description": "Rate card identifier (e.g. 2025)", - "default": "yearly", - "type": "string" - }, - "scenario": { - "default": "Default CPT++", - "type": "string", - "description": "Scenario name" - }, - "fleetType": { - "description": "Container type, either CONTAINER or AWS_ACCOUNT", - "enum": [ - "CONTAINER", - "AWS_ACCOUNT" - ], - "default": "CONTAINER", - "type": "string" - }, - "month": { - "description": "Month in YYYY-MM-01 format", - "type": "string", - "default": "2025-09-01" - }, - "period": { - "enum": [ - "MONTH", - "YEAR_TO_DATE", - "FULL_YEAR" - ], - "description": "Time period for the summary", - "type": "string", - "default": "YEAR_TO_DATE" - } - }, - "required": [ - "resourceId" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "search_MCMs", - "description": "Search and filter Change Management (CM) records by various criteria:\n • Personnel: requesters, technicians, approvers, resolver groups\n • Status: CM status and closure codes\n • Time-based: creation, updates, scheduling, and execution dates\n • Results: configurable result limits", - "input_schema": { - "json": { - "properties": { - "scheduledStart": { - "properties": { - "lessThan": { - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have scheduled end time less than the given value" - }, - "lessThanOrEqualTo": { - "additionalProperties": false, - "description": "provide this predicate to find Cms that have scheduled end time less than or equal to the given value", - "type": "object", - "required": [ - "value" - ], - "properties": { - "value": { - "type": "number" - } - } - }, - "greaterThan": { - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "description": "provide this predicate to find Cms that have scheduled end time greater than the given value", - "required": [ - "value" - ], - "additionalProperties": false - }, - "between": { - "additionalProperties": false, - "description": "provide this predicate to find Cms that have scheduled end time between the two values", - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start", - "end" - ] - }, - "greaterThanOrEqualTo": { - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - }, - "type": "object", - "description": "provide this predicate to find Cms that have scheduled end time greater than or equal to the given value", - "required": [ - "value" - ] - } - }, - "additionalProperties": false, - "type": "object", - "description": "the scheduled start of the cm" - }, - "actualStart": { - "type": "object", - "description": "the actual start of the cm", - "properties": { - "lessThan": { - "description": "provide this predicate to find Cms that have actual start time less than the given value", - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "additionalProperties": false - }, - "greaterThan": { - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "type": "object", - "description": "provide this predicate to find Cms that have actual start time greater than the given value" - }, - "between": { - "description": "provide this predicate to find Cms that have actual start time between the two values", - "additionalProperties": false, - "required": [ - "start", - "end" - ], - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - } - }, - "lessThanOrEqualTo": { - "additionalProperties": false, - "required": [ - "value" - ], - "description": "provide this predicate to find Cms that have actual start time less than or equal to the given value", - "properties": { - "value": { - "type": "number" - } - }, - "type": "object" - }, - "greaterThanOrEqualTo": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - }, - "description": "provide this predicate to find Cms that have actual start time greater than or equal to the given value", - "required": [ - "value" - ] - } - }, - "additionalProperties": false - }, - "cmStatus": { - "description": "the status of the Cm", - "type": "array", - "items": { - "enum": [ - "Draft", - "PendingApproval", - "Scheduled", - "Modified", - "Rejected", - "Cancelled", - "Completed", - "Paused", - "Aborted", - "Discarded", - "Rework Required", - "Scheduled with Comments", - "In Progress", - "Pending Reapproval", - "Modified after Execution", - "Pending Reapproval after Execution", - "Preflight" - ], - "type": "string" - } - }, - "updatedAt": { - "properties": { - "between": { - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have updated at time between the two values", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start", - "end" - ] - }, - "greaterThanOrEqualTo": { - "description": "provide this predicate to find Cms that have updated at time greater than or equal to the given value", - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "additionalProperties": false, - "required": [ - "value" - ] - }, - "lessThan": { - "description": "provide this predicate to find Cms that have updated at time less than the given value", - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "type": "object", - "additionalProperties": false - }, - "greaterThan": { - "additionalProperties": false, - "description": "provide this predicate to find Cms that have updated at time greater than the given value", - "required": [ - "value" - ], - "type": "object", - "properties": { - "value": { - "type": "number" - } - } - }, - "lessThanOrEqualTo": { - "type": "object", - "description": "provide this predicate to find Cms that have updated at time less than or equal to the given value", - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "additionalProperties": false - } - }, - "type": "object", - "additionalProperties": false, - "description": "the time the Cm was updated" - }, - "actualEnd": { - "type": "object", - "properties": { - "greaterThan": { - "properties": { - "value": { - "type": "number" - } - }, - "description": "provide this predicate to find Cms that have actual end time greater than the given value", - "required": [ - "value" - ], - "additionalProperties": false, - "type": "object" - }, - "greaterThanOrEqualTo": { - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ], - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have actual end time greater than or equal to the given value" - }, - "between": { - "required": [ - "start", - "end" - ], - "additionalProperties": false, - "description": "provide this predicate to find Cms that have actual end time between the two values", - "type": "object", - "properties": { - "end": { - "type": "number" - }, - "start": { - "type": "number" - } - } - }, - "lessThan": { - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "additionalProperties": false, - "required": [ - "value" - ], - "description": "provide this predicate to find Cms that have actual end time less than the given value" - }, - "lessThanOrEqualTo": { - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "additionalProperties": false, - "required": [ - "value" - ], - "description": "provide this predicate to find Cms that have actual end time less than or equal to the given value" - } - }, - "description": "the actual end of the cm", - "additionalProperties": false - }, - "createdAt": { - "description": "the time the Cm was created", - "type": "object", - "additionalProperties": false, - "properties": { - "lessThan": { - "required": [ - "value" - ], - "additionalProperties": false, - "description": "provide this predicate to find Cms that have created at time less than the given value", - "type": "object", - "properties": { - "value": { - "type": "number" - } - } - }, - "lessThanOrEqualTo": { - "type": "object", - "required": [ - "value" - ], - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - }, - "description": "provide this predicate to find Cms that have created at time less than or equal to the given value" - }, - "between": { - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have created at time between the two values", - "required": [ - "start", - "end" - ] - }, - "greaterThanOrEqualTo": { - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have created at time greater than or equal to the given value", - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ] - }, - "greaterThan": { - "required": [ - "value" - ], - "description": "provide this predicate to find Cms that have created at time greater than the given value", - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - } - } - } - }, - "approvers": { - "additionalProperties": false, - "type": "object", - "description": "Filter CMs by approver criteria - use matchAny to find CMs with any of the specified approvers, or matchAll to find CMs with all specified approvers", - "properties": { - "matchAny": { - "required": [ - "values" - ], - "additionalProperties": false, - "type": "object", - "properties": { - "values": { - "type": "array", - "items": { - "additionalProperties": false, - "properties": { - "level": { - "type": "string" - }, - "status": { - "type": "string" - }, - "assignedApproverLogin": { - "type": "string" - } - }, - "type": "object", - "required": [ - "assignedApproverLogin" - ] - } - } - } - }, - "matchAll": { - "required": [ - "values" - ], - "additionalProperties": false, - "type": "object", - "properties": { - "values": { - "type": "array", - "items": { - "required": [ - "assignedApproverLogin" - ], - "properties": { - "status": { - "type": "string" - }, - "assignedApproverLogin": { - "type": "string" - }, - "level": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } - } - } - } - }, - "closureCode": { - "type": "array", - "items": { - "enum": [ - "Successful", - "Successful - Off Script", - "Unsuccessful" - ], - "type": "string" - }, - "description": "the closure code of the CMs" - }, - "numResults": { - "default": 100, - "type": "number", - "description": "Number of results to return" - }, - "requesters": { - "type": "array", - "items": { - "type": "string", - "description": "List of requesters of the CMs" - } - }, - "scheduledEnd": { - "additionalProperties": false, - "type": "object", - "properties": { - "greaterThanOrEqualTo": { - "properties": { - "value": { - "type": "number" - } - }, - "type": "object", - "additionalProperties": false, - "description": "provide this predicate to find Cms that have scheduled end time greater than or equal to the given value", - "required": [ - "value" - ] - }, - "between": { - "additionalProperties": false, - "description": "provide this predicate to find Cms that have scheduled end time between the two values", - "properties": { - "end": { - "type": "number" - }, - "start": { - "type": "number" - } - }, - "type": "object", - "required": [ - "start", - "end" - ] - }, - "greaterThan": { - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have scheduled end time greater than the given value", - "properties": { - "value": { - "type": "number" - } - }, - "required": [ - "value" - ] - }, - "lessThan": { - "type": "object", - "properties": { - "value": { - "type": "number" - } - }, - "description": "provide this predicate to find Cms that have scheduled end time less than the given value", - "additionalProperties": false, - "required": [ - "value" - ] - }, - "lessThanOrEqualTo": { - "required": [ - "value" - ], - "additionalProperties": false, - "type": "object", - "description": "provide this predicate to find Cms that have scheduled end time less than or equal to the given value", - "properties": { - "value": { - "type": "number" - } - } - } - }, - "description": "the scheduled end of the cm" - }, - "technician": { - "type": "array", - "items": { - "type": "string", - "description": "List of technicians of the CMs" - } - }, - "cmOwnerCtiResolverGroup": { - "items": { - "type": "string", - "description": "List of Resolver groups for the CMs" - }, - "type": "array" - } - }, - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - } - }, - { - "ToolSpecification": { - "name": "sage_post_answer", - "description": "Post an answer to an existing question on Sage (Amazon's internal Q&A platform).\n\nThis tool allows you to contribute answers to questions on Sage through the MCP interface.\nThe answer content supports Markdown formatting for rich text, code blocks, and links.\n\nAuthentication:\n- Requires valid Midway authentication (run `mwinit` if you encounter authentication errors)\n\nCommon use cases:\n- Answering technical questions about Amazon internal tools and services\n- Providing code examples or troubleshooting steps\n- Sharing knowledge about internal processes\n\nExample usage:\n{ \"questionId\": 1234567, \"contents\": \"To solve this issue, you need to run:\\n\\n```bash\\nbrazil workspace merge\\n```\\n\\nThis will resolve the dependency conflicts.\" }", - "input_schema": { - "json": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "required": [ - "questionId", - "contents" - ], - "properties": { - "questionId": { - "type": "number", - "description": "ID of the question to answer" - }, - "contents": { - "description": "Content of the answer in Markdown format", - "type": "string" - } - } - } - } - } - }, - { - "ToolSpecification": { - "name": "search_sable", - "description": "Search for Sable scope recode with region id, scope, key or key prefix. This tool allows you to search Sable record by key or key prefix.", - "input_schema": { - "json": { - "properties": { - "scope": { - "type": "string", - "description": "Sable scope name" - }, - "keyPrefix": { - "description": "Sable record key or key prefix", - "type": "string" - }, - "region": { - "type": "string", - "description": "Sable region that are going to query data, e.g.: NA, EU, FE, Integ" - } - }, - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "required": [ - "region", - "scope", - "keyPrefix" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "search_katal_components", - "description": "Search for Katal components\n\nThis tool allows you to search for Katal components using keywords.\nThe search looks through component names and tag names.\n\nExamples:\n1. Search for button components:\n```json\n{\n \"query\": \"button\"\n}\n```", - "input_schema": { - "json": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "query": { - "description": "Search query to find matching Katal components", - "type": "string" - } - }, - "required": [ - "query" - ] - } - } - } - }, - { - "ToolSpecification": { - "name": "overleaf_read_file", - "description": "Read a file from an Overleaf project with automatic synchronization.\n\nThis tool reads the specified file from an Overleaf project. Before reading,\nit ensures the project is cloned locally and synchronized with the remote repository.\nSupports both text and binary files with proper encoding detection.\n\nExample usage:\n```json\n{\n \"project_id\": \"507f1f77bcf86cd799439011\",\n \"file_path\": \"main.tex\"\n}\n```", - "input_schema": { - "json": { - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "project_id": { - "type": "string", - "description": "Project ID containing the file" - }, - "file_path": { - "description": "Path to the file within the project", - "type": "string" - } - }, - "type": "object", - "required": [ - "project_id", - "file_path" - ] - } - } - } - } - ] - }, - "context_manager": { - "max_context_files_size": 150000, - "current_profile": "q_cli_default", - "paths": [ - "AmazonQ.md", - "README.md", - ".amazonq/rules/**/*.md" - ], - "hooks": {} - }, - "context_message_length": 1753, - "latest_summary": null, - "model_info": { - "model_name": "claude-sonnet-4", - "model_id": "claude-sonnet-4", - "context_window_tokens": 200000 - }, - "file_line_tracker": { - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientRequest.cs": { - "prev_fswrite_lines": 19, - "before_fswrite_lines": 0, - "after_fswrite_lines": 19, - "lines_added_by_agent": 19, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/appsettings.json": { - "prev_fswrite_lines": 10, - "before_fswrite_lines": 9, - "after_fswrite_lines": 10, - "lines_added_by_agent": 10, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ClientResponse.cs": { - "prev_fswrite_lines": 6, - "before_fswrite_lines": 0, - "after_fswrite_lines": 6, - "lines_added_by_agent": 6, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Models/ErrorModels.cs": { - "prev_fswrite_lines": 13, - "before_fswrite_lines": 0, - "after_fswrite_lines": 13, - "lines_added_by_agent": 13, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/.gitignore": { - "prev_fswrite_lines": 44, - "before_fswrite_lines": 0, - "after_fswrite_lines": 44, - "lines_added_by_agent": 44, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Services/ClientCacheService.cs": { - "prev_fswrite_lines": 29, - "before_fswrite_lines": 0, - "after_fswrite_lines": 29, - "lines_added_by_agent": 29, - "lines_removed_by_agent": 0, - "is_first_write": false - }, - "/Users/rishavkj/Documents/Storage/Team-Repos/amazon-s3-encryption-client-python/test-server/net-v3-server/src/NetV3Server/Program.cs": { - "prev_fswrite_lines": 14, - "before_fswrite_lines": 42, - "after_fswrite_lines": 14, - "lines_added_by_agent": 14, - "lines_removed_by_agent": 0, - "is_first_write": false - } - }, - "mcp_enabled": true -} \ No newline at end of file From e883161463d30e07a5e0d8a50d26c8c08e0231bc Mon Sep 17 00:00:00 2001 From: Ryan Emery Date: Wed, 17 Sep 2025 16:18:27 -0700 Subject: [PATCH 021/201] always run the tests --- test-server/java-tests/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index f35a2ac6..813e8369 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -46,6 +46,8 @@ tasks { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs classpath = sourceSets["it"].runtimeClasspath + outputs.upToDateWhen { false } + outputs.cacheIf { false } } } From d9924f07c9131bf4adaeb021f73c30e627c16bb0 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 16:20:13 -0700 Subject: [PATCH 022/201] logs --- .../Controllers/ClientController.cs | 16 +++++++++++++--- .../Controllers/ObjectController.cs | 10 ++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index d6dd0637..8264701f 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -9,7 +9,7 @@ namespace NetV3Server.Controllers; [ApiController] [Route("[controller]")] -public class ClientController(IClientCacheService clientCacheService) : ControllerBase +public class ClientController(IClientCacheService clientCacheService, ILogger logger) : ControllerBase { [HttpPost] public IActionResult CreateClient([FromBody] ClientRequest request) @@ -17,18 +17,28 @@ public IActionResult CreateClient([FromBody] ClientRequest request) try { var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; - var enableLegacyMode = request.Config.EnableLegacyMode; + var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; + var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; var encryptionContext = request.Config.EncryptionContext; var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); + logger.LogInformation( + "Created EncryptionMaterialsV2: KMS={KmsKeyId}, Encryption Context={EncryptionContext}", + kmsKeyId, encryptionContext); // SecurityProfile V2AndLegacy can decrypt from legacy S3EC while V2 cannot + var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; + + logger.LogInformation("Created securityProfile= {securityProfile}", securityProfile.ToString()); + var configuration = new AmazonS3CryptoConfigurationV2(securityProfile); // 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("Created S3EC client with ID: {clientId}", clientId); + return new ContentResult { Content = JsonSerializer.Serialize(response), diff --git a/test-server/net-v2-v3-server/Controllers/ObjectController.cs b/test-server/net-v2-v3-server/Controllers/ObjectController.cs index a648a2ef..ae4926c4 100644 --- a/test-server/net-v2-v3-server/Controllers/ObjectController.cs +++ b/test-server/net-v2-v3-server/Controllers/ObjectController.cs @@ -8,11 +8,12 @@ namespace NetV3Server.Controllers; [ApiController] [Route("[controller]")] -public class ObjectController(IClientCacheService clientCacheService) : ControllerBase +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 = "ClientID header is required" }); @@ -39,7 +40,10 @@ public async Task PutObject(string bucket, string key) await client.PutObjectAsync(putRequest); var response = new { bucket, key }; - + + logger.LogInformation( + "Put object succeeded for bucket={bucket}, key={key} and clientId = {clientId}", + bucket, key, clientId); return new ContentResult { Content = JsonSerializer.Serialize(response), @@ -56,6 +60,7 @@ public async Task PutObject(string bucket, string key) [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 = "ClientID header is required" }); @@ -72,6 +77,7 @@ public async Task GetObject(string bucket, string key) Key = key }; var response = await client.GetObjectAsync(getRequest); + logger.LogInformation("Got object from S3 for bucket={bucket}, key={key}", bucket, key); // Read response body using var memoryStream = new MemoryStream(); await response.ResponseStream.CopyToAsync(memoryStream); From 231286503fef7352296faa077a140c310513c248 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Wed, 17 Sep 2025 16:20:25 -0700 Subject: [PATCH 023/201] auto commit --- test-server/net-v2-v3-server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/README.md b/test-server/net-v2-v3-server/README.md index e14583ad..9463d8b5 100644 --- a/test-server/net-v2-v3-server/README.md +++ b/test-server/net-v2-v3-server/README.md @@ -49,7 +49,7 @@ All object operations require a `clientId` header to specify which client to use 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":false,"enableDelayedAuthenticationMode":false,"enableLegacyWrappingAlgorithms":false,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}, "encryptionContext": {"abc": "b"}}}' \ + -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"}}}' \ http://localhost:8083/client ``` From 167cdb79407d794f00b8ed063ad4dc18e9bb8e6d Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Thu, 18 Sep 2025 11:01:39 -0400 Subject: [PATCH 024/201] implement test server for c++ V2 --- test-server/cpp-v2-server/CMakeLists.txt | 29 +++ test-server/cpp-v2-server/README.md | 37 +++ test-server/cpp-v2-server/main.cpp | 283 +++++++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 test-server/cpp-v2-server/CMakeLists.txt create mode 100644 test-server/cpp-v2-server/README.md create mode 100644 test-server/cpp-v2-server/main.cpp diff --git a/test-server/cpp-v2-server/CMakeLists.txt b/test-server/cpp-v2-server/CMakeLists.txt new file mode 100644 index 00000000..16a5fd72 --- /dev/null +++ b/test-server/cpp-v2-server/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) +project(s3ec-cpp-server) + +set(CMAKE_CXX_STANDARD 17) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) + +find_package(AWSSDK REQUIRED COMPONENTS s3 s3-encryption kms) +find_package(nlohmann_json REQUIRED) + +add_executable(s3ec-server main.cpp) + +target_include_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_INCLUDE_DIRS} + /opt/homebrew/include +) + +target_link_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_LIBRARY_DIRS} + /opt/homebrew/lib +) + +target_link_libraries(s3ec-server + ${LIBMICROHTTPD_LIBRARIES} + ${AWSSDK_LINK_LIBRARIES} + nlohmann_json::nlohmann_json + uuid +) \ No newline at end of file diff --git a/test-server/cpp-v2-server/README.md b/test-server/cpp-v2-server/README.md new file mode 100644 index 00000000..06d55be1 --- /dev/null +++ b/test-server/cpp-v2-server/README.md @@ -0,0 +1,37 @@ +# C++ S3 Encryption Test Server + +Minimal C++ implementation of the S3 Encryption test server. + +## Dependencies + +- libmicrohttpd +- AWS SDK for C++ +- nlohmann/json +- uuid + +On MacOS you can +```bash +brew install libmicrohttpd nlohmann-json ossp-uuid +``` + +## Build + +```bash +mkdir build && cd build +cmake .. +make +``` + +## Run + +```bash +./s3ec-server +``` + +Server runs on localhost:8081 + +## API Endpoints + +- `POST /client` - Create S3 encryption client +- `GET /object/{bucket}/{key}` - Get encrypted object +- `PUT /object/{bucket}/{key}` - Put encrypted object \ No newline at end of file diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp new file mode 100644 index 00000000..dc53f38f --- /dev/null +++ b/test-server/cpp-v2-server/main.cpp @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#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; + +std::string generate_uuid() { + uuid_t uuid; + uuid_generate(uuid); + char uuid_str[37]; + uuid_unparse(uuid, uuid_str); + return std::string(uuid_str); +} + +MHD_Result print_key(void *cls, enum MHD_ValueKind kind, const char *key, + const char *value) { + fprintf(stderr, "%s: %s\n", key, value); + return MHD_YES; +} + +std::string get_header_value(struct MHD_Connection *connection, + const char *key) { + const char *value = + MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); + return value ? std::string(value) : ""; +} + +MHD_Result send_response(struct MHD_Connection *connection, int status_code, + const std::string &content) { + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.c_str(), MHD_RESPMEM_MUST_COPY); + MHD_Result ret = MHD_queue_response(connection, status_code, response); + if (ret != MHD_YES) + fprintf(stderr, "MHD_queue_response returned %d\n", ret); + MHD_destroy_response(response); + return ret; +} + +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"]; + + Aws::KMS::KMSClient kms_client; + auto materials = + std::make_shared(kms_key_id); + CryptoConfigurationV2 config(materials); + config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + + auto encryption_client = std::make_shared(config); + + std::string client_id = generate_uuid(); + client_cache[client_id] = encryption_client; + + json response = {{"clientId", client_id}}; + return send_response(connection, 200, response.dump()); + } catch (const std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return send_response(connection, 500, + "{\"error\":\"" + std::string(e.what()) + "\"}"); + } catch (...) { + fprintf(stderr, "Super secret error"); + return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); + } +} + +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()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + fprintf(stderr, "handle_get_object <%s>\n", metadata.c_str()); + try { + Aws::S3::Model::GetObjectRequest request; + request.SetBucket(bucket); + request.SetKey(key); + + auto outcome = it->second->GetObject(request); + if (outcome.IsSuccess()) { + auto &stream = outcome.GetResult().GetBody(); + std::string content((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + return send_response(connection, 200, content); + } else { + fprintf(stderr, "GetObject Failed : %s\n", + outcome.GetError().GetMessage().c_str()); + return send_response(connection, 500, "{\"error\":\"GetObject failed\"}"); + } + } catch (const std::exception &e) { + return send_response(connection, 500, + "{\"error\":\"" + std::string(e.what()) + "\"}"); + } +} + +void fill_context(Aws::Map &map, + const std::string &metadata) { + if (metadata.empty()) { + return; + } + + // Parse metadata format: [key1]:[value1],[key2]:[value2],... + // or single pair: [key]:[value] + std::string current = metadata; + size_t pos = 0; + + while (pos < current.length()) { + // Find opening bracket for key + size_t key_start = current.find('[', pos); + if (key_start == std::string::npos) + break; + + // Find closing bracket for key + size_t key_end = current.find(']', key_start); + if (key_end == std::string::npos) + break; + + // Find colon separator + size_t colon = current.find(':', key_end); + if (colon == std::string::npos) + break; + + // Find opening bracket for value + size_t value_start = current.find('[', colon); + if (value_start == std::string::npos) + break; + + // Find closing bracket for value + size_t value_end = current.find(']', value_start); + if (value_end == std::string::npos) + break; + + // Extract key and value + std::string key = current.substr(key_start + 1, key_end - key_start - 1); + std::string value = + current.substr(value_start + 1, value_end - value_start - 1); + + // Add to map + map.emplace(key, value); + + // Move to next pair (look for comma or next opening bracket) + pos = value_end + 1; + size_t comma = current.find(',', pos); + if (comma != std::string::npos) { + pos = comma + 1; + } + } +} + +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) { + auto it = client_cache.find(client_id); + if (it == client_cache.end()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + fprintf(stderr, "handle_put_object <%s>\n", metadata.c_str()); + try { + Aws::Map kmsContextMap; + // Parse metadata and populate the context map + fill_context(kmsContextMap, metadata); + + Aws::S3::Model::PutObjectRequest request; + request.SetBucket(bucket); + request.SetKey(key); + + auto stream = std::make_shared(body); + request.SetBody(stream); + + auto outcome = it->second->PutObject(request, kmsContextMap); + if (outcome.IsSuccess()) { + json response = {{"bucket", bucket}, {"key", key}}; + return send_response(connection, 200, response.dump()); + } else { + return send_response(connection, 500, "{\"error\":\"PutObject failed\"}"); + } + } catch (const std::exception &e) { + return send_response(connection, 500, + "{\"error\":\"" + std::string(e.what()) + "\"}"); + } +} + +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; + } + return MHD_YES; + } + if (is_push && *upload_data_size) { + 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); + + if (is_push && url_str == "/client") { + std::string *body = static_cast(*con_cls); + auto foo = handle_create_client(connection, *body); + delete body; + return foo; + } + + // fprintf(stderr, "request_handler <%s> <%s> <%s>\n", url, method, + // upload_data); fprintf(stderr, "keys<<\n"); MHD_get_connection_values + // (connection, MHD_HEADER_KIND, &print_key, NULL); fprintf(stderr, ">>\n"); + + if (url_str.find("/object/") == 0) { + 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"); + if (method_str == "GET") { + return handle_get_object(connection, bucket, key, client_id, metadata); + } else if (method_str == "PUT") { + std::string *body = static_cast(*con_cls); + *upload_data_size = 0; + auto foo = handle_put_object(connection, bucket, key, client_id, *body, + metadata); + delete body; + return foo; + } + } + } + + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); +} + +int main() { + Aws::SDKOptions options; + Aws::InitAPI(options); + + struct MHD_Daemon *daemon = + MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, 8085, NULL, NULL, + &request_handler, NULL, MHD_OPTION_END); + + if (!daemon) { + return 1; + } + + printf("Server running on port 8085\n"); + getchar(); + + MHD_stop_daemon(daemon); + Aws::ShutdownAPI(options); + fprintf(stderr, "Ending server\n"); + return 0; +} From 3000858b045ff47d890afcd0b1b00720a609a698 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Thu, 18 Sep 2025 12:08:06 -0400 Subject: [PATCH 025/201] m --- test-server/Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-server/Makefile b/test-server/Makefile index 4831d68d..bfb92180 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -98,6 +98,12 @@ stop-servers: fi @echo "Servers stopped" +install-cpp-dependencies: + brew install libmicrohttpd nlohmann-json ossp-uuid aws-sdk-cpp + +start-cpp-v2-server: + cd cpp-v2-server && mkdir -p build && cd build && cmake .. && make && ./s3ec-server + # Clean up logs and pid files clean: stop-servers @echo "Cleaning up..." @@ -113,6 +119,8 @@ help: @echo " start-python-v3-server : Start only the Python V3 server" @echo " start-java-v3-server : Start only the Java V3 server" @echo " start-go-v3-server : Start only the Go V3 server" + @echo " start-cpp-v2-server : Start only the C++ V2 server" + @echo " install-cpp-dependencies: use brew to install things necessary for start-cpp-v2-server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" @echo " clean : Stop servers and clean up logs" From 9ad923c7d42e07119c0d05522c083a7f875bf066 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Thu, 18 Sep 2025 12:08:52 -0400 Subject: [PATCH 026/201] m --- test-server/cpp-v2-server/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test-server/cpp-v2-server/CMakeLists.txt b/test-server/cpp-v2-server/CMakeLists.txt index 16a5fd72..d9fd9d1b 100644 --- a/test-server/cpp-v2-server/CMakeLists.txt +++ b/test-server/cpp-v2-server/CMakeLists.txt @@ -3,10 +3,17 @@ project(s3ec-cpp-server) set(CMAKE_CXX_STANDARD 17) +# Configure AWS SDK build options +set(BUILD_ONLY "kms;s3" CACHE STRING "Build only KMS and S3 components") +set(ENABLE_TESTING OFF CACHE BOOL "Disable testing") +set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static libraries") + +# Add AWS SDK as subdirectory +add_subdirectory(/Users/ajewell/head/aws-sdk-cpp aws-sdk-cpp) + find_package(PkgConfig REQUIRED) pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) -find_package(AWSSDK REQUIRED COMPONENTS s3 s3-encryption kms) find_package(nlohmann_json REQUIRED) add_executable(s3ec-server main.cpp) @@ -23,7 +30,10 @@ target_link_directories(s3ec-server PRIVATE target_link_libraries(s3ec-server ${LIBMICROHTTPD_LIBRARIES} - ${AWSSDK_LINK_LIBRARIES} + aws-cpp-sdk-core + aws-cpp-sdk-kms + aws-cpp-sdk-s3 + aws-cpp-sdk-s3-encryption nlohmann_json::nlohmann_json uuid ) \ No newline at end of file From a142eab8dbcc5d06bddb3dab6b0acca82a9336ec Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 09:51:44 -0700 Subject: [PATCH 027/201] logs --- test-server/net-v2-v3-server/Controllers/ClientController.cs | 3 ++- test-server/net-v2-v3-server/Controllers/ObjectController.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index 8264701f..8025b0c1 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -24,7 +24,7 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation( "Created EncryptionMaterialsV2: KMS={KmsKeyId}, Encryption Context={EncryptionContext}", kmsKeyId, encryptionContext); - // SecurityProfile V2AndLegacy can decrypt from legacy S3EC while V2 cannot + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; @@ -48,6 +48,7 @@ public IActionResult CreateClient([FromBody] ClientRequest request) } catch (Exception ex) { + logger.LogError(ex, "Failed to create S3EC client"); return StatusCode(500, new S3EncryptionClientError { Message = $"Failed to create client: {ex.Message}" diff --git a/test-server/net-v2-v3-server/Controllers/ObjectController.cs b/test-server/net-v2-v3-server/Controllers/ObjectController.cs index ae4926c4..6472b641 100644 --- a/test-server/net-v2-v3-server/Controllers/ObjectController.cs +++ b/test-server/net-v2-v3-server/Controllers/ObjectController.cs @@ -53,6 +53,7 @@ public async Task PutObject(string bucket, string key) } catch (Exception ex) { + logger.LogError(ex, "Failed to put object from S3 for bucket={bucket}, key={key}", bucket, key); return StatusCode(500, new S3EncryptionClientError { Message = $"Failed to put object: {ex.Message}" }); } } @@ -95,7 +96,8 @@ public async Task GetObject(string bucket, string key) return File(bodyBytes, "application/octet-stream"); } catch (Exception ex) - { + { + logger.LogError(ex, "Failed to get object from S3 for bucket={bucket}, key={key}", bucket, key); return StatusCode(500, new S3EncryptionClientError { Message = ex.Message }); } } From f82b4ab20c5e270e314d227b6d4cfc4967ef814a Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 09:52:56 -0700 Subject: [PATCH 028/201] legacy --- test-server/net-v2-v3-server/Models/ClientRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index 53b63ecc..2a1457d3 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -8,7 +8,8 @@ public class ClientRequest public class ClientConfig { public Dictionary EncryptionContext { get; set; } = new(); - public bool EnableLegacyMode { get; set; } + public bool EnableLegacyUnauthenticatedModes { get; set; } + public bool EnableLegacyWrappingAlgorithms { get; set; } public KeyMaterial KeyMaterial { get; set; } = new(); } From 04bc3c7d7275063df01502d6336d23db527ddaa0 Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 18 Sep 2025 11:43:19 -0700 Subject: [PATCH 029/201] Break up the makefiles (#9) Break up the makefiles * Update test-server/go-v3-server/Makefile Co-authored-by: Lucas McDonald * Apply suggestions from code review Co-authored-by: Lucas McDonald --------- Co-authored-by: Lucas McDonald --- test-server/Makefile | 122 ++++++++++---------------- test-server/go-v3-server/Makefile | 25 ++++++ test-server/java-v3-server/Makefile | 24 +++++ test-server/python-v3-server/Makefile | 28 ++++++ 4 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 test-server/go-v3-server/Makefile create mode 100644 test-server/java-v3-server/Makefile create mode 100644 test-server/python-v3-server/Makefile diff --git a/test-server/Makefile b/test-server/Makefile index 4831d68d..90d15648 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -8,65 +8,31 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -# Start Python server in background -start-python-v3-server: - @echo "Starting Python V3 server..." - cd python-v3-server && \ - python -m venv .venv && \ - .venv/bin/python -m ensurepip && \ - .venv/bin/python -m pip install -e . && \ - .venv/bin/python -m pip install -e ../.. && \ - 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" \ - .venv/bin/python src/main.py & echo $$! > ../python-v3-server.pid - @echo "Python server starting..." - -# Start Java server in background -start-java-v3-server: - @echo "Starting Java V3 server..." - cd java-v3-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" \ - ./gradlew --build-cache --parallel run & echo $$! > ../java-v3-server.pid - @echo "Java server starting..." - -# Start Go server in background -start-go-v3-server: - @echo "Starting Go V3 server..." - cd go-v3-server && \ - go mod tidy && \ - 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" \ - go run . & echo $$! > ../go-v3-server.pid - @echo "Go server starting..." +SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) # Start all servers in parallel start-servers: - @echo "Starting servers in parallel..." - @$(MAKE) -j3 start-python-v3-server start-java-v3-server start-go-v3-server - @echo "Waiting for servers to be ready..." - @for i in $$(seq 1 360); do \ - if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082; then \ - echo "Ports are open, waiting for servers to initialize..."; \ - sleep 5; \ - echo "All servers are ready!"; \ - break; \ - fi; \ - if [ $$i -eq 360 ]; then \ - echo "Timeout waiting for servers to start"; \ - exit 1; \ - fi; \ - echo "Waiting for servers to start ($$i/360)..."; \ - sleep 1; \ + @echo "Starting all servers..." + $(MAKE) start-all-servers + @echo "Waiting for servers to start..." + @for dir in $(SERVER_DIRS); do \ + echo "Waiting for server in $$dir..."; \ + $(MAKE) -C $$dir wait-for-server; \ done +start-all-servers: $(SERVER_TARGETS) + +$(SERVER_TARGETS): start-%: + @if [ -f $*/Makefile ]; then \ + echo "Starting server in $*..."; \ + $(MAKE) -C $* start-server; \ + else \ + echo "❌ Error: no Makefile found in $$dir"; \ + exit 1; \ + fi; \ + # Run the Java tests run-tests: @@ -84,38 +50,20 @@ run-tests: # Stop the servers stop-servers: @echo "Stopping servers..." - @if [ -f python-v3-server.pid ]; then \ - kill $$(cat python-v3-server.pid) 2>/dev/null || true; \ - rm python-v3-server.pid; \ - fi - @if [ -f java-v3-server.pid ]; then \ - kill $$(cat java-v3-server.pid) 2>/dev/null || true; \ - rm java-v3-server.pid; \ - fi - @if [ -f go-v3-server.pid ]; then \ - kill $$(cat go-v3-server.pid) 2>/dev/null || true; \ - rm go-v3-server.pid; \ - fi + @for dir in $(SERVER_DIRS); do \ + echo "Starting server in $$dir..."; \ + $(MAKE) -C $$dir stop-server; \ + done @echo "Servers stopped" -# Clean up logs and pid files -clean: stop-servers - @echo "Cleaning up..." - @rm -f python-v3-server.log java-v3-server.log go-v3-server.log - @echo "Cleanup complete" - # Help target help: @echo "Available targets:" @echo " all : Start servers and run tests (default, output to stdout)" @echo " ci : Run in CI mode (start servers, run tests, stop servers)" @echo " start-servers : Start all servers in parallel" - @echo " start-python-v3-server : Start only the Python V3 server" - @echo " start-java-v3-server : Start only the Java V3 server" - @echo " start-go-v3-server : Start only the Go V3 server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" - @echo " clean : Stop servers and clean up logs" @echo " check-env : Check if required environment variables are set" @echo " help : Show this help message" @@ -126,3 +74,25 @@ check-env: @if [ -z "$$AWS_SECRET_ACCESS_KEY" ]; then echo "AWS_SECRET_ACCESS_KEY is not set"; else echo "AWS_SECRET_ACCESS_KEY is set"; fi @if [ -z "$$AWS_SESSION_TOKEN" ]; then echo "AWS_SESSION_TOKEN is not set"; else echo "AWS_SESSION_TOKEN is set"; fi @if [ -z "$$AWS_REGION" ]; then echo "AWS_REGION is not set (will use us-west-2 as default)"; else echo "AWS_REGION is set to $$AWS_REGION"; fi + +TIMEOUT := 360 + +wait-for-port: + @if [ -z "$(PORT)" ]; then \ + echo "❌ Error: PORT is required"; \ + exit 1; \ + fi + @for i in $$(seq 1 $(TIMEOUT)); do \ + if nc -z localhost $$PORT; then \ + echo "Ports are open, waiting for servers to initialize..."; \ + sleep 5; \ + echo "Server at $$PORT is ready!"; \ + break; \ + fi; \ + if [ $$i -eq $(TIMEOUT) ]; then \ + echo "Timeout waiting for $$PORT start"; \ + exit 1; \ + fi; \ + echo "Waiting for $$PORT to start ($$i/$(TIMEOUT))..."; \ + sleep 1; \ + done diff --git a/test-server/go-v3-server/Makefile b/test-server/go-v3-server/Makefile new file mode 100644 index 00000000..0ab142de --- /dev/null +++ b/test-server/go-v3-server/Makefile @@ -0,0 +1,25 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8082 + +start-server: + @echo "Starting Go V3 server..." + go mod tidy + 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" \ + go run . & echo $$! > $(PID_FILE) + @echo "Go V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v3-server/Makefile b/test-server/java-v3-server/Makefile new file mode 100644 index 00000000..1e0dc763 --- /dev/null +++ b/test-server/java-v3-server/Makefile @@ -0,0 +1,24 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8080 + +start-server: + @echo "Starting Java V3 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" \ + ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + @echo "Java V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/python-v3-server/Makefile b/test-server/python-v3-server/Makefile new file mode 100644 index 00000000..e6e9d509 --- /dev/null +++ b/test-server/python-v3-server/Makefile @@ -0,0 +1,28 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8081 + +start-server: + @echo "Starting Python V3 server..." + python -m venv .venv + .venv/bin/python -m ensurepip + .venv/bin/python -m pip install -e . + .venv/bin/python -m pip install -e ../.. + 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" \ + .venv/bin/python src/main.py & echo $$! > $(PID_FILE) + @echo "Python V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) From d01506ed09481c6267fe528334a5a1b20359b316 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 11:53:13 -0700 Subject: [PATCH 030/201] makefile --- test-server/Makefile | 55 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index 4831d68d..26629db6 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server start-net-v2-server start-net-v3-server run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -21,7 +21,7 @@ start-python-v3-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - .venv/bin/python src/main.py & echo $$! > ../python-v3-server.pid + .venv/bin/python src/main.py & echo $$! > python-v3-server.pid @echo "Python server starting..." # Start Java server in background @@ -32,7 +32,7 @@ start-java-v3-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - ./gradlew --build-cache --parallel run & echo $$! > ../java-v3-server.pid + ./gradlew --build-cache --parallel run & echo $$! > java-v3-server.pid @echo "Java server starting..." # Start Go server in background @@ -44,16 +44,46 @@ start-go-v3-server: AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ - go run . & echo $$! > ../go-v3-server.pid + go run . & echo $$! > go-v3-server.pid @echo "Go server starting..." +# Start .NET V2 server in background +# This builds first into bin/v2 and runs through dll +# to avoid simultaneous dotnet run conflict +start-net-v2-server: + @echo "Starting .NET V2 server..." + cd net-v2-v3-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" \ + rm -rf obj/v2 bin/v2 && \ + dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ && \ + dotnet bin/v2/NetV2V3Server.dll > ../net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid + @echo ".NET V2 server starting..." + +# Start .NET V3 server in background +# This builds first into bin/v3 and runs through dll +# to avoid simultaneous dotnet run conflict +start-net-v3-server: + @echo "Starting .NET V3 server..." + cd net-v2-v3-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" \ + rm -rf obj/v3 bin/v3 && \ + dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ && \ + dotnet bin/v3/NetV2V3Server.dll > ../net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid + @echo ".NET V3 server starting..." + # Start all servers in parallel start-servers: @echo "Starting servers in parallel..." - @$(MAKE) -j3 start-python-v3-server start-java-v3-server start-go-v3-server + @$(MAKE) -j5 start-python-v3-server start-java-v3-server start-go-v3-server start-net-v2-server start-net-v3-server @echo "Waiting for servers to be ready..." @for i in $$(seq 1 360); do \ - if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082; then \ + if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082 && nc -z localhost 8083 && nc -z localhost 8084; then \ echo "Ports are open, waiting for servers to initialize..."; \ sleep 5; \ echo "All servers are ready!"; \ @@ -96,12 +126,21 @@ stop-servers: kill $$(cat go-v3-server.pid) 2>/dev/null || true; \ rm go-v3-server.pid; \ fi + @if [ -f net-v2-server.pid ]; then \ + kill $$(cat net-v2-server.pid) 2>/dev/null || true; \ + rm net-v2-server.pid; \ + fi + @if [ -f net-v3-server.pid ]; then \ + kill $$(cat net-v3-server.pid) 2>/dev/null || true; \ + rm net-v3-server.pid; \ + fi @echo "Servers stopped" # Clean up logs and pid files clean: stop-servers @echo "Cleaning up..." - @rm -f python-v3-server.log java-v3-server.log go-v3-server.log + @rm -f python-v3-server.log java-v3-server.log go-v3-server.log net-v2-server.log net-v3-server.log + @lsof -ti:8080,8081,8082,8083,8084 | xargs -r kill -9 2>/dev/null || true @echo "Cleanup complete" # Help target @@ -113,6 +152,8 @@ help: @echo " start-python-v3-server : Start only the Python V3 server" @echo " start-java-v3-server : Start only the Java V3 server" @echo " start-go-v3-server : Start only the Go V3 server" + @echo " start-net-v2-server : Start only the .NET V2 server" + @echo " start-net-v3-server : Start only the .NET V3 server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" @echo " clean : Stop servers and clean up logs" From 8b19cdf4cf1aee8c0440ad503b1674325e91d262 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 13:30:43 -0700 Subject: [PATCH 031/201] auto commit --- .../amazon/encryption/s3/RoundTripTests.java | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 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 535e4d8d..e4d109d5 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 @@ -54,6 +54,12 @@ import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; public class RoundTripTests { + private static final String JAVA_V3 = "Java-V3"; + private static final String PYTHON_V3 = "Python-V3"; + private static final String GO_V3 = "Go-V3"; + private static final String NET_V2 = "NET-v2"; + private static final String NET_V3 = "NET-v3"; + private static final List serverList; private static final Map serverMap; @@ -65,22 +71,33 @@ public class RoundTripTests { static { serverList = new ArrayList<>(14); - serverList.add(new LanguageServerTarget("Java-V3", "8080")); - serverList.add(new LanguageServerTarget("Python-V3", "8081")); - serverList.add(new LanguageServerTarget("Go-V3", "8082")); + serverList.add(new LanguageServerTarget(JAVA_V3, "8080")); + serverList.add(new LanguageServerTarget(PYTHON_V3, "8081")); + serverList.add(new LanguageServerTarget(GO_V3, "8082")); + serverList.add(new LanguageServerTarget(NET_V2, "8083")); + serverList.add(new LanguageServerTarget(NET_V3, "8084")); serverMap = new HashMap<>(14); - serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080")); - serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081")); - serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); + serverMap.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); + serverMap.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); + serverMap.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); + serverMap.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); + serverMap.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); } - // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt). + // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt) (Go) + // or validates only from the stored object (.NET). // If the encryption context provided to getObject does not match the encryption context on the stored object, // these implementations will not raise an error as expected. // For now, skip tests that expect encryption context validation on decrypt. private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of("Go-V3"); + Set.of(GO_V3, NET_V2, NET_V3); + + // These S3EC implementations do not have a way to provide encryption context to putObject (i.e. on encrypt) (dotnet). + // So, the way these tests are configured, in these languages encryption context will not be passed on encrypt. + // For now, skip tests that expect encryption context validation on decrypt. + private static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = + Set.of(NET_V2, NET_V3); static public class LanguageServerTarget { public String getLanguageName() { @@ -222,6 +239,9 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("crossLanguageClients") public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, LanguageServerTarget decLang) { + if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + return; + } S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-" + encLang; final String input = "simple-test-input"; @@ -269,6 +289,9 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { return; } + if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + return; + } S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-subset-fails" + encLang; final String input = "simple-test-input"; @@ -494,7 +517,13 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); + if (language.equals(NET_V3) || language.equals(NET_V2)) { + assertTrue(e.getMessage().contains( + "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration V2." + )); + } else { + assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); + } } } From 43514e09a325c432cf3e10019a448a39b3deb62b Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 14:16:07 -0700 Subject: [PATCH 032/201] auto commit --- test-server/Makefile | 163 ++++++++------------------ test-server/net-v2-v3-server/Makefile | 58 +++++++++ 2 files changed, 104 insertions(+), 117 deletions(-) create mode 100644 test-server/net-v2-v3-server/Makefile diff --git a/test-server/Makefile b/test-server/Makefile index 26629db6..8a148d30 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server start-net-v2-server start-net-v3-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -8,95 +8,31 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -# Start Python server in background -start-python-v3-server: - @echo "Starting Python V3 server..." - cd python-v3-server && \ - python -m venv .venv && \ - .venv/bin/python -m ensurepip && \ - .venv/bin/python -m pip install -e . && \ - .venv/bin/python -m pip install -e ../.. && \ - 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" \ - .venv/bin/python src/main.py & echo $$! > python-v3-server.pid - @echo "Python server starting..." - -# Start Java server in background -start-java-v3-server: - @echo "Starting Java V3 server..." - cd java-v3-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" \ - ./gradlew --build-cache --parallel run & echo $$! > java-v3-server.pid - @echo "Java server starting..." - -# Start Go server in background -start-go-v3-server: - @echo "Starting Go V3 server..." - cd go-v3-server && \ - go mod tidy && \ - 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" \ - go run . & echo $$! > go-v3-server.pid - @echo "Go server starting..." - -# Start .NET V2 server in background -# This builds first into bin/v2 and runs through dll -# to avoid simultaneous dotnet run conflict -start-net-v2-server: - @echo "Starting .NET V2 server..." - cd net-v2-v3-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" \ - rm -rf obj/v2 bin/v2 && \ - dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ && \ - dotnet bin/v2/NetV2V3Server.dll > ../net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid - @echo ".NET V2 server starting..." - -# Start .NET V3 server in background -# This builds first into bin/v3 and runs through dll -# to avoid simultaneous dotnet run conflict -start-net-v3-server: - @echo "Starting .NET V3 server..." - cd net-v2-v3-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" \ - rm -rf obj/v3 bin/v3 && \ - dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ && \ - dotnet bin/v3/NetV2V3Server.dll > ../net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid - @echo ".NET V3 server starting..." +SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) # Start all servers in parallel start-servers: - @echo "Starting servers in parallel..." - @$(MAKE) -j5 start-python-v3-server start-java-v3-server start-go-v3-server start-net-v2-server start-net-v3-server - @echo "Waiting for servers to be ready..." - @for i in $$(seq 1 360); do \ - if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082 && nc -z localhost 8083 && nc -z localhost 8084; then \ - echo "Ports are open, waiting for servers to initialize..."; \ - sleep 5; \ - echo "All servers are ready!"; \ - break; \ - fi; \ - if [ $$i -eq 360 ]; then \ - echo "Timeout waiting for servers to start"; \ - exit 1; \ - fi; \ - echo "Waiting for servers to start ($$i/360)..."; \ - sleep 1; \ + @echo "Starting all servers..." + $(MAKE) start-all-servers + @echo "Waiting for servers to start..." + @for dir in $(SERVER_DIRS); do \ + echo "Waiting for server in $$dir..."; \ + $(MAKE) -C $$dir wait-for-server; \ done +start-all-servers: $(SERVER_TARGETS) + +$(SERVER_TARGETS): start-%: + @if [ -f $*/Makefile ]; then \ + echo "Starting server in $*..."; \ + $(MAKE) -C $* start-server; \ + else \ + echo "❌ Error: no Makefile found in $$dir"; \ + exit 1; \ + fi; \ + # Run the Java tests run-tests: @@ -114,49 +50,20 @@ run-tests: # Stop the servers stop-servers: @echo "Stopping servers..." - @if [ -f python-v3-server.pid ]; then \ - kill $$(cat python-v3-server.pid) 2>/dev/null || true; \ - rm python-v3-server.pid; \ - fi - @if [ -f java-v3-server.pid ]; then \ - kill $$(cat java-v3-server.pid) 2>/dev/null || true; \ - rm java-v3-server.pid; \ - fi - @if [ -f go-v3-server.pid ]; then \ - kill $$(cat go-v3-server.pid) 2>/dev/null || true; \ - rm go-v3-server.pid; \ - fi - @if [ -f net-v2-server.pid ]; then \ - kill $$(cat net-v2-server.pid) 2>/dev/null || true; \ - rm net-v2-server.pid; \ - fi - @if [ -f net-v3-server.pid ]; then \ - kill $$(cat net-v3-server.pid) 2>/dev/null || true; \ - rm net-v3-server.pid; \ - fi + @for dir in $(SERVER_DIRS); do \ + echo "Starting server in $$dir..."; \ + $(MAKE) -C $$dir stop-server; \ + done @echo "Servers stopped" -# Clean up logs and pid files -clean: stop-servers - @echo "Cleaning up..." - @rm -f python-v3-server.log java-v3-server.log go-v3-server.log net-v2-server.log net-v3-server.log - @lsof -ti:8080,8081,8082,8083,8084 | xargs -r kill -9 2>/dev/null || true - @echo "Cleanup complete" - # Help target help: @echo "Available targets:" @echo " all : Start servers and run tests (default, output to stdout)" @echo " ci : Run in CI mode (start servers, run tests, stop servers)" @echo " start-servers : Start all servers in parallel" - @echo " start-python-v3-server : Start only the Python V3 server" - @echo " start-java-v3-server : Start only the Java V3 server" - @echo " start-go-v3-server : Start only the Go V3 server" - @echo " start-net-v2-server : Start only the .NET V2 server" - @echo " start-net-v3-server : Start only the .NET V3 server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" - @echo " clean : Stop servers and clean up logs" @echo " check-env : Check if required environment variables are set" @echo " help : Show this help message" @@ -167,3 +74,25 @@ check-env: @if [ -z "$$AWS_SECRET_ACCESS_KEY" ]; then echo "AWS_SECRET_ACCESS_KEY is not set"; else echo "AWS_SECRET_ACCESS_KEY is set"; fi @if [ -z "$$AWS_SESSION_TOKEN" ]; then echo "AWS_SESSION_TOKEN is not set"; else echo "AWS_SESSION_TOKEN is set"; fi @if [ -z "$$AWS_REGION" ]; then echo "AWS_REGION is not set (will use us-west-2 as default)"; else echo "AWS_REGION is set to $$AWS_REGION"; fi + +TIMEOUT := 360 + +wait-for-port: + @if [ -z "$(PORT)" ]; then \ + echo "❌ Error: PORT is required"; \ + exit 1; \ + fi + @for i in $$(seq 1 $(TIMEOUT)); do \ + if nc -z localhost $$PORT; then \ + echo "Ports are open, waiting for servers to initialize..."; \ + sleep 5; \ + echo "Server at $$PORT is ready!"; \ + break; \ + fi; \ + if [ $$i -eq $(TIMEOUT) ]; then \ + echo "Timeout waiting for $$PORT start"; \ + exit 1; \ + fi; \ + echo "Waiting for $$PORT to start ($$i/$(TIMEOUT))..."; \ + sleep 1; \ + done \ No newline at end of file diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile new file mode 100644 index 00000000..dba97e4b --- /dev/null +++ b/test-server/net-v2-v3-server/Makefile @@ -0,0 +1,58 @@ +# Makefile for S3 Encryption Client .NET Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE_NET_V2 := net-v2-server.pid +PID_FILE_NET_V3 := net-v3-server.pid +PORT_NET_V2 := 8083 +PORT_NET_V3 := 8084 + +start-server: + $(MAKE) start-net-v2-server; \ + $(MAKE) start-net-v3-server; + +stop-server: + @if [ -f $(PID_FILE_NET_V2) ]; then \ + kill $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ + rm $(PID_FILE_NET_V2); \ + fi + @if [ -f $(PID_FILE_NET_V3) ]; then \ + kill $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ + rm $(PID_FILE_NET_V3); \ + fi + +# Start .NET V2 server in background +# This builds first into bin/v2 and runs through dll +# to avoid simultaneous dotnet run conflict +start-net-v2-server: + @echo "Starting .NET V2 server..." + cd net-v2-v3-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" \ + rm -rf obj/v2 bin/v2 && \ + dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ && \ + dotnet bin/v2/NetV2V3Server.dll > ../net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid + @echo ".NET V2 server starting..." + + +# Start .NET V3 server in background +# This builds first into bin/v3 and runs through dll +# to avoid simultaneous dotnet run conflict +start-net-v3-server: + @echo "Starting .NET V3 server..." + cd net-v2-v3-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" \ + rm -rf obj/v3 bin/v3 && \ + dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ && \ + dotnet bin/v3/NetV2V3Server.dll > ../net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid + @echo ".NET V3 server starting..." + + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) \ + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) \ No newline at end of file From 86146f5179c565d35cee46c350161929c379d7ee Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 14:39:34 -0700 Subject: [PATCH 033/201] auto commit --- test-server/net-v2-v3-server/Makefile | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile index dba97e4b..f5b18688 100644 --- a/test-server/net-v2-v3-server/Makefile +++ b/test-server/net-v2-v3-server/Makefile @@ -26,14 +26,13 @@ stop-server: # to avoid simultaneous dotnet run conflict start-net-v2-server: @echo "Starting .NET V2 server..." - cd net-v2-v3-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" \ rm -rf obj/v2 bin/v2 && \ dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ && \ - dotnet bin/v2/NetV2V3Server.dll > ../net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid + dotnet bin/v2/NetV2V3Server.dll > net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid @echo ".NET V2 server starting..." @@ -42,17 +41,15 @@ start-net-v2-server: # to avoid simultaneous dotnet run conflict start-net-v3-server: @echo "Starting .NET V3 server..." - cd net-v2-v3-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" \ rm -rf obj/v3 bin/v3 && \ dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ && \ - dotnet bin/v3/NetV2V3Server.dll > ../net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid + dotnet bin/v3/NetV2V3Server.dll > net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid @echo ".NET V3 server starting..." - wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) \ $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) \ No newline at end of file From b9acf931b7a46e9c1f6a0390c7c0359870bc903a Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:18:38 -0700 Subject: [PATCH 034/201] auto commit --- .../Controllers/ClientController.cs | 35 ++++++++++++------- .../Controllers/ObjectController.cs | 14 ++++---- .../net-v2-v3-server/Models/ClientRequest.cs | 2 +- .../net-v2-v3-server/Models/ClientResponse.cs | 2 +- .../net-v2-v3-server/Models/ErrorModels.cs | 2 +- test-server/net-v2-v3-server/Program.cs | 2 +- .../Services/ClientCacheService.cs | 2 +- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index 8025b0c1..570fad61 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -2,10 +2,10 @@ using Amazon.Extensions.S3.Encryption; using Amazon.Extensions.S3.Encryption.Primitives; using Microsoft.AspNetCore.Mvc; -using NetV3Server.Models; -using NetV3Server.Services; +using NetV2V3Server.Models; +using NetV2V3Server.Services; -namespace NetV3Server.Controllers; +namespace NetV2V3Server.Controllers; [ApiController] [Route("[controller]")] @@ -14,31 +14,40 @@ public class ClientController(IClientCacheService clientCacheService, ILogger logger) : ControllerBase +public class ObjectController(IClientCacheService clientCacheService, ILogger logger) : ControllerBase { [HttpPut("{bucket}/{key}")] public async Task PutObject(string bucket, string key) @@ -40,9 +40,9 @@ public async Task PutObject(string bucket, string key) await client.PutObjectAsync(putRequest); var response = new { bucket, key }; - + logger.LogInformation( - "Put object succeeded for bucket={bucket}, key={key} and clientId = {clientId}", + "Put object succeeded for bucket={bucket}, key={key} and clientId = {clientId}", bucket, key, clientId); return new ContentResult { @@ -96,7 +96,7 @@ public async Task GetObject(string bucket, string key) return File(bodyBytes, "application/octet-stream"); } catch (Exception ex) - { + { logger.LogError(ex, "Failed to get object from S3 for bucket={bucket}, key={key}", bucket, key); return StatusCode(500, new S3EncryptionClientError { Message = ex.Message }); } diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index 2a1457d3..710b5e8a 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace NetV3Server.Models; +namespace NetV2V3Server.Models; public class ClientRequest { diff --git a/test-server/net-v2-v3-server/Models/ClientResponse.cs b/test-server/net-v2-v3-server/Models/ClientResponse.cs index ad5e9034..1e029ef7 100644 --- a/test-server/net-v2-v3-server/Models/ClientResponse.cs +++ b/test-server/net-v2-v3-server/Models/ClientResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace NetV3Server.Models; +namespace NetV2V3Server.Models; public class ClientResponse { diff --git a/test-server/net-v2-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs index 755747fc..4bed926a 100644 --- a/test-server/net-v2-v3-server/Models/ErrorModels.cs +++ b/test-server/net-v2-v3-server/Models/ErrorModels.cs @@ -1,4 +1,4 @@ -namespace NetV3Server.Models; +namespace NetV2V3Server.Models; public class GenericServerError { diff --git a/test-server/net-v2-v3-server/Program.cs b/test-server/net-v2-v3-server/Program.cs index c2ca4937..1b77de5d 100644 --- a/test-server/net-v2-v3-server/Program.cs +++ b/test-server/net-v2-v3-server/Program.cs @@ -1,4 +1,4 @@ -using NetV3Server.Services; +using NetV2V3Server.Services; var builder = WebApplication.CreateBuilder(args); diff --git a/test-server/net-v2-v3-server/Services/ClientCacheService.cs b/test-server/net-v2-v3-server/Services/ClientCacheService.cs index 00e14a33..514f2616 100644 --- a/test-server/net-v2-v3-server/Services/ClientCacheService.cs +++ b/test-server/net-v2-v3-server/Services/ClientCacheService.cs @@ -2,7 +2,7 @@ using Amazon.Extensions.S3.Encryption; using System.Collections.Concurrent; -namespace NetV3Server.Services; +namespace NetV2V3Server.Services; public interface IClientCacheService { From 6255978d12e4f206157430c80476ee9a13cf2e39 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:23:29 -0700 Subject: [PATCH 035/201] Error model --- test-server/net-v2-v3-server/Models/ErrorModels.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/net-v2-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs index 4bed926a..549a6daf 100644 --- a/test-server/net-v2-v3-server/Models/ErrorModels.cs +++ b/test-server/net-v2-v3-server/Models/ErrorModels.cs @@ -2,12 +2,12 @@ namespace NetV2V3Server.Models; public class GenericServerError { - public string __type { get; set; } = "software.amazon.encryption.s3#GenericServerError"; + public string Type { get; set; } = "software.amazon.encryption.s3#GenericServerError"; public string Message { get; set; } = string.Empty; } public class S3EncryptionClientError { - public string __type { get; set; } = "software.amazon.encryption.s3#S3EncryptionClientError"; + public string Type { get; set; } = "software.amazon.encryption.s3#S3EncryptionClientError"; public string Message { get; set; } = string.Empty; } From 69fdd205fc00d0a13748e7fcde84a9a358964ead Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:42:52 -0700 Subject: [PATCH 036/201] auto commit --- test-server/net-v2-v3-server/Models/ErrorModels.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-server/net-v2-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs index 549a6daf..a219ed25 100644 --- a/test-server/net-v2-v3-server/Models/ErrorModels.cs +++ b/test-server/net-v2-v3-server/Models/ErrorModels.cs @@ -2,12 +2,14 @@ namespace NetV2V3Server.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; } From 4738803574597bf0360d8dd5a7d8c6bebbfcc5c8 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:47:59 -0700 Subject: [PATCH 037/201] auto commit --- .../software/amazon/encryption/s3/RoundTripTests.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 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 e4d109d5..56734b7d 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 @@ -85,16 +85,18 @@ public class RoundTripTests { serverMap.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); } - // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt) (Go) - // or validates only from the stored object (.NET). + // Encryption context validation behavior varies by implementation: + // - Go: Does not validate encryption context on decrypt operations + // - .NET: Only validates against encryption context stored in the object metadata // If the encryption context provided to getObject does not match the encryption context on the stored object, // these implementations will not raise an error as expected. // For now, skip tests that expect encryption context validation on decrypt. private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = Set.of(GO_V3, NET_V2, NET_V3); - // These S3EC implementations do not have a way to provide encryption context to putObject (i.e. on encrypt) (dotnet). - // So, the way these tests are configured, in these languages encryption context will not be passed on encrypt. + // S3EC .NET implementations does not accept encryption context (EC) during putObject operations. + // These tests are not configured to pass encryption context at client level but at encrypt, + // So, for .NET EC is not passed. // For now, skip tests that expect encryption context validation on decrypt. private static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2, NET_V3); From a3baf9d20f18f912db3acc63d855843e80068afb Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:53:15 -0700 Subject: [PATCH 038/201] auto commit --- test-server/net-v2-v3-server/Models/ErrorModels.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-server/net-v2-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs index a219ed25..af1646e7 100644 --- a/test-server/net-v2-v3-server/Models/ErrorModels.cs +++ b/test-server/net-v2-v3-server/Models/ErrorModels.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace NetV2V3Server.Models; public class GenericServerError From a37a232940c856ca251779d8dd148442f3e486be Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 15:59:29 -0700 Subject: [PATCH 039/201] remove redundant import --- test-server/net-v2-v3-server/Services/ClientCacheService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test-server/net-v2-v3-server/Services/ClientCacheService.cs b/test-server/net-v2-v3-server/Services/ClientCacheService.cs index 514f2616..d8239c9b 100644 --- a/test-server/net-v2-v3-server/Services/ClientCacheService.cs +++ b/test-server/net-v2-v3-server/Services/ClientCacheService.cs @@ -1,4 +1,3 @@ -using Amazon.S3; using Amazon.Extensions.S3.Encryption; using System.Collections.Concurrent; From 8d07ee42c606a63c581d3964c19a672ecab21479 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 18:53:36 -0700 Subject: [PATCH 040/201] auto commit --- test-server/net-v2-v3-server/Controllers/ClientController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index 570fad61..d3d19305 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -21,7 +21,7 @@ public IActionResult CreateClient([FromBody] ClientRequest request) if (string.IsNullOrEmpty(kmsKeyId)) { - return BadRequest(new S3EncryptionClientError + return BadRequest(new GenericServerError { Message = "KMS Key ID is required" }); From a89ce299ae84ed8a8336fb3eb4ddb61ce4a3143c Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Thu, 18 Sep 2025 19:24:51 -0700 Subject: [PATCH 041/201] validation --- .../net-v2-v3-server/Controllers/ClientController.cs | 8 -------- test-server/net-v2-v3-server/Models/ClientRequest.cs | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index d3d19305..ad4f0e1e 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -19,14 +19,6 @@ public IActionResult CreateClient([FromBody] ClientRequest request) var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; var encryptionContext = request.Config.EncryptionContext; - if (string.IsNullOrEmpty(kmsKeyId)) - { - return BadRequest(new GenericServerError - { - Message = "KMS Key ID is required" - }); - } - try { var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index 710b5e8a..cbc1ae0e 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -1,7 +1,10 @@ +using System.ComponentModel.DataAnnotations; + namespace NetV2V3Server.Models; public class ClientRequest { + [Required] public ClientConfig Config { get; set; } = new(); } @@ -10,10 +13,12 @@ public class ClientConfig public Dictionary EncryptionContext { get; set; } = new(); public bool EnableLegacyUnauthenticatedModes { get; set; } public bool EnableLegacyWrappingAlgorithms { get; set; } + [Required] public KeyMaterial KeyMaterial { get; set; } = new(); } public class KeyMaterial { + [Required] public string KmsKeyId { get; set; } = string.Empty; } \ No newline at end of file From 35cc7e222cc31c9914c799ff87a02cc4311b3b82 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 09:53:43 -0400 Subject: [PATCH 042/201] m --- test-server/Makefile | 4 +- test-server/cpp-v2-server/CMakeLists.txt | 4 +- test-server/cpp-v2-server/main.cpp | 205 ++++++++++++------ .../amazon/encryption/s3/RoundTripTests.java | 5 +- 4 files changed, 148 insertions(+), 70 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index bfb92180..3c415cb6 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -99,7 +99,9 @@ stop-servers: @echo "Servers stopped" install-cpp-dependencies: - brew install libmicrohttpd nlohmann-json ossp-uuid aws-sdk-cpp + brew install libmicrohttpd nlohmann-json ossp-uuid + cd cpp-v2-server && git clone --recurse-submodules git@github.com:aws/aws-sdk-cpp.git + cd cpp-v2-server/aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object start-cpp-v2-server: cd cpp-v2-server && mkdir -p build && cd build && cmake .. && make && ./s3ec-server diff --git a/test-server/cpp-v2-server/CMakeLists.txt b/test-server/cpp-v2-server/CMakeLists.txt index d9fd9d1b..4b5bd3b8 100644 --- a/test-server/cpp-v2-server/CMakeLists.txt +++ b/test-server/cpp-v2-server/CMakeLists.txt @@ -4,12 +4,12 @@ project(s3ec-cpp-server) set(CMAKE_CXX_STANDARD 17) # Configure AWS SDK build options -set(BUILD_ONLY "kms;s3" CACHE STRING "Build only KMS and S3 components") +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") # Add AWS SDK as subdirectory -add_subdirectory(/Users/ajewell/head/aws-sdk-cpp aws-sdk-cpp) +add_subdirectory(aws-sdk-cpp) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index dc53f38f..b3fa281c 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -19,7 +19,8 @@ using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; std::unordered_map> client_cache; -std::string generate_uuid() { +std::string generate_uuid() +{ uuid_t uuid; uuid_generate(uuid); char uuid_str[37]; @@ -28,20 +29,23 @@ std::string generate_uuid() { } MHD_Result print_key(void *cls, enum MHD_ValueKind kind, const char *key, - const char *value) { + const char *value) +{ fprintf(stderr, "%s: %s\n", key, value); return MHD_YES; } std::string get_header_value(struct MHD_Connection *connection, - const char *key) { + const char *key) +{ const char *value = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); return value ? std::string(value) : ""; } MHD_Result send_response(struct MHD_Connection *connection, int status_code, - const std::string &content) { + const std::string &content) +{ struct MHD_Response *response = MHD_create_response_from_buffer( content.length(), (void *)content.c_str(), MHD_RESPMEM_MUST_COPY); MHD_Result ret = MHD_queue_response(connection, status_code, response); @@ -51,17 +55,33 @@ MHD_Result send_response(struct MHD_Connection *connection, int status_code, return ret; } +std::string make_error(const std::string &message, int status_code) +{ + std::string inner = "{\"__type\": \"software.amazon.encryption.s3#S3EncryptionClientError\", \"message\": \"" + message + "\"}"; + return inner; +} + MHD_Result handle_create_client(struct MHD_Connection *connection, - const std::string &body) { - try { + const std::string &body) +{ + try + { json request = json::parse(body); std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; + bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; Aws::KMS::KMSClient kms_client; auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (legacy) + { + config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + } + else + { + config.SetSecurityProfile(SecurityProfile::V2); + } auto encryption_client = std::make_shared(config); @@ -70,50 +90,25 @@ 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) { + } + catch (const std::exception &e) + { fprintf(stderr, "Error: %s\n", e.what()); return send_response(connection, 500, "{\"error\":\"" + std::string(e.what()) + "\"}"); - } catch (...) { + } + catch (...) + { fprintf(stderr, "Super secret error"); return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); } } -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()) { - return send_response(connection, 404, "{\"error\":\"Client not found\"}"); - } - fprintf(stderr, "handle_get_object <%s>\n", metadata.c_str()); - try { - Aws::S3::Model::GetObjectRequest request; - request.SetBucket(bucket); - request.SetKey(key); - - auto outcome = it->second->GetObject(request); - if (outcome.IsSuccess()) { - auto &stream = outcome.GetResult().GetBody(); - std::string content((std::istreambuf_iterator(stream)), - std::istreambuf_iterator()); - return send_response(connection, 200, content); - } else { - fprintf(stderr, "GetObject Failed : %s\n", - outcome.GetError().GetMessage().c_str()); - return send_response(connection, 500, "{\"error\":\"GetObject failed\"}"); - } - } catch (const std::exception &e) { - return send_response(connection, 500, - "{\"error\":\"" + std::string(e.what()) + "\"}"); - } -} - void fill_context(Aws::Map &map, - const std::string &metadata) { - if (metadata.empty()) { + const std::string &metadata) +{ + if (metadata.empty()) + { return; } @@ -122,7 +117,8 @@ void fill_context(Aws::Map &map, std::string current = metadata; size_t pos = 0; - while (pos < current.length()) { + while (pos < current.length()) + { // Find opening bracket for key size_t key_start = current.find('[', pos); if (key_start == std::string::npos) @@ -159,25 +155,80 @@ void fill_context(Aws::Map &map, // Move to next pair (look for comma or next opening bracket) pos = value_end + 1; size_t comma = current.find(',', pos); - if (comma != std::string::npos) { + if (comma != std::string::npos) + { pos = comma + 1; } } } +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()) + { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + fprintf(stderr, "handle_get_object <%s>\n", metadata.c_str()); + try + { + Aws::S3::Model::GetObjectRequest request; + 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); + + if (outcome.IsSuccess()) + { + auto &stream = outcome.GetResult().GetBody(); + std::string content((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + return send_response(connection, 200, content); + } + else + { + auto msg = make_error( outcome.GetError().GetMessage(), 500); + fprintf(stderr, "GetObject Failed : %s\n", msg.c_str()); + return send_response(connection, 500, msg); + } + } + catch (const std::exception &e) + { + auto msg = make_error( e.what(), 500); + fprintf(stderr, "GetObject Threw : %s\n", msg.c_str()); + 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) { + const std::string &metadata) +{ auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + if (it == client_cache.end()) + { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } fprintf(stderr, "handle_put_object <%s>\n", metadata.c_str()); - try { + try + { Aws::Map kmsContextMap; - // Parse metadata and populate the context map fill_context(kmsContextMap, metadata); Aws::S3::Model::PutObjectRequest request; @@ -188,34 +239,48 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, request.SetBody(stream); auto outcome = it->second->PutObject(request, kmsContextMap); - if (outcome.IsSuccess()) { + if (outcome.IsSuccess()) + { json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); - } else { - return send_response(connection, 500, "{\"error\":\"PutObject failed\"}"); } - } catch (const std::exception &e) { - return send_response(connection, 500, - "{\"error\":\"" + std::string(e.what()) + "\"}"); + else + { + auto msg = make_error( outcome.GetError().GetMessage(), 500); + fprintf(stderr, "PutObject Failed : %s\n", msg.c_str()); + return send_response(connection, 500, msg); + } + } + catch (const std::exception &e) + { + auto msg = make_error( e.what(), 500); + fprintf(stderr, "PutObject Threw : %s\n", msg.c_str()); + return send_response(connection, 500, msg); } } 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) { + 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) { + if (*con_cls == nullptr) + { + if (is_push) + { *con_cls = new std::string(); - } else { + } + else + { *con_cls = &dummy; } return MHD_YES; } - if (is_push && *upload_data_size) { + if (is_push && *upload_data_size) + { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); *upload_data_size = 0; @@ -224,7 +289,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string url_str(url); - if (is_push && url_str == "/client") { + if (is_push && url_str == "/client") + { std::string *body = static_cast(*con_cls); auto foo = handle_create_client(connection, *body); delete body; @@ -235,18 +301,23 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, // upload_data); fprintf(stderr, "keys<<\n"); MHD_get_connection_values // (connection, MHD_HEADER_KIND, &print_key, NULL); fprintf(stderr, ">>\n"); - if (url_str.find("/object/") == 0) { + if (url_str.find("/object/") == 0) + { std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); - if (slash_pos != std::string::npos) { + 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"); - if (method_str == "GET") { + if (method_str == "GET") + { return handle_get_object(connection, bucket, key, client_id, metadata); - } else if (method_str == "PUT") { + } + else if (method_str == "PUT") + { std::string *body = static_cast(*con_cls); *upload_data_size = 0; auto foo = handle_put_object(connection, bucket, key, client_id, *body, @@ -261,7 +332,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, "{\"error\":\"Not idea what is happening\"}"); } -int main() { +int main() +{ Aws::SDKOptions options; Aws::InitAPI(options); @@ -269,7 +341,8 @@ int main() { MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, 8085, NULL, NULL, &request_handler, NULL, MHD_OPTION_END); - if (!daemon) { + if (!daemon) + { return 1; } 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 535e4d8d..2ec03e73 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 @@ -494,7 +494,10 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - 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") + || e.getMessage().contains("Retry with V2_AND_LEGACY enabled") + ); } } From c8cdae48b58cd4337484b0225564af849d507094 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 09:57:54 -0400 Subject: [PATCH 043/201] m --- test-server/cpp-v2-server/CMakeLists.txt | 2 +- test-server/cpp-v2-server/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/cpp-v2-server/CMakeLists.txt b/test-server/cpp-v2-server/CMakeLists.txt index 4b5bd3b8..b282dbc4 100644 --- a/test-server/cpp-v2-server/CMakeLists.txt +++ b/test-server/cpp-v2-server/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(s3ec-cpp-server) +project(s3ec-cpp-v2-server) set(CMAKE_CXX_STANDARD 17) diff --git a/test-server/cpp-v2-server/README.md b/test-server/cpp-v2-server/README.md index 06d55be1..8e77feda 100644 --- a/test-server/cpp-v2-server/README.md +++ b/test-server/cpp-v2-server/README.md @@ -28,7 +28,7 @@ make ./s3ec-server ``` -Server runs on localhost:8081 +Server runs on localhost:8085 ## API Endpoints From c9224b5dddb259ee0a693a9f8e9e1c467a95717e Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 10:00:46 -0400 Subject: [PATCH 044/201] m --- test-server/Makefile | 132 +++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 86 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index 3c415cb6..90d15648 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,6 +1,6 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers start-python-v3-server start-java-v3-server start-go-v3-server run-tests stop-servers clean ci check-env help +.PHONY: all start-servers run-tests stop-servers clean ci check-env help # Default target all: start-servers run-tests @@ -8,65 +8,31 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -# Start Python server in background -start-python-v3-server: - @echo "Starting Python V3 server..." - cd python-v3-server && \ - python -m venv .venv && \ - .venv/bin/python -m ensurepip && \ - .venv/bin/python -m pip install -e . && \ - .venv/bin/python -m pip install -e ../.. && \ - 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" \ - .venv/bin/python src/main.py & echo $$! > ../python-v3-server.pid - @echo "Python server starting..." - -# Start Java server in background -start-java-v3-server: - @echo "Starting Java V3 server..." - cd java-v3-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" \ - ./gradlew --build-cache --parallel run & echo $$! > ../java-v3-server.pid - @echo "Java server starting..." - -# Start Go server in background -start-go-v3-server: - @echo "Starting Go V3 server..." - cd go-v3-server && \ - go mod tidy && \ - 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" \ - go run . & echo $$! > ../go-v3-server.pid - @echo "Go server starting..." +SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) # Start all servers in parallel start-servers: - @echo "Starting servers in parallel..." - @$(MAKE) -j3 start-python-v3-server start-java-v3-server start-go-v3-server - @echo "Waiting for servers to be ready..." - @for i in $$(seq 1 360); do \ - if nc -z localhost 8080 && nc -z localhost 8081 && nc -z localhost 8082; then \ - echo "Ports are open, waiting for servers to initialize..."; \ - sleep 5; \ - echo "All servers are ready!"; \ - break; \ - fi; \ - if [ $$i -eq 360 ]; then \ - echo "Timeout waiting for servers to start"; \ - exit 1; \ - fi; \ - echo "Waiting for servers to start ($$i/360)..."; \ - sleep 1; \ + @echo "Starting all servers..." + $(MAKE) start-all-servers + @echo "Waiting for servers to start..." + @for dir in $(SERVER_DIRS); do \ + echo "Waiting for server in $$dir..."; \ + $(MAKE) -C $$dir wait-for-server; \ done +start-all-servers: $(SERVER_TARGETS) + +$(SERVER_TARGETS): start-%: + @if [ -f $*/Makefile ]; then \ + echo "Starting server in $*..."; \ + $(MAKE) -C $* start-server; \ + else \ + echo "❌ Error: no Makefile found in $$dir"; \ + exit 1; \ + fi; \ + # Run the Java tests run-tests: @@ -84,48 +50,20 @@ run-tests: # Stop the servers stop-servers: @echo "Stopping servers..." - @if [ -f python-v3-server.pid ]; then \ - kill $$(cat python-v3-server.pid) 2>/dev/null || true; \ - rm python-v3-server.pid; \ - fi - @if [ -f java-v3-server.pid ]; then \ - kill $$(cat java-v3-server.pid) 2>/dev/null || true; \ - rm java-v3-server.pid; \ - fi - @if [ -f go-v3-server.pid ]; then \ - kill $$(cat go-v3-server.pid) 2>/dev/null || true; \ - rm go-v3-server.pid; \ - fi + @for dir in $(SERVER_DIRS); do \ + echo "Starting server in $$dir..."; \ + $(MAKE) -C $$dir stop-server; \ + done @echo "Servers stopped" -install-cpp-dependencies: - brew install libmicrohttpd nlohmann-json ossp-uuid - cd cpp-v2-server && git clone --recurse-submodules git@github.com:aws/aws-sdk-cpp.git - cd cpp-v2-server/aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object - -start-cpp-v2-server: - cd cpp-v2-server && mkdir -p build && cd build && cmake .. && make && ./s3ec-server - -# Clean up logs and pid files -clean: stop-servers - @echo "Cleaning up..." - @rm -f python-v3-server.log java-v3-server.log go-v3-server.log - @echo "Cleanup complete" - # Help target help: @echo "Available targets:" @echo " all : Start servers and run tests (default, output to stdout)" @echo " ci : Run in CI mode (start servers, run tests, stop servers)" @echo " start-servers : Start all servers in parallel" - @echo " start-python-v3-server : Start only the Python V3 server" - @echo " start-java-v3-server : Start only the Java V3 server" - @echo " start-go-v3-server : Start only the Go V3 server" - @echo " start-cpp-v2-server : Start only the C++ V2 server" - @echo " install-cpp-dependencies: use brew to install things necessary for start-cpp-v2-server" @echo " run-tests : Run Java tests" @echo " stop-servers : Stop running servers" - @echo " clean : Stop servers and clean up logs" @echo " check-env : Check if required environment variables are set" @echo " help : Show this help message" @@ -136,3 +74,25 @@ check-env: @if [ -z "$$AWS_SECRET_ACCESS_KEY" ]; then echo "AWS_SECRET_ACCESS_KEY is not set"; else echo "AWS_SECRET_ACCESS_KEY is set"; fi @if [ -z "$$AWS_SESSION_TOKEN" ]; then echo "AWS_SESSION_TOKEN is not set"; else echo "AWS_SESSION_TOKEN is set"; fi @if [ -z "$$AWS_REGION" ]; then echo "AWS_REGION is not set (will use us-west-2 as default)"; else echo "AWS_REGION is set to $$AWS_REGION"; fi + +TIMEOUT := 360 + +wait-for-port: + @if [ -z "$(PORT)" ]; then \ + echo "❌ Error: PORT is required"; \ + exit 1; \ + fi + @for i in $$(seq 1 $(TIMEOUT)); do \ + if nc -z localhost $$PORT; then \ + echo "Ports are open, waiting for servers to initialize..."; \ + sleep 5; \ + echo "Server at $$PORT is ready!"; \ + break; \ + fi; \ + if [ $$i -eq $(TIMEOUT) ]; then \ + echo "Timeout waiting for $$PORT start"; \ + exit 1; \ + fi; \ + echo "Waiting for $$PORT to start ($$i/$(TIMEOUT))..."; \ + sleep 1; \ + done From 7cdf7e69f881a70449ba0de874f045a3036c3a5c Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 11:04:31 -0400 Subject: [PATCH 045/201] m --- test-server/cpp-v2-server/Makefile | 33 ++++ test-server/cpp-v2-server/main.cpp | 167 ++++++------------ .../amazon/encryption/s3/RoundTripTests.java | 2 + 3 files changed, 87 insertions(+), 115 deletions(-) create mode 100644 test-server/cpp-v2-server/Makefile diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile new file mode 100644 index 00000000..7238b99d --- /dev/null +++ b/test-server/cpp-v2-server/Makefile @@ -0,0 +1,33 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8085 + +build/s3ec-server: + pwd + ls aws-sdk-cpp/README.md + brew install libmicrohttpd nlohmann-json ossp-uuid + git clone --recurse-submodules git@github.com:aws/aws-sdk-cpp.git + cd aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object + mkdir -p build && cd build && cmake .. + +start-server: | build/s3ec-server + @echo "Starting Cpp V2 server..." + cd build && make && \ + 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" \ + ./s3ec-server & echo $$! > $(PID_FILE) + @echo "Go V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index b3fa281c..aa790959 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -19,8 +19,7 @@ using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; std::unordered_map> client_cache; -std::string generate_uuid() -{ +std::string generate_uuid() { uuid_t uuid; uuid_generate(uuid); char uuid_str[37]; @@ -28,44 +27,32 @@ std::string generate_uuid() return std::string(uuid_str); } -MHD_Result print_key(void *cls, enum MHD_ValueKind kind, const char *key, - const char *value) -{ - fprintf(stderr, "%s: %s\n", key, value); - return MHD_YES; -} - std::string get_header_value(struct MHD_Connection *connection, - const char *key) -{ + const char *key) { const char *value = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); return value ? std::string(value) : ""; } MHD_Result send_response(struct MHD_Connection *connection, int status_code, - const std::string &content) -{ + const std::string &content) { struct MHD_Response *response = MHD_create_response_from_buffer( content.length(), (void *)content.c_str(), MHD_RESPMEM_MUST_COPY); MHD_Result ret = MHD_queue_response(connection, status_code, response); - if (ret != MHD_YES) - fprintf(stderr, "MHD_queue_response returned %d\n", ret); MHD_destroy_response(response); return ret; } -std::string make_error(const std::string &message, int status_code) -{ - std::string inner = "{\"__type\": \"software.amazon.encryption.s3#S3EncryptionClientError\", \"message\": \"" + message + "\"}"; - return inner; +std::string make_error(const std::string &message, int status_code) { + return "{\"__type\": " + "\"software.amazon.encryption.s3#S3EncryptionClientError\", " + "\"message\": \"" + + message + "\"}"; } MHD_Result handle_create_client(struct MHD_Connection *connection, - const std::string &body) -{ - try - { + const std::string &body) { + try { json request = json::parse(body); std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; @@ -74,12 +61,9 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - if (legacy) - { + if (legacy) { config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - } - else - { + } else { config.SetSecurityProfile(SecurityProfile::V2); } @@ -90,25 +74,17 @@ 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, "Error: %s\n", e.what()); + } catch (const std::exception &e) { return send_response(connection, 500, "{\"error\":\"" + std::string(e.what()) + "\"}"); - } - catch (...) - { - fprintf(stderr, "Super secret error"); + } catch (...) { return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); } } void fill_context(Aws::Map &map, - const std::string &metadata) -{ - if (metadata.empty()) - { + const std::string &metadata) { + if (metadata.empty()) { return; } @@ -117,8 +93,7 @@ void fill_context(Aws::Map &map, std::string current = metadata; size_t pos = 0; - while (pos < current.length()) - { + while (pos < current.length()) { // Find opening bracket for key size_t key_start = current.find('[', pos); if (key_start == std::string::npos) @@ -155,8 +130,7 @@ void fill_context(Aws::Map &map, // Move to next pair (look for comma or next opening bracket) pos = value_end + 1; size_t comma = current.find(',', pos); - if (comma != std::string::npos) - { + if (comma != std::string::npos) { pos = comma + 1; } } @@ -165,16 +139,13 @@ 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) -{ + const std::string &metadata) { auto it = client_cache.find(client_id); - if (it == client_cache.end()) - { + if (it == client_cache.end()) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } - fprintf(stderr, "handle_get_object <%s>\n", metadata.c_str()); - try - { + + try { Aws::S3::Model::GetObjectRequest request; request.SetBucket(bucket); request.SetKey(key); @@ -192,24 +163,17 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, fill_context(kmsContextMap, metadata); auto outcome = it->second->GetObject(request, kmsContextMap); - if (outcome.IsSuccess()) - { + if (outcome.IsSuccess()) { auto &stream = outcome.GetResult().GetBody(); std::string content((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); return send_response(connection, 200, content); - } - else - { - auto msg = make_error( outcome.GetError().GetMessage(), 500); - fprintf(stderr, "GetObject Failed : %s\n", msg.c_str()); + } else { + auto msg = make_error(outcome.GetError().GetMessage(), 500); return send_response(connection, 500, msg); } - } - catch (const std::exception &e) - { - auto msg = make_error( e.what(), 500); - fprintf(stderr, "GetObject Threw : %s\n", msg.c_str()); + } catch (const std::exception &e) { + auto msg = make_error(e.what(), 500); return send_response(connection, 500, msg); } } @@ -218,16 +182,13 @@ 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) -{ + const std::string &metadata) { auto it = client_cache.find(client_id); - if (it == client_cache.end()) - { + if (it == client_cache.end()) { return send_response(connection, 404, "{\"error\":\"Client not found\"}"); } - fprintf(stderr, "handle_put_object <%s>\n", metadata.c_str()); - try - { + + try { Aws::Map kmsContextMap; fill_context(kmsContextMap, metadata); @@ -239,48 +200,35 @@ MHD_Result handle_put_object(struct MHD_Connection *connection, request.SetBody(stream); auto outcome = it->second->PutObject(request, kmsContextMap); - if (outcome.IsSuccess()) - { + if (outcome.IsSuccess()) { json response = {{"bucket", bucket}, {"key", key}}; return send_response(connection, 200, response.dump()); - } - else - { - auto msg = make_error( outcome.GetError().GetMessage(), 500); - fprintf(stderr, "PutObject Failed : %s\n", msg.c_str()); + } else { + auto msg = make_error(outcome.GetError().GetMessage(), 500); return send_response(connection, 500, msg); } - } - catch (const std::exception &e) - { - auto msg = make_error( e.what(), 500); - fprintf(stderr, "PutObject Threw : %s\n", msg.c_str()); - return send_response(connection, 500, msg); + } catch (const std::exception &e) { + auto msg = make_error(e.what(), 500); + return send_response(connection, 500, msg); } } 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) -{ + 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) - { + if (*con_cls == nullptr) { + if (is_push) { *con_cls = new std::string(); - } - else - { + } else { *con_cls = &dummy; } return MHD_YES; } - if (is_push && *upload_data_size) - { + if (is_push && *upload_data_size) { std::string *body = static_cast(*con_cls); body->append(upload_data, *upload_data_size); *upload_data_size = 0; @@ -289,35 +237,25 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string url_str(url); - if (is_push && url_str == "/client") - { + if (is_push && url_str == "/client") { std::string *body = static_cast(*con_cls); auto foo = handle_create_client(connection, *body); delete body; return foo; } - // fprintf(stderr, "request_handler <%s> <%s> <%s>\n", url, method, - // upload_data); fprintf(stderr, "keys<<\n"); MHD_get_connection_values - // (connection, MHD_HEADER_KIND, &print_key, NULL); fprintf(stderr, ">>\n"); - - if (url_str.find("/object/") == 0) - { + if (url_str.find("/object/") == 0) { std::string path = url_str.substr(8); // Remove "/object/" size_t slash_pos = path.find('/'); - if (slash_pos != std::string::npos) - { + 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"); - if (method_str == "GET") - { + if (method_str == "GET") { return handle_get_object(connection, bucket, key, client_id, metadata); - } - else if (method_str == "PUT") - { + } else if (method_str == "PUT") { std::string *body = static_cast(*con_cls); *upload_data_size = 0; auto foo = handle_put_object(connection, bucket, key, client_id, *body, @@ -332,8 +270,7 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, "{\"error\":\"Not idea what is happening\"}"); } -int main() -{ +int main() { Aws::SDKOptions options; Aws::InitAPI(options); @@ -341,13 +278,13 @@ int main() MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, 8085, NULL, NULL, &request_handler, NULL, MHD_OPTION_END); - if (!daemon) - { + if (!daemon) { + fprintf(stderr, "Failed to start server on port 8085\n"); return 1; } - printf("Server running on port 8085\n"); - getchar(); + fprintf(stderr, "Server running on port 8085\n"); + sleep(10000); MHD_stop_daemon(daemon); Aws::ShutdownAPI(options); 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 2ec03e73..20381fa8 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 @@ -68,11 +68,13 @@ public class RoundTripTests { serverList.add(new LanguageServerTarget("Java-V3", "8080")); serverList.add(new LanguageServerTarget("Python-V3", "8081")); serverList.add(new LanguageServerTarget("Go-V3", "8082")); + serverList.add(new LanguageServerTarget("Cpp-V2", "8085")); serverMap = new HashMap<>(14); serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080")); serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081")); serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); + serverMap.put("Cpp-V2", new LanguageServerTarget("Cpp-V2", "8085")); } // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt). From 6b6bcd77b73424875bd176fe20af61ad869df14c Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 12:42:18 -0400 Subject: [PATCH 046/201] m --- test-server/cpp-v2-server/Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 7238b99d..e8145593 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -6,8 +6,6 @@ PID_FILE := server.pid PORT := 8085 build/s3ec-server: - pwd - ls aws-sdk-cpp/README.md brew install libmicrohttpd nlohmann-json ossp-uuid git clone --recurse-submodules git@github.com:aws/aws-sdk-cpp.git cd aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object From f133f014bf36884c34912cee3c8c7265adc0c90d Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 12:59:17 -0400 Subject: [PATCH 047/201] m --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16057711..637003f3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: jobs: lint: - runs-on: ubuntu-latest + runs-on: macos-13 steps: - name: Checkout code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8025246..e636855e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: macos-13 permissions: id-token: write contents: read From a2ed8254542ecf7a5837a96d2120a0c6c7f07c59 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 13:04:04 -0400 Subject: [PATCH 048/201] m --- test-server/cpp-v2-server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index e8145593..2d6328e8 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -7,7 +7,7 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid - git clone --recurse-submodules git@github.com:aws/aws-sdk-cpp.git + git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git cd aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object mkdir -p build && cd build && cmake .. From 715785045dfd88b1f09dcae781c34219984a5626 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 13:18:09 -0400 Subject: [PATCH 049/201] m --- .github/workflows/test.yml | 5 +++++ test-server/cpp-v2-server/Makefile | 2 +- test-server/cpp-v2-server/main.cpp | 9 +++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e636855e..2b1fe8a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,11 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.25 + # Cache uv dependencies - name: Cache uv dependencies uses: actions/cache@v3 diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 2d6328e8..e9156d64 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -19,7 +19,7 @@ start-server: | build/s3ec-server AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ AWS_REGION="us-west-2" \ ./s3ec-server & echo $$! > $(PID_FILE) - @echo "Go V3 server starting..." + @echo "Cpp V2 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index aa790959..51cfd65a 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -37,7 +37,7 @@ std::string get_header_value(struct MHD_Connection *connection, MHD_Result send_response(struct MHD_Connection *connection, int status_code, const std::string &content) { struct MHD_Response *response = MHD_create_response_from_buffer( - content.length(), (void *)content.c_str(), MHD_RESPMEM_MUST_COPY); + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); MHD_Result ret = MHD_queue_response(connection, status_code, response); MHD_destroy_response(response); return ret; @@ -76,7 +76,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, return send_response(connection, 200, response.dump()); } catch (const std::exception &e) { return send_response(connection, 500, - "{\"error\":\"" + std::string(e.what()) + "\"}"); + "{\"error\":\"An exception was thrown.\"}"); } catch (...) { return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); } @@ -173,7 +173,7 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, return send_response(connection, 500, msg); } } catch (const std::exception &e) { - auto msg = make_error(e.what(), 500); + auto msg = make_error("An exception was thrown", 500); return send_response(connection, 500, msg); } } @@ -275,11 +275,12 @@ int main() { Aws::InitAPI(options); struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, 8085, NULL, NULL, + MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, 8085, NULL, NULL, &request_handler, NULL, MHD_OPTION_END); if (!daemon) { fprintf(stderr, "Failed to start server on port 8085\n"); + Aws::ShutdownAPI(options); return 1; } From a21bc8ec965dcdf9b1008b653f2b83fd3e7e5d62 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 19 Sep 2025 14:28:41 -0400 Subject: [PATCH 050/201] m --- test-server/cpp-v2-server/main.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 51cfd65a..ccc72d76 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -238,10 +238,8 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, std::string url_str(url); if (is_push && url_str == "/client") { - std::string *body = static_cast(*con_cls); - auto foo = handle_create_client(connection, *body); - delete body; - return foo; + std::unique_ptr body(static_cast(*con_cls)); + return handle_create_client(connection, *body); } if (url_str.find("/object/") == 0) { @@ -256,12 +254,9 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, if (method_str == "GET") { return handle_get_object(connection, bucket, key, client_id, metadata); } else if (method_str == "PUT") { - std::string *body = static_cast(*con_cls); + std::unique_ptr body(static_cast(*con_cls)); *upload_data_size = 0; - auto foo = handle_put_object(connection, bucket, key, client_id, *body, - metadata); - delete body; - return foo; + return handle_put_object(connection, bucket, key, client_id, *body, metadata); } } } From f9ade30da70040c3fa575cd29740d8b314d3ef70 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 19 Sep 2025 14:41:28 -0700 Subject: [PATCH 051/201] Update test-server/net-v2-v3-server/Controllers/ObjectController.cs Co-authored-by: Tony Knapp <5892063+texastony@users.noreply.github.com> --- test-server/net-v2-v3-server/Controllers/ObjectController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test-server/net-v2-v3-server/Controllers/ObjectController.cs b/test-server/net-v2-v3-server/Controllers/ObjectController.cs index 98c8ce8b..bf1842ae 100644 --- a/test-server/net-v2-v3-server/Controllers/ObjectController.cs +++ b/test-server/net-v2-v3-server/Controllers/ObjectController.cs @@ -26,6 +26,7 @@ public async Task PutObject(string bucket, string key) { // 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(); From 1276c331a2753d3765af840b68d4dcf29ed7448e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:45:42 -0700 Subject: [PATCH 052/201] chore: add php servers --- .github/workflows/test.yml | 16 + .../amazon/encryption/s3/RoundTripTests.java | 6 +- test-server/php-v2-server/.gitignore | 4 + test-server/php-v2-server/Makefile | 24 ++ test-server/php-v2-server/README.md | 69 ++++ test-server/php-v2-server/composer.json | 24 ++ test-server/php-v2-server/src/client.php | 68 ++++ test-server/php-v2-server/src/errors.php | 42 +++ test-server/php-v2-server/src/get_object.php | 85 +++++ test-server/php-v2-server/src/index.php | 294 ++++++++++++++++++ test-server/php-v2-server/src/put_object.php | 72 +++++ test-server/php-v3-server/.gitignore | 4 + test-server/php-v3-server/Makefile | 24 ++ test-server/php-v3-server/README.md | 66 ++++ test-server/php-v3-server/composer.json | 24 ++ test-server/php-v3-server/src/client.php | 68 ++++ test-server/php-v3-server/src/errors.php | 42 +++ test-server/php-v3-server/src/get_object.php | 85 +++++ test-server/php-v3-server/src/index.php | 294 ++++++++++++++++++ test-server/php-v3-server/src/put_object.php | 72 +++++ 20 files changed, 1382 insertions(+), 1 deletion(-) create mode 100644 test-server/php-v2-server/.gitignore create mode 100644 test-server/php-v2-server/Makefile create mode 100644 test-server/php-v2-server/README.md create mode 100644 test-server/php-v2-server/composer.json create mode 100644 test-server/php-v2-server/src/client.php create mode 100644 test-server/php-v2-server/src/errors.php create mode 100644 test-server/php-v2-server/src/get_object.php create mode 100644 test-server/php-v2-server/src/index.php create mode 100644 test-server/php-v2-server/src/put_object.php create mode 100644 test-server/php-v3-server/.gitignore create mode 100644 test-server/php-v3-server/Makefile create mode 100644 test-server/php-v3-server/README.md create mode 100644 test-server/php-v3-server/composer.json create mode 100644 test-server/php-v3-server/src/client.php create mode 100644 test-server/php-v3-server/src/errors.php create mode 100644 test-server/php-v3-server/src/get_object.php create mode 100644 test-server/php-v3-server/src/index.php create mode 100644 test-server/php-v3-server/src/put_object.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8025246..0639f45a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,22 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} + - name: Set up PHP with Composer + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer:v2 + + - name: Install PHP V2 dependencies + working-directory: ./test-server/php-v2-server + shell: bash + run: composer install + + - name: Install PHP V3 dependencies + working-directory: ./test-server/php-v3-server + shell: bash + run: composer install + # Cache uv dependencies - name: Cache uv dependencies uses: actions/cache@v3 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 535e4d8d..5b9eb535 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 @@ -68,11 +68,15 @@ public class RoundTripTests { serverList.add(new LanguageServerTarget("Java-V3", "8080")); serverList.add(new LanguageServerTarget("Python-V3", "8081")); serverList.add(new LanguageServerTarget("Go-V3", "8082")); + serverList.add(new LanguageServerTarget("PHP-V2", "8087")); + serverList.add(new LanguageServerTarget("PHP-V3", "8093")); serverMap = new HashMap<>(14); serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080")); serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081")); serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082")); + serverMap.put("PHP-V2", new LanguageServerTarget("PHP-V2", "8087")); + serverMap.put("PHP-V3", new LanguageServerTarget("PHP-V3", "8093")); } // These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt). @@ -80,7 +84,7 @@ public class RoundTripTests { // these implementations will not raise an error as expected. // For now, skip tests that expect encryption context validation on decrypt. private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of("Go-V3"); + Set.of("Go-V3", "PHP-V2", "PHP-V3"); static public class LanguageServerTarget { public String getLanguageName() { diff --git a/test-server/php-v2-server/.gitignore b/test-server/php-v2-server/.gitignore new file mode 100644 index 00000000..07108589 --- /dev/null +++ b/test-server/php-v2-server/.gitignore @@ -0,0 +1,4 @@ +vendor/* +cookies.txt +server.pid +composer.lock \ No newline at end of file diff --git a/test-server/php-v2-server/Makefile b/test-server/php-v2-server/Makefile new file mode 100644 index 00000000..6962ce5e --- /dev/null +++ b/test-server/php-v2-server/Makefile @@ -0,0 +1,24 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8087 + +start-server: + @echo "Starting PHP V2 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" \ + composer run start & echo $$! > $(PID_FILE) + @echo "PHP V2 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/php-v2-server/README.md b/test-server/php-v2-server/README.md new file mode 100644 index 00000000..c4ba49fe --- /dev/null +++ b/test-server/php-v2-server/README.md @@ -0,0 +1,69 @@ +# S3EC PHP v2 Test Server + +This is the PHP V2 implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality. + +## Overview + +The S3ECPhpV2TestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients with session-based caching +- Putting objects with encryption +- Getting and decrypting objects + +## Starting the Server + +### Method 1: Using Composer (Recommended) +```bash +composer run start +``` + +The server will start on port `8087`. + +## Available Endpoints + +### Server Status +- **GET /** - Returns server status and available endpoints + +### Client Management +- **POST /client** - Creates an S3EncryptionClient and caches it with session persistence +- **GET /cache** - Shows current session state and cached clients (for debugging) + +### Object Operations +- **GET /object/{bucket}/{key}** - Handle GET requests using the S3EncryptionClient +- **PUT /object/{bucket}/{key}** - Handle PUT requests using the S3EncryptionClient + +## Testing with curl + +### Important: Session Cookie Management + +To properly test the server and maintain session persistence, you **must** use cookies with curl: + +#### First Request (creates session cookie): +```bash +curl -X POST http://localhost:8087/client \ + -H "Content-Type: application/json" \ + -c cookies.txt \ + -v +``` + +#### Subsequent Requests (reuses session cookie): +```bash +curl -X POST http://localhost:8087/client \ + -H "Content-Type: application/json" \ + -b cookies.txt \ + -c cookies.txt \ + -v +``` + +#### Check Cache Status: +```bash +curl http://localhost:8087/cache \ + -b cookies.txt +``` + +#### Helpful Notes +- **Session Storage**: Client configurations are stored in `$_SESSION['s3ecCache']` +- **Object Recreation**: AWS SDK objects are recreated from stored configuration (they cannot be serialized) +AWS SDK obbjects cannot be serialized due to internal resources and closures. +- **Helper Function**: `getCachedClient($clientId)` retrieves and recreates clients from cache +- **Debugging**: Enhanced logging and `/cache` endpoint for troubleshooting diff --git a/test-server/php-v2-server/composer.json b/test-server/php-v2-server/composer.json new file mode 100644 index 00000000..e9c399ac --- /dev/null +++ b/test-server/php-v2-server/composer.json @@ -0,0 +1,24 @@ +{ + "name": "aws/s3ec-php-v2-test-server", + "description": "PHP v2 implementation of the S3EC Test Server framework", + "type": "project", + "license": "Apache-2.0", + "require": { + "php": ">=7.4", + "aws/aws-sdk-php": "^3.356", + "ramsey/uuid": "^4.9" + }, + "autoload": { + "psr-4": { + "S3EC\\PhpV2Server\\": "src/" + } + }, + "scripts": { + "start": [ + "php -S 0.0.0.0:8087 src/index.php" + ] + }, + "config": { + "optimize-autoloader": true + } +} \ No newline at end of file diff --git a/test-server/php-v2-server/src/client.php b/test-server/php-v2-server/src/client.php new file mode 100644 index 00000000..44fe1b39 --- /dev/null +++ b/test-server/php-v2-server/src/client.php @@ -0,0 +1,68 @@ +toString(); + $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + + if ($configData == []) { + return GenericServerError("Invalid config in request body", 400); + } + if (($keyMaterial || $kmsKeyId) === null) { + return GenericServerError("Invalid keyMaterial in config", 400); + } + + // Store client configuration instead of objects (AWS objects can't be serialized) + $_SESSION['s3ecCache'][$clientId] = [ + 's3Config' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsConfig' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsKeyId' => $kmsKeyId, + 'legacy' => $legacyAlgorithms, + 'created' => time() + ]; + + // Auto-update cookies.txt with current session ID so tests can access cached clients + writeSessionIdToCookiesFile(session_id()); + + header("Content-Type: application/json"); + return json_encode([ + 'clientId' => $clientId, + ]); +} diff --git a/test-server/php-v2-server/src/errors.php b/test-server/php-v2-server/src/errors.php new file mode 100644 index 00000000..2b59861d --- /dev/null +++ b/test-server/php-v2-server/src/errors.php @@ -0,0 +1,42 @@ + 'GenericServerError', + 'message' => $message + ]; + + return json_encode($errorResponse); +} + +/** + * Used for modeled errors, e.g. errors thrown by the S3EC + * Tests SHOULD expect this error in negative tests. + * + * @param string $message The error message to include in the response + * @return string JSON-encoded error response + */ +function S3EncryptionClientError($message) +{ + http_response_code(500); + header('Content-Type: application/json'); + + $errorResponse = [ + "__type" => "software.amazon.encryption.s3#S3EncryptionClientError", + 'message' => $message + ]; + + return json_encode($errorResponse); +} diff --git a/test-server/php-v2-server/src/get_object.php b/test-server/php-v2-server/src/get_object.php new file mode 100644 index 00000000..61bacb5b --- /dev/null +++ b/test-server/php-v2-server/src/get_object.php @@ -0,0 +1,85 @@ +getObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + 'Bucket' => $bucket, + 'Key' => $key, + ]); + + // Capture and discard any unwanted output from AWS SDK + $unwantedOutput = ob_get_clean(); + if (!empty($unwantedOutput)) { + error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes"); + } + + $body = $result['Body']->getContents(); + $formattedMetadata = formatMetadataForResponse($result["Metadata"]); + + // Now set headers safely + header("Content-Metadata: " . $formattedMetadata); + header("Content-Type: application/octet-stream"); + header("Content-Length: " . strlen($body)); + return $body; + } catch (InvalidArgumentException $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + return GenericServerError("Invalid argument: " . $e->getMessage(), 400); + } catch (Exception $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { + return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); + } else { + return GenericServerError("Server argument: " . $e->getMessage(), 500); + } + } +} diff --git a/test-server/php-v2-server/src/index.php b/test-server/php-v2-server/src/index.php new file mode 100644 index 00000000..cc5dee29 --- /dev/null +++ b/test-server/php-v2-server/src/index.php @@ -0,0 +1,294 @@ += 7 && $parts[5] === 'PHPSESSID') { + error_log("Found session ID in cookies.txt: " . $parts[6]); + return $parts[6]; // Return the session ID value + } + } + + error_log("No PHPSESSID found in cookies.txt file"); + return null; +} + +// Function to write session ID to cookies.txt file +function writeSessionIdToCookiesFile($sessionId) +{ + $cookiesFile = __DIR__ . '/../cookies.txt'; + + // Create Netscape cookie format entry + $cookieLine = "localhost\tFALSE\t/\tFALSE\t0\tPHPSESSID\t$sessionId"; + + // Write header and cookie entry + $content = "# Netscape HTTP Cookie File\n"; + $content .= "# https://curl.se/docs/http-cookies.html\n"; + $content .= "# This file was generated by libcurl! Edit at your own risk.\n\n"; + $content .= $cookieLine . "\n"; + + $result = file_put_contents($cookiesFile, $content); + + if ($result === false) { + error_log("Failed to write session ID to cookies.txt file: $cookiesFile"); + return false; + } + + error_log("Successfully wrote session ID to cookies.txt: $sessionId"); + return true; +} + +set_time_limit(600); +// Start session to persist cache across requests +// First try to use session ID from cookies.txt if available +$sessionId = getSessionIdFromCookiesFile(); +if ($sessionId) { + session_id($sessionId); +} +session_start(); + +// Initialize session cache if it doesn't exist +if (!isset($_SESSION['s3ecCache'])) { + $_SESSION['s3ecCache'] = []; +} + +// Simple router class +class SimpleRouter +{ + private $routes = []; + + public function addRoute($method, $path, $handler) + { + $this->routes[] = [ + 'method' => strtoupper($method), + 'path' => $path, + 'handler' => $handler + ]; + } + + public function handleRequest() + { + $method = $_SERVER['REQUEST_METHOD']; + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + + foreach ($this->routes as $route) { + if ($route['method'] === $method) { + $params = $this->matchPathWithParams($route['path'], $path); + if ($params !== false) { + return call_user_func($route['handler'], $params); + } + } + } + + // Default 404 response + http_response_code(404); + return json_encode(['error' => 'Not Found']); + } + + private function matchPathWithParams($routePath, $requestPath) + { + // Handle exact matches first (for routes without parameters) + if ($routePath === $requestPath) { + return []; + } + + // Convert route path like '/object/{bucket}/{key}' to regex + $pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath); + $pattern = '/^' . str_replace('/', '\/', $pattern) . '$/'; + + if (preg_match($pattern, $requestPath, $matches)) { + array_shift($matches); // Remove full match + + // Extract parameter names + preg_match_all('/\{([^}]+)\}/', $routePath, $paramNames); + $params = []; + + foreach ($paramNames[1] as $index => $paramName) { + $params[$paramName] = $matches[$index] ?? null; + } + + return $params; + } + + return false; + } +} + +// Helper function to get cached client by ID +function getCachedClient($clientId) +{ + if (!isset($_SESSION['s3ecCache'][$clientId])) { + return null; + } + + $config = $_SESSION['s3ecCache'][$clientId]; + + // Recreate the AWS clients from stored configuration + $s3Client = new S3Client($config['s3Config']); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient($config['kmsConfig']); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider, + 'config' => $config + ]; +} + +function createDefaultClientTuple(): array +{ + $s3Client = new S3Client([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider + ]; +} + +function metadataStringToMap($metadata): array +{ + $md = []; + + if (empty($metadata)) { + return $md; + } + + $mdList = explode(',', $metadata); + + foreach ($mdList as $entry) { + $parts = explode(']:[', $entry); + + if (count($parts) === 2) { + $key = substr($parts[0], 1); + $value = substr($parts[1], 0, -1); + $md[$key] = $value; + } else { + throw new InvalidArgumentException("Malformed metadata list entry: " . $entry); + } + } + + return $md; +} +function formatMetadataForResponse($metadata) +{ + $metadataList = []; + // Handle different metadata input types + if (is_array($metadata)) { + // If it's an associative array (like Python dict) + foreach ($metadata as $key => $value) { + $metadataList[] = $key . '=' . $value; + } + } elseif (is_string($metadata) && !empty($metadata)) { + // If it's already a string, assume it's in the correct format + return $metadata; + } + + // Convert array to comma-separated string + return implode(',', $metadataList); +} + +// Initialize router +$router = new SimpleRouter(); + +// Add basic routes +$router->addRoute('GET', '/', function () { + return json_encode([ + 'service' => 'S3EC PHP v2 Test Server', + 'status' => 'running', + 'port' => 8087, + 'endpoints' => [ + 'GET /' => 'Server status', + 'POST /client' => 'Create an S3EncryptionClient and cache it.', + 'GET /object/{bucket}/{key}' => 'Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a GetObject request to S3.', + 'PUT /object/{bucket}/{key}' => 'Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a PutObject request to S3.', + ] + ]); +}); + +$router->addRoute('GET', '/cache', function () { + return json_encode([ + 'sessionId' => session_id(), + 'sessionStatus' => session_status(), + 'totalCachedClients' => count($_SESSION['s3ecCache'] ?? []), + 'allClientIds' => array_keys($_SESSION['s3ecCache'] ?? []), + 'cacheDetails' => $_SESSION['s3ecCache'] ?? [] + ]); +}); + +$router->addRoute('GET', '/object/{bucket}/{key}', function ($params) { + return handleGetObject($params); +}); + +$router->addRoute('PUT', '/object/{bucket}/{key}', function ($params) { + return handlePutObject($params); +}); + +$router->addRoute('POST', '/client', function () { + return handleCreateClient(); +}); + +// Handle the request and output response +$result = $router->handleRequest(); +if ($result !== false) { + echo $result; +} diff --git a/test-server/php-v2-server/src/put_object.php b/test-server/php-v2-server/src/put_object.php new file mode 100644 index 00000000..63058f7d --- /dev/null +++ b/test-server/php-v2-server/src/put_object.php @@ -0,0 +1,72 @@ + 'gcm', + 'KeySize' => 256, + ]; + $legacyConfig = $s3ecClientTuple["legacy"] ?? false; + $legacy = null; + if ($legacyConfig === false) { + $legacy = "V2"; + } else { + $legacy = "V2_AND_LEGACY"; + } + + try { + $result = $s3ec->putObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucket, + 'Key' => $key, + 'Body' => $rawBody, + ]); + + header("Content-Type: application/json"); + return json_encode([ + "bucket" => $bucket, + "key" => $key, + // php for some reason blows java's heap if we pass the metadata + // "metadata" => $encryptionContext + ]); + + } catch (InvalidArgumentException $e) { + return S3EncryptionClientError("Invalid arguement: " . $e->getMessage()); + } catch (Exception $e) { + return GenericServerError("Server error: " . $e->getMessage()); + } +} diff --git a/test-server/php-v3-server/.gitignore b/test-server/php-v3-server/.gitignore new file mode 100644 index 00000000..07108589 --- /dev/null +++ b/test-server/php-v3-server/.gitignore @@ -0,0 +1,4 @@ +vendor/* +cookies.txt +server.pid +composer.lock \ No newline at end of file diff --git a/test-server/php-v3-server/Makefile b/test-server/php-v3-server/Makefile new file mode 100644 index 00000000..d62be452 --- /dev/null +++ b/test-server/php-v3-server/Makefile @@ -0,0 +1,24 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8093 + +start-server: + @echo "Starting PHP V2 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" \ + composer run start & echo $$! > $(PID_FILE) + @echo "PHP V2 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/php-v3-server/README.md b/test-server/php-v3-server/README.md new file mode 100644 index 00000000..284c6e97 --- /dev/null +++ b/test-server/php-v3-server/README.md @@ -0,0 +1,66 @@ +# S3EC PHP v3 Test Server + +This is the PHP V3 implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality. + +## Overview + +The S3ECPhpV3TestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients with session-based caching +- Putting objects with encryption +- Getting and decrypting objects + +## Starting the Server + +### Method 1: Using Composer (Recommended) +```bash +composer run start +``` + +The server will start on port `8093`. + +## Available Endpoints + +### Server Status +- **GET /** - Returns server status and available endpoints + +### Client Management +- **POST /client** - Creates an S3EncryptionClient and caches it with session persistence +- **GET /cache** - Shows current session state and cached clients (for debugging) + +### Object Operations +- **GET /object/{bucket}/{key}** - Handle GET requests using the S3EncryptionClient +- **PUT /object/{bucket}/{key}** - Handle PUT requests using the S3EncryptionClient + +## Testing with curl + +### Important: Session Cookie Management + +To properly test the server and maintain session persistence, you **must** use cookies with curl: + +#### First Request (creates session cookie): +```bash +curl -X POST http://localhost:8093/client \ + -H "Content-Type: application/json" \ + -v +``` + +#### Subsequent Requests (reuses session cookie): +```bash +curl -X POST http://localhost:8093/client \ + -H "Content-Type: application/json" \ + -v +``` + +#### Check Cache Status: +```bash +curl http://localhost:8093/cache \ + -b cookies.txt +``` + +#### Helpful Notes +- **Session Storage**: Client configurations are stored in `$_SESSION['s3ecCache']` +- **Object Recreation**: AWS SDK objects are recreated from stored configuration (they cannot be serialized) +AWS SDK obbjects cannot be serialized due to internal resources and closures. +- **Helper Function**: `getCachedClient($clientId)` retrieves and recreates clients from cache +- **Debugging**: Enhanced logging and `/cache` endpoint for troubleshooting diff --git a/test-server/php-v3-server/composer.json b/test-server/php-v3-server/composer.json new file mode 100644 index 00000000..7ed1daf3 --- /dev/null +++ b/test-server/php-v3-server/composer.json @@ -0,0 +1,24 @@ +{ + "name": "aws/s3ec-php-v2-test-server", + "description": "PHP v2 implementation of the S3EC Test Server framework", + "type": "project", + "license": "Apache-2.0", + "require": { + "php": ">=7.4", + "aws/aws-sdk-php": "^3.356", + "ramsey/uuid": "^4.9" + }, + "autoload": { + "psr-4": { + "S3EC\\PhpV2Server\\": "src/" + } + }, + "scripts": { + "start": [ + "php -S 0.0.0.0:8093 src/index.php" + ] + }, + "config": { + "optimize-autoloader": true + } +} \ No newline at end of file diff --git a/test-server/php-v3-server/src/client.php b/test-server/php-v3-server/src/client.php new file mode 100644 index 00000000..6c40f590 --- /dev/null +++ b/test-server/php-v3-server/src/client.php @@ -0,0 +1,68 @@ +toString(); + $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + + if (empty($configData)) { + return GenericServerError("Invalid config in request body", 400); + } + if (is_null($keyMaterial) || is_null($kmsKeyId)) { + return GenericServerError("Invalid keyMaterial in config", 400); + } + + // Store client configuration instead of objects (AWS objects can't be serialized) + $_SESSION['s3ecCache'][$clientId] = [ + 's3Config' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsConfig' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsKeyId' => $kmsKeyId, + 'legacy' => $legacyAlgorithms, + 'created' => time() + ]; + + // Auto-update cookies.txt with current session ID so tests can access cached clients + writeSessionIdToCookiesFile(session_id()); + + header("Content-Type: application/json"); + return json_encode([ + 'clientId' => $clientId, + ]); +} diff --git a/test-server/php-v3-server/src/errors.php b/test-server/php-v3-server/src/errors.php new file mode 100644 index 00000000..2b59861d --- /dev/null +++ b/test-server/php-v3-server/src/errors.php @@ -0,0 +1,42 @@ + 'GenericServerError', + 'message' => $message + ]; + + return json_encode($errorResponse); +} + +/** + * Used for modeled errors, e.g. errors thrown by the S3EC + * Tests SHOULD expect this error in negative tests. + * + * @param string $message The error message to include in the response + * @return string JSON-encoded error response + */ +function S3EncryptionClientError($message) +{ + http_response_code(500); + header('Content-Type: application/json'); + + $errorResponse = [ + "__type" => "software.amazon.encryption.s3#S3EncryptionClientError", + 'message' => $message + ]; + + return json_encode($errorResponse); +} diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php new file mode 100644 index 00000000..59e2192c --- /dev/null +++ b/test-server/php-v3-server/src/get_object.php @@ -0,0 +1,85 @@ +getObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + 'Bucket' => $bucket, + 'Key' => $key, + ]); + + // Capture and discard any unwanted output from AWS SDK + $unwantedOutput = ob_get_clean(); + if (!empty($unwantedOutput)) { + error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes"); + } + + $body = $result['Body']->getContents(); + $formattedMetadata = formatMetadataForResponse($result["Metadata"]); + + // Now set headers safely + header("Content-Metadata: " . $formattedMetadata); + header("Content-Type: application/octet-stream"); + header("Content-Length: " . strlen($body)); + return $body; + } catch (InvalidArgumentException $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + return GenericServerError("Invalid argument: " . $e->getMessage(), 400); + } catch (Exception $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { + return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); + } else { + return GenericServerError("Server argument: " . $e->getMessage(), 500); + } + } +} diff --git a/test-server/php-v3-server/src/index.php b/test-server/php-v3-server/src/index.php new file mode 100644 index 00000000..cc5dee29 --- /dev/null +++ b/test-server/php-v3-server/src/index.php @@ -0,0 +1,294 @@ += 7 && $parts[5] === 'PHPSESSID') { + error_log("Found session ID in cookies.txt: " . $parts[6]); + return $parts[6]; // Return the session ID value + } + } + + error_log("No PHPSESSID found in cookies.txt file"); + return null; +} + +// Function to write session ID to cookies.txt file +function writeSessionIdToCookiesFile($sessionId) +{ + $cookiesFile = __DIR__ . '/../cookies.txt'; + + // Create Netscape cookie format entry + $cookieLine = "localhost\tFALSE\t/\tFALSE\t0\tPHPSESSID\t$sessionId"; + + // Write header and cookie entry + $content = "# Netscape HTTP Cookie File\n"; + $content .= "# https://curl.se/docs/http-cookies.html\n"; + $content .= "# This file was generated by libcurl! Edit at your own risk.\n\n"; + $content .= $cookieLine . "\n"; + + $result = file_put_contents($cookiesFile, $content); + + if ($result === false) { + error_log("Failed to write session ID to cookies.txt file: $cookiesFile"); + return false; + } + + error_log("Successfully wrote session ID to cookies.txt: $sessionId"); + return true; +} + +set_time_limit(600); +// Start session to persist cache across requests +// First try to use session ID from cookies.txt if available +$sessionId = getSessionIdFromCookiesFile(); +if ($sessionId) { + session_id($sessionId); +} +session_start(); + +// Initialize session cache if it doesn't exist +if (!isset($_SESSION['s3ecCache'])) { + $_SESSION['s3ecCache'] = []; +} + +// Simple router class +class SimpleRouter +{ + private $routes = []; + + public function addRoute($method, $path, $handler) + { + $this->routes[] = [ + 'method' => strtoupper($method), + 'path' => $path, + 'handler' => $handler + ]; + } + + public function handleRequest() + { + $method = $_SERVER['REQUEST_METHOD']; + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + + foreach ($this->routes as $route) { + if ($route['method'] === $method) { + $params = $this->matchPathWithParams($route['path'], $path); + if ($params !== false) { + return call_user_func($route['handler'], $params); + } + } + } + + // Default 404 response + http_response_code(404); + return json_encode(['error' => 'Not Found']); + } + + private function matchPathWithParams($routePath, $requestPath) + { + // Handle exact matches first (for routes without parameters) + if ($routePath === $requestPath) { + return []; + } + + // Convert route path like '/object/{bucket}/{key}' to regex + $pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath); + $pattern = '/^' . str_replace('/', '\/', $pattern) . '$/'; + + if (preg_match($pattern, $requestPath, $matches)) { + array_shift($matches); // Remove full match + + // Extract parameter names + preg_match_all('/\{([^}]+)\}/', $routePath, $paramNames); + $params = []; + + foreach ($paramNames[1] as $index => $paramName) { + $params[$paramName] = $matches[$index] ?? null; + } + + return $params; + } + + return false; + } +} + +// Helper function to get cached client by ID +function getCachedClient($clientId) +{ + if (!isset($_SESSION['s3ecCache'][$clientId])) { + return null; + } + + $config = $_SESSION['s3ecCache'][$clientId]; + + // Recreate the AWS clients from stored configuration + $s3Client = new S3Client($config['s3Config']); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient($config['kmsConfig']); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider, + 'config' => $config + ]; +} + +function createDefaultClientTuple(): array +{ + $s3Client = new S3Client([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider + ]; +} + +function metadataStringToMap($metadata): array +{ + $md = []; + + if (empty($metadata)) { + return $md; + } + + $mdList = explode(',', $metadata); + + foreach ($mdList as $entry) { + $parts = explode(']:[', $entry); + + if (count($parts) === 2) { + $key = substr($parts[0], 1); + $value = substr($parts[1], 0, -1); + $md[$key] = $value; + } else { + throw new InvalidArgumentException("Malformed metadata list entry: " . $entry); + } + } + + return $md; +} +function formatMetadataForResponse($metadata) +{ + $metadataList = []; + // Handle different metadata input types + if (is_array($metadata)) { + // If it's an associative array (like Python dict) + foreach ($metadata as $key => $value) { + $metadataList[] = $key . '=' . $value; + } + } elseif (is_string($metadata) && !empty($metadata)) { + // If it's already a string, assume it's in the correct format + return $metadata; + } + + // Convert array to comma-separated string + return implode(',', $metadataList); +} + +// Initialize router +$router = new SimpleRouter(); + +// Add basic routes +$router->addRoute('GET', '/', function () { + return json_encode([ + 'service' => 'S3EC PHP v2 Test Server', + 'status' => 'running', + 'port' => 8087, + 'endpoints' => [ + 'GET /' => 'Server status', + 'POST /client' => 'Create an S3EncryptionClient and cache it.', + 'GET /object/{bucket}/{key}' => 'Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a GetObject request to S3.', + 'PUT /object/{bucket}/{key}' => 'Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a PutObject request to S3.', + ] + ]); +}); + +$router->addRoute('GET', '/cache', function () { + return json_encode([ + 'sessionId' => session_id(), + 'sessionStatus' => session_status(), + 'totalCachedClients' => count($_SESSION['s3ecCache'] ?? []), + 'allClientIds' => array_keys($_SESSION['s3ecCache'] ?? []), + 'cacheDetails' => $_SESSION['s3ecCache'] ?? [] + ]); +}); + +$router->addRoute('GET', '/object/{bucket}/{key}', function ($params) { + return handleGetObject($params); +}); + +$router->addRoute('PUT', '/object/{bucket}/{key}', function ($params) { + return handlePutObject($params); +}); + +$router->addRoute('POST', '/client', function () { + return handleCreateClient(); +}); + +// Handle the request and output response +$result = $router->handleRequest(); +if ($result !== false) { + echo $result; +} diff --git a/test-server/php-v3-server/src/put_object.php b/test-server/php-v3-server/src/put_object.php new file mode 100644 index 00000000..63058f7d --- /dev/null +++ b/test-server/php-v3-server/src/put_object.php @@ -0,0 +1,72 @@ + 'gcm', + 'KeySize' => 256, + ]; + $legacyConfig = $s3ecClientTuple["legacy"] ?? false; + $legacy = null; + if ($legacyConfig === false) { + $legacy = "V2"; + } else { + $legacy = "V2_AND_LEGACY"; + } + + try { + $result = $s3ec->putObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucket, + 'Key' => $key, + 'Body' => $rawBody, + ]); + + header("Content-Type: application/json"); + return json_encode([ + "bucket" => $bucket, + "key" => $key, + // php for some reason blows java's heap if we pass the metadata + // "metadata" => $encryptionContext + ]); + + } catch (InvalidArgumentException $e) { + return S3EncryptionClientError("Invalid arguement: " . $e->getMessage()); + } catch (Exception $e) { + return GenericServerError("Server error: " . $e->getMessage()); + } +} From ac556cb7bc68fa78871555eaba056edf7c0f6650 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Fri, 19 Sep 2025 15:46:24 -0700 Subject: [PATCH 053/201] Add not implemented feature --- .../Controllers/ClientController.cs | 20 +++++++++++++++---- .../net-v2-v3-server/Models/ClientRequest.cs | 10 +++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index ad4f0e1e..eff4602a 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -14,17 +14,29 @@ public class ClientController(IClientCacheService clientCacheService, ILogger(); var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); logger.LogInformation( - "Created EncryptionMaterialsV2: KMS={KmsKeyId}, Encryption Context={EncryptionContext}", - kmsKeyId, encryptionContext); + "Created EncryptionMaterialsV2: KMS={KmsKeyId}", + kmsKeyId); // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index cbc1ae0e..6882b4f9 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -10,15 +10,19 @@ public class ClientRequest public class ClientConfig { - public Dictionary EncryptionContext { get; set; } = new(); - public bool EnableLegacyUnauthenticatedModes { get; set; } - public bool EnableLegacyWrappingAlgorithms { get; set; } + 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(); } public class KeyMaterial { + public byte[]? RsaKey { get; set; } + public byte[]? AesKey { get; set; } + [Required] public string KmsKeyId { get; set; } = string.Empty; } \ No newline at end of file From 46ae0231321e05270b7929528239016aba358c10 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 19 Sep 2025 15:53:34 -0700 Subject: [PATCH 054/201] Update test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java --- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 cf7847b6..b6ecc6a6 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 @@ -87,8 +87,8 @@ public class RoundTripTests { serverMap.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); serverMap.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); serverMap.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - serverMap.put(PHP_V2, new LanguageServerTarget("PHP-V2", "8087")); - serverMap.put(PHP_V3, new LanguageServerTarget("PHP-V3", "8093")); + serverMap.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); + serverMap.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); } // Encryption context validation behavior varies by implementation: From 0b1871140424123b79dfe4358d5578c78a1b9ff7 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 19 Sep 2025 15:56:24 -0700 Subject: [PATCH 055/201] Update test-server/net-v2-v3-server/Controllers/ClientController.cs --- test-server/net-v2-v3-server/Controllers/ClientController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index eff4602a..01bf610c 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -30,7 +30,8 @@ public IActionResult CreateClient([FromBody] ClientRequest request) try { - // The POST request does not contain encryption context. However, encryption context is a required field for KMS. + // 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 encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); From b047b11f476e0c4c769a098d5cab747a15edc05d Mon Sep 17 00:00:00 2001 From: seebees Date: Sun, 21 Sep 2025 14:49:03 -0700 Subject: [PATCH 056/201] Initial commit of Ruby V2 and V3 test server (#5) This includes both Ruby test servers. I also updated the RoundTripTests.java, to work with specific Ruby error messages. As well as simplified the server list into only a map. And added a case insensitive commas separated filter to run only a subset of tests. --- .github/workflows/test.yml | 10 +- .gitmodules | 6 + test-server/Makefile | 32 ++- test-server/java-tests/build.gradle.kts | 11 + .../amazon/encryption/s3/RoundTripTests.java | 82 +++++-- test-server/ruby-v2-server/.gitignore | 2 + test-server/ruby-v2-server/Gemfile | 15 ++ test-server/ruby-v2-server/Gemfile.lock | 102 ++++++++ test-server/ruby-v2-server/Makefile | 29 +++ test-server/ruby-v2-server/README.md | 73 ++++++ test-server/ruby-v2-server/app.rb | 231 ++++++++++++++++++ test-server/ruby-v2-server/config.ru | 3 + .../ruby-v2-server/lib/client_manager.rb | 74 ++++++ .../ruby-v2-server/lib/error_handlers.rb | 42 ++++ test-server/ruby-v2-server/lib/logger.rb | 105 ++++++++ .../ruby-v2-server/lib/metadata_utils.rb | 50 ++++ test-server/ruby-v2-server/local-ruby-sdk | 1 + test-server/ruby-v3-server/.bundle/config | 2 + test-server/ruby-v3-server/.gitignore | 2 + test-server/ruby-v3-server/Gemfile | 15 ++ test-server/ruby-v3-server/Gemfile.lock | 102 ++++++++ test-server/ruby-v3-server/Makefile | 29 +++ test-server/ruby-v3-server/README.md | 74 ++++++ test-server/ruby-v3-server/app.rb | 231 ++++++++++++++++++ test-server/ruby-v3-server/config.ru | 3 + .../ruby-v3-server/lib/client_manager.rb | 74 ++++++ .../ruby-v3-server/lib/error_handlers.rb | 42 ++++ test-server/ruby-v3-server/lib/logger.rb | 105 ++++++++ .../ruby-v3-server/lib/metadata_utils.rb | 50 ++++ test-server/ruby-v3-server/local-ruby-sdk | 1 + 30 files changed, 1568 insertions(+), 30 deletions(-) create mode 100644 .gitmodules create mode 100644 test-server/ruby-v2-server/.gitignore create mode 100644 test-server/ruby-v2-server/Gemfile create mode 100644 test-server/ruby-v2-server/Gemfile.lock create mode 100644 test-server/ruby-v2-server/Makefile create mode 100644 test-server/ruby-v2-server/README.md create mode 100644 test-server/ruby-v2-server/app.rb create mode 100644 test-server/ruby-v2-server/config.ru create mode 100644 test-server/ruby-v2-server/lib/client_manager.rb create mode 100644 test-server/ruby-v2-server/lib/error_handlers.rb create mode 100644 test-server/ruby-v2-server/lib/logger.rb create mode 100644 test-server/ruby-v2-server/lib/metadata_utils.rb create mode 160000 test-server/ruby-v2-server/local-ruby-sdk create mode 100644 test-server/ruby-v3-server/.bundle/config create mode 100644 test-server/ruby-v3-server/.gitignore create mode 100644 test-server/ruby-v3-server/Gemfile create mode 100644 test-server/ruby-v3-server/Gemfile.lock create mode 100644 test-server/ruby-v3-server/Makefile create mode 100644 test-server/ruby-v3-server/README.md create mode 100644 test-server/ruby-v3-server/app.rb create mode 100644 test-server/ruby-v3-server/config.ru create mode 100644 test-server/ruby-v3-server/lib/client_manager.rb create mode 100644 test-server/ruby-v3-server/lib/error_handlers.rb create mode 100644 test-server/ruby-v3-server/lib/logger.rb create mode 100644 test-server/ruby-v3-server/lib/metadata_utils.rb create mode 160000 test-server/ruby-v3-server/local-ruby-sdk diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0639f45a..180ed6de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,11 +20,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version || '3.11' }} + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' - name: Set up PHP with Composer uses: shivammathur/setup-php@v2 @@ -61,7 +69,7 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - test-server/java-server/.gradle + test-server/java-v3-server/.gradle test-server/java-tests/.gradle key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..b2b112a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "test-server/ruby-v2-server/local-ruby-sdk"] + path = test-server/ruby-v2-server/local-ruby-sdk + url = git@github.com:aws/aws-sdk-ruby-staging.git +[submodule "test-server/ruby-v3-server/local-ruby-sdk"] + path = test-server/ruby-v3-server/local-ruby-sdk + url = git@github.com:aws/aws-sdk-ruby-staging.git diff --git a/test-server/Makefile b/test-server/Makefile index 90d15648..f23b738d 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -10,7 +10,8 @@ ci: start-servers run-tests stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) +START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) +WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) # Start all servers in parallel start-servers: @@ -22,14 +23,25 @@ start-servers: $(MAKE) -C $$dir wait-for-server; \ done -start-all-servers: $(SERVER_TARGETS) +start-all-servers: $(START_SERVER_TARGETS) -$(SERVER_TARGETS): start-%: +$(START_SERVER_TARGETS): start-%: @if [ -f $*/Makefile ]; then \ echo "Starting server in $*..."; \ $(MAKE) -C $* start-server; \ else \ - echo "❌ Error: no Makefile found in $$dir"; \ + echo "❌ Error: no Makefile found in $*"; \ + exit 1; \ + fi; \ + +wait-all-servers: $(WAIT_SERVER_TARGETS) + +$(WAIT_SERVER_TARGETS): wait-%: + @if [ -f $*/Makefile ]; then \ + echo "Waiting server in $*..."; \ + $(MAKE) -C $* wait-for-server; \ + else \ + echo "❌ Error: no Makefile found in $*"; \ exit 1; \ fi; \ @@ -44,7 +56,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 --rerun-tasks --parallel integ + ./gradlew --build-cache --parallel integ -Dtest.filter.servers="$(FILTER)" @echo "Tests completed successfully" # Stop the servers @@ -96,3 +108,13 @@ wait-for-port: echo "Waiting for $$PORT to start ($$i/$(TIMEOUT))..."; \ sleep 1; \ done + +# Here are some helpful curl commands +# that you can use to test specific test servers: +test-create-client: + @echo $(PORT) + @curl -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":false,"enableDelayedAuthenticationMode":false,"enableLegacyWrappingAlgorithms":false,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}}}' \ + http://localhost:$(PORT)/client \ No newline at end of file diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 813e8369..bc37514f 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -48,6 +48,17 @@ tasks { classpath = sourceSets["it"].runtimeClasspath outputs.upToDateWhen { false } outputs.cacheIf { false } + // 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 + // // Enable System.out output + // testLogging { + // events("passed", "skipped", "failed", "standardOut", "standardError") + // showStandardStreams = true + // } + + // // Disable AWS SDK v1 deprecation warnings + // systemProperty("aws.java.v1.disableDeprecationAnnouncement", "true") } } 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 b6ecc6a6..459bbbd3 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 @@ -14,11 +14,14 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.amazonaws.services.s3.model.KMSEncryptionMaterials; @@ -61,8 +64,9 @@ public class RoundTripTests { private static final String NET_V3 = "NET-V3"; private static final String PHP_V2 = "PHP-V2"; private static final String PHP_V3 = "PHP-V3"; + private static final String RUBY_V2 = "Ruby-V2"; + private static final String RUBY_V3 = "Ruby-V3"; - private static final List serverList; private static final Map serverMap; private static final String KMS_KEY_ARN = System.getenv("TEST_SERVER_KMS_KEY_ARN") != null ? @@ -72,25 +76,45 @@ public class RoundTripTests { System.getenv("TEST_SERVER_S3_BUCKET") : "s3ec-test-server-github-bucket"; static { - serverList = new ArrayList<>(14); - serverList.add(new LanguageServerTarget(JAVA_V3, "8080")); - serverList.add(new LanguageServerTarget(PYTHON_V3, "8081")); - serverList.add(new LanguageServerTarget(GO_V3, "8082")); - serverList.add(new LanguageServerTarget(NET_V2, "8083")); - serverList.add(new LanguageServerTarget(NET_V3, "8084")); - serverList.add(new LanguageServerTarget(PHP_V2, "8087")); - serverList.add(new LanguageServerTarget(PHP_V3, "8093")); - - serverMap = new HashMap<>(14); - serverMap.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); - serverMap.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); - serverMap.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); - serverMap.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); - serverMap.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - serverMap.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); - serverMap.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); + final Map servers = new LinkedHashMap<>(); + servers.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); + servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); + servers.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); + servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); + servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); + servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); + servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); + servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); + servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + + serverMap = filterServers(servers); } + private static Map filterServers(Map allServers) { + + final String maybeFilter = System.getProperty("test.filter.servers"); + if (maybeFilter == null || maybeFilter.trim().isEmpty()) { + return allServers; // No filtering - use all servers + } + + final String[] filters = Arrays.stream(maybeFilter.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .toArray(String[]::new); + + return allServers.entrySet().stream() + .filter(entry -> { + String key = entry.getKey().toLowerCase(); + return Arrays.stream(filters).anyMatch(key::contains); + }) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, // merge function (not really needed) + LinkedHashMap::new // preserve order + )); + } + // Encryption context validation behavior varies by implementation: // - Go: Does not validate encryption context on decrypt operations // - .NET: Only validates against encryption context stored in the object metadata @@ -149,7 +173,7 @@ public String toString() { @BeforeAll public static void setup() { // Wait for servers to start - for (LanguageServerTarget server : serverList) { + for (LanguageServerTarget server : serverMap.values()) { if (!serverListening(server.getServerURI())) { throw new RuntimeException(String.format("Test Server for %s is not running at endpoint: %s", server.getLanguageName(), server.getServerURI())); } @@ -179,14 +203,14 @@ static S3ECTestServerClient testServerClientFor(LanguageServerTarget server) { } static Stream clientsForTest() { - return serverList.stream() + return serverMap.values().stream() .map(LanguageServerTarget::getLanguageName) .map(Arguments::of); } static Stream crossLanguageClients() { - return serverList.stream() - .flatMap(t1 -> serverList.stream() + return serverMap.values().stream() + .flatMap(t1 -> serverMap.values().stream() .flatMap(t2 -> Stream.of( Arguments.of(t1, t2) ))); @@ -337,7 +361,11 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + if (decLang.languageName.equals(RUBY_V3) || decLang.languageName.equals(RUBY_V2)) { + assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); + } else { + assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + } } } @@ -389,7 +417,11 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + if (decLang.languageName.equals(RUBY_V3) || decLang.languageName.equals(RUBY_V2)) { + assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); + } else { + assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + } } } @@ -529,6 +561,8 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration V2." )); + } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { + 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.")); } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); } diff --git a/test-server/ruby-v2-server/.gitignore b/test-server/ruby-v2-server/.gitignore new file mode 100644 index 00000000..d20a29c0 --- /dev/null +++ b/test-server/ruby-v2-server/.gitignore @@ -0,0 +1,2 @@ +vendor +server.pid \ No newline at end of file diff --git a/test-server/ruby-v2-server/Gemfile b/test-server/ruby-v2-server/Gemfile new file mode 100644 index 00000000..2759a87b --- /dev/null +++ b/test-server/ruby-v2-server/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' + +ruby '~> 3.0' + +gem 'sinatra', '~> 3.0' +gem 'puma', '~> 6.0' +gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' +gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' +gem 'json', '~> 2.0' +gem 'concurrent-ruby', '~> 1.0' +gem 'nokogiri', '~> 1.13' + +group :development do + gem 'rubocop', '~> 1.0' +end diff --git a/test-server/ruby-v2-server/Gemfile.lock b/test-server/ruby-v2-server/Gemfile.lock new file mode 100644 index 00000000..660aadd5 --- /dev/null +++ b/test-server/ruby-v2-server/Gemfile.lock @@ -0,0 +1,102 @@ +PATH + remote: local-ruby-sdk/gems/aws-sdk-kms + specs: + aws-sdk-kms (1.112.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + +PATH + remote: local-ruby-sdk/gems/aws-sdk-s3 + specs: + aws-sdk-s3 (1.199.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1160.0) + aws-sdk-core (3.232.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bigdecimal (3.2.3) + concurrent-ruby (1.3.5) + jmespath (1.6.2) + json (2.13.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + mustermann (3.0.4) + ruby2_keywords (~> 0.0.1) + nio4r (2.7.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + prism (1.5.1) + puma (6.6.1) + nio4r (~> 2.0) + racc (1.8.1) + rack (2.2.17) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rainbow (3.1.1) + regexp_parser (2.11.3) + rubocop (1.80.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sinatra (3.2.0) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.2.0) + tilt (~> 2.0) + tilt (2.6.1) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + +PLATFORMS + arm64-darwin-24 + +DEPENDENCIES + aws-sdk-kms! + aws-sdk-s3! + concurrent-ruby (~> 1.0) + json (~> 2.0) + nokogiri (~> 1.13) + puma (~> 6.0) + rubocop (~> 1.0) + sinatra (~> 3.0) + +RUBY VERSION + ruby 3.4.5p51 + +BUNDLED WITH + 2.6.9 diff --git a/test-server/ruby-v2-server/Makefile b/test-server/ruby-v2-server/Makefile new file mode 100644 index 00000000..5d552aac --- /dev/null +++ b/test-server/ruby-v2-server/Makefile @@ -0,0 +1,29 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8086 + +start-server: + @if [ -f $(PID_FILE) ]; then \ + echo "❌ Error: Server already running. Stop before starting."; \ + exit 1; \ + fi; + @echo "Starting Ruby V2 server..." + bundle install + 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" \ + bundle exec ruby app.rb & echo $$! > server.pid + @echo "Ruby V2 server starting..." + +stop-server: + @if [ -f server.pid ]; then \ + kill $$(cat server.pid) 2>/dev/null || true; \ + rm server.pid; \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=8086 \ No newline at end of file diff --git a/test-server/ruby-v2-server/README.md b/test-server/ruby-v2-server/README.md new file mode 100644 index 00000000..4b3e5209 --- /dev/null +++ b/test-server/ruby-v2-server/README.md @@ -0,0 +1,73 @@ +# Ruby S3 Encryption Client Test Server + +This is a Ruby implementation of the S3 Encryption Client test server +that provides an invariant interface around the S3 Encryption Client v2. +It's designed to work alongside other implementations of test servers for cross-language compatibility testing. + +## Overview + +The server provides a REST API that wraps the AWS S3 Encryption Client v2, +allowing tests to verify that all language implementations behave consistently. + +## Endpoints + +- `POST /client` - Create a new S3 encryption client instance +- `PUT /object/{bucket}/{key}` - Encrypt and store an object +- `GET /object/{bucket}/{key}` - Retrieve and decrypt an object +- `GET /health` - Health check endpoint + +## Configuration + +The server runs on port **8086** by default. + +## Setup + +1. Install Ruby 3.x +2. Install dependencies: + + ```bash + cd test-server/ruby-v2-server + bundle install + ``` + +3. Set up AWS credentials (via AWS CLI, environment variables, or IAM roles) + +4. Start the server: + + ```bash + ruby app.rb + # or using Rack + bundle exec rackup -p 8086 + ``` + +## Usage + +The server is designed to be used by the Java test suite in `test-server/java-tests/`. +The tests will automatically discover and use this server for cross-language compatibility testing. + +### Environment Variables + +- `TEST_SERVER_KMS_KEY_ARN` - KMS key ARN for encryption (defaults to test key) +- `TEST_SERVER_S3_BUCKET` - S3 bucket for testing (defaults to test bucket) + +## Architecture + +- `app.rb` - Main Sinatra application +- `lib/client_manager.rb` - Manages S3 encryption client instances +- `lib/metadata_utils.rb` - Handles metadata serialization/deserialization +- `lib/error_handlers.rb` - Smithy-compliant error responses + +## Error Handling + +The server returns errors in the format expected by the Smithy model: + +- `GenericServerError` - Internal server errors +- `S3EncryptionClientError` - Errors from the S3 Encryption Client + +## Compatibility + +This server is compatible with: + +- S3 Encryption Client v2 +- Legacy v1 clients (when `enableLegacyWrappingAlgorithms` is true) +- Cross-language testing with other implementations diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb new file mode 100644 index 00000000..6096a5ea --- /dev/null +++ b/test-server/ruby-v2-server/app.rb @@ -0,0 +1,231 @@ +require 'sinatra' +require 'json' +require_relative 'lib/client_manager' +require_relative 'lib/metadata_utils' +require_relative 'lib/error_handlers' +require_relative 'lib/logger' + +class S3ECRubyServer < Sinatra::Base + configure do + set :port, 8086 + set :bind, '0.0.0.0' + set :show_exceptions, false + set :raise_errors, false + end + + def initialize + super + @client_manager = ClientManager.new + S3ECLogger.info("S3EC_SERVER: Ruby V2 server initialized on port #{settings.port}") + end + + # Request logging middleware + before do + @request_id = S3ECLogger.generate_request_id + S3ECLogger.log_request(request.request_method, request.path_info, request.env, @request_id) + end + + # Response logging middleware + after do + S3ECLogger.log_response(response.status, @request_id) + end + + # Health check endpoint + get '/health' do + content_type :json + { status: 'OK', server: 'Ruby V2 S3EC Test Server', port: settings.port.to_i }.to_json + end + + # POST /client - Create S3 encryption client + post '/client' do + begin + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Processing client creation request") + + # Parse request body + request_body = request.body.read + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Request body size: #{request_body.length} bytes") + + parsed_data = JSON.parse(request_body) + config = parsed_data['config'] || {} + + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Parsed config: #{config.inspect}") + + # Create client using client manager + client_id = @client_manager.create_client(config) + + S3ECLogger.info("CLIENT_ENDPOINT [#{@request_id}]: Successfully created client #{client_id}") + + # Return client ID + content_type :json + { clientId: client_id }.to_json + + rescue JSON::ParserError => e + S3ECLogger.log_error(e, { endpoint: '/client', operation: 'JSON parsing' }, @request_id) + ErrorHandlers.send_generic_server_error(self, "Invalid JSON in request body", 400) + rescue => e + S3ECLogger.log_error(e, { endpoint: '/client', operation: 'client creation', config: config }, @request_id) + ErrorHandlers.send_s3_encryption_client_error(self, "Failed to create client: #{e.message}") + end + end + + # PUT /object/{bucket}/{key} - Encrypt and put object + put '/object/:bucket/:key' do + bucket = params[:bucket] + key = params[:key] + client_id = request.env['HTTP_CLIENTID'] + + begin + S3ECLogger.debug("PUT_ENDPOINT [#{@request_id}]: Processing PUT request for s3://#{bucket}/#{key}") + + # Validate client ID + unless client_id + S3ECLogger.log_validation_error('ClientID', 'missing', @request_id) + ErrorHandlers.send_generic_server_error(self, "ClientID header is required", 400) + end + + # Get client from cache + client = @client_manager.get_client(client_id) + unless client + S3ECLogger.log_validation_error('ClientID', client_id, @request_id) + ErrorHandlers.send_generic_server_error(self, "No client found for ClientID: #{client_id}", 404) + end + + # Get request body + body = request.body.read + S3ECLogger.debug("PUT_ENDPOINT [#{@request_id}]: Request body size: #{body.length} bytes") + + # Parse metadata from header + metadata_header = request.env['HTTP_CONTENT_METADATA'] || '' + encryption_context = MetadataUtils.string_to_map(metadata_header) + S3ECLogger.log_metadata_processing('parse', metadata_header, encryption_context) + + # Prepare S3 put_object parameters + put_params = { + bucket: bucket, + key: key, + body: body + } + + # Add encryption context if present + put_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + + # Log S3 operation + S3ECLogger.log_s3_operation('put', bucket, key, encryption_context, "ClientID: #{client_id}, BodySize: #{body.length}") + + # Make the put_object request + response = client.put_object(put_params) + + S3ECLogger.info("PUT_ENDPOINT [#{@request_id}]: Successfully put object s3://#{bucket}/#{key}") + + # Prepare response metadata + response_metadata = MetadataUtils.map_to_array(encryption_context) + S3ECLogger.log_metadata_processing('response', encryption_context, response_metadata) + + # Return response matching Smithy model + content_type :json + { + bucket: bucket, + key: key, + metadata: response_metadata + }.to_json + + rescue Aws::S3::EncryptionV2::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 + # Handle generic server errors (return as GenericServerError) + S3ECLogger.log_error(e, { endpoint: '/put', error_category: 'generic_server' }, @request_id) + status_code = e.respond_to?(:code) ? e.code : 500 + ErrorHandlers.send_generic_server_error(self, e.message, status_code) + end + end + + # GET /object/{bucket}/{key} - Get and decrypt object + get '/object/:bucket/:key' do + bucket = params[:bucket] + key = params[:key] + client_id = request.env['HTTP_CLIENTID'] + + begin + S3ECLogger.debug("GET_ENDPOINT [#{@request_id}]: Processing GET request for s3://#{bucket}/#{key}") + + # Validate client ID + unless client_id + S3ECLogger.log_validation_error('ClientID', 'missing', @request_id) + ErrorHandlers.send_generic_server_error(self, "ClientID header is required", 400) + end + + # Get client from cache + client = @client_manager.get_client(client_id) + unless client + S3ECLogger.log_validation_error('ClientID', client_id, @request_id) + ErrorHandlers.send_generic_server_error(self, "No client found for ClientID: #{client_id}", 404) + end + + # Parse metadata from header + metadata_header = request.env['HTTP_CONTENT_METADATA'] || '' + encryption_context = MetadataUtils.string_to_map(metadata_header) + S3ECLogger.log_metadata_processing('parse', metadata_header, encryption_context) + + # Prepare S3 get_object parameters + get_params = { + bucket: bucket, + key: key + } + + # Add encryption context if present + get_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + + # Log S3 operation + S3ECLogger.log_s3_operation('get', bucket, key, encryption_context, "ClientID: #{client_id}") + + # Make the get_object request + response = client.get_object(get_params) + + # Extract body and metadata + body = response.body.read + metadata = response.metadata || {} + + S3ECLogger.info("GET_ENDPOINT [#{@request_id}]: Successfully got object s3://#{bucket}/#{key}, BodySize: #{body.length}") + + # Set Content-Metadata header in response + metadata_str = MetadataUtils.map_to_string(metadata) + S3ECLogger.log_metadata_processing('response', metadata, metadata_str) + + headers['Content-Metadata'] = metadata_str unless metadata_str.empty? + + # Return the body as response + content_type 'application/octet-stream' + body + + rescue Aws::S3::EncryptionV2::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 + # Handle generic server errors (return as GenericServerError) + S3ECLogger.log_error(e, { endpoint: '/get', error_category: 'generic_server' }, @request_id) + status_code = e.respond_to?(:code) ? e.code : 500 + ErrorHandlers.send_generic_server_error(self, e.message, status_code) + end + end + + # Global error handler + error do + error = env['sinatra.error'] + context = { + endpoint: request.path_info, + method: request.request_method, + params: params, + error_type: 'global_error_handler' + } + + S3ECLogger.log_error(error, context, @request_id) + ErrorHandlers.send_generic_server_error(self, "Internal server error: #{error.message}") + end + + # Start server when run directly + if __FILE__ == $0 + S3ECLogger.info("S3EC_SERVER: Starting Ruby server on port #{settings.port}...") + run! + end +end diff --git a/test-server/ruby-v2-server/config.ru b/test-server/ruby-v2-server/config.ru new file mode 100644 index 00000000..99d3a689 --- /dev/null +++ b/test-server/ruby-v2-server/config.ru @@ -0,0 +1,3 @@ +require_relative 'app' + +run S3ECRubyServer diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb new file mode 100644 index 00000000..d3b12b23 --- /dev/null +++ b/test-server/ruby-v2-server/lib/client_manager.rb @@ -0,0 +1,74 @@ +require 'concurrent-ruby' +require 'securerandom' +require 'aws-sdk-s3' +require 'aws-sdk-kms' +require_relative 'logger' + +# Manages S3 Encryption Client instances +class ClientManager + def initialize + @client_cache = Concurrent::Hash.new + @kms_client = Aws::KMS::Client.new(region: 'us-west-2') + S3ECLogger.info("CLIENT_MANAGER: Initialized with KMS client for us-west-2") + end + + # Create a new S3 encryption client and return its ID + def create_client(config) + # Extract configuration + kms_key_id = config.dig('keyMaterial', 'kmsKeyId') + enable_legacy_wrapping = config['enableLegacyWrappingAlgorithms'] || false + + 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, + content_encryption_schema: :aes_gcm_no_padding, + # Set security profile based on legacy wrapping algorithms setting + security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 + } + + # Create the S3 encryption client + s3_client = Aws::S3::Client.new(region: 'us-west-2') + encryption_client = Aws::S3::EncryptionV2::Client.new( + client: s3_client, + **encryption_config + ) + + # Generate client ID and store in cache + client_id = SecureRandom.uuid + @client_cache[client_id] = encryption_client + + # Log client creation + S3ECLogger.log_client_creation(config, client_id) + S3ECLogger.log_cache_stats(@client_cache.size) + + client_id + end + + # Get a client by ID + def get_client(client_id) + client = @client_cache[client_id] + if client + S3ECLogger.log_client_cache_hit(client_id) + else + S3ECLogger.log_client_cache_miss(client_id) + end + client + end + + # Remove a client from cache (optional cleanup) + def remove_client(client_id) + removed = @client_cache.delete(client_id) + S3ECLogger.info("CLIENT_CACHE: Removed client #{client_id} from cache") if removed + S3ECLogger.log_cache_stats(@client_cache.size) + removed + end + + # Get cache size (for debugging) + def cache_size + @client_cache.size + end +end diff --git a/test-server/ruby-v2-server/lib/error_handlers.rb b/test-server/ruby-v2-server/lib/error_handlers.rb new file mode 100644 index 00000000..234a9a55 --- /dev/null +++ b/test-server/ruby-v2-server/lib/error_handlers.rb @@ -0,0 +1,42 @@ +# Error handling utilities to match Smithy error types +require 'json' + +class ErrorHandlers + # Create a response that matches the GenericServerError type from the Smithy model + # Used for internal server errors + def self.create_generic_server_error(message, status_code = 500) + { + status: status_code, + headers: { 'Content-Type' => 'application/json' }, + body: { + '__type' => 'software.amazon.encryption.s3#GenericServerError', + 'message' => message + }.to_json + } + end + + # Create a response that matches the S3EncryptionClientError type from the Smithy model + # Used for errors thrown by the S3 Encryption Client + def self.create_s3_encryption_client_error(message, status_code = 500) + { + status: status_code, + headers: { 'Content-Type' => 'application/json' }, + body: { + '__type' => 'software.amazon.encryption.s3#S3EncryptionClientError', + 'message' => message + }.to_json + } + end + + # Helper method to send error response in Sinatra + def self.send_generic_server_error(app, message, status_code = 500) + error_response = create_generic_server_error(message, status_code) + app.halt error_response[:status], error_response[:headers], error_response[:body] + end + + # Helper method to send S3EC error response in Sinatra + def self.send_s3_encryption_client_error(app, message, status_code = 500) + error_response = create_s3_encryption_client_error(message, status_code) + app.halt error_response[:status], error_response[:headers], error_response[:body] + end +end diff --git a/test-server/ruby-v2-server/lib/logger.rb b/test-server/ruby-v2-server/lib/logger.rb new file mode 100644 index 00000000..2febcbab --- /dev/null +++ b/test-server/ruby-v2-server/lib/logger.rb @@ -0,0 +1,105 @@ +require 'logger' +require 'securerandom' + +# Centralized logging utility for the S3EC Ruby server +class S3ECLogger + def self.instance + @instance ||= new + end + + def initialize + @logger = Logger.new(STDOUT) + @logger.level = ENV['LOG_LEVEL'] ? Logger.const_get(ENV['LOG_LEVEL'].upcase) : Logger::INFO + @logger.formatter = proc do |severity, datetime, progname, msg| + "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" + end + end + + # Generate a unique request ID for correlation + def self.generate_request_id + SecureRandom.hex(8) + end + + # Request/Response logging + def self.log_request(method, path, headers = {}, request_id = nil) + client_id = headers['HTTP_CLIENTID'] || headers['ClientID'] || 'none' + content_metadata = headers['HTTP_CONTENT_METADATA'] || headers['Content-Metadata'] || 'none' + + instance.logger.info("REQUEST [#{request_id}] #{method} #{path} | ClientID: #{client_id} | Metadata: #{content_metadata}") + end + + def self.log_response(status, request_id = nil, additional_info = "") + info_str = additional_info.empty? ? "" : " | #{additional_info}" + instance.logger.info("RESPONSE [#{request_id}] Status: #{status}#{info_str}") + end + + # Operation-level logging + def self.log_client_creation(config, client_id) + kms_key = config.dig('keyMaterial', 'kmsKeyId') || 'unknown' + legacy_enabled = config['enableLegacyWrappingAlgorithms'] || false + instance.logger.info("CLIENT_CREATION: Created S3EC client #{client_id} | KMS Key: #{kms_key} | Legacy: #{legacy_enabled}") + end + + def self.log_client_cache_hit(client_id) + instance.logger.debug("CACHE_HIT: Found client #{client_id} in cache") + end + + def self.log_client_cache_miss(client_id) + instance.logger.warn("CACHE_MISS: Client #{client_id} not found in cache") + end + + def self.log_cache_stats(cache_size) + instance.logger.debug("CACHE_STATS: Current client cache size: #{cache_size}") + end + + def self.log_s3_operation(operation, bucket, key, encryption_context = {}, additional_info = "") + enc_ctx_str = encryption_context.empty? ? "none" : encryption_context.inspect + info_str = additional_info.empty? ? "" : " | #{additional_info}" + instance.logger.info("S3_OPERATION: #{operation.upcase} s3://#{bucket}/#{key} | EncCtx: #{enc_ctx_str}#{info_str}") + end + + def self.log_metadata_processing(operation, input, output) + instance.logger.debug("METADATA_#{operation.upcase}: Input: #{input.inspect} | Output: #{output.inspect}") + end + + # Enhanced error logging + def self.log_error(error, context = {}, request_id = nil) + error_context = context.empty? ? "" : " | Context: #{context.inspect}" + instance.logger.error("ERROR [#{request_id}] #{error.class}: #{error.message}#{error_context}") + + if error.backtrace && instance.debug? + instance.logger.debug("ERROR_BACKTRACE [#{request_id}]:\n#{error.backtrace.join("\n")}") + end + end + + def self.log_validation_error(field, value, request_id = nil) + instance.logger.warn("VALIDATION_ERROR [#{request_id}] Invalid #{field}: #{value}") + end + + def self.log_aws_error(error, operation, request_id = nil) + instance.logger.error("AWS_ERROR [#{request_id}] #{operation} failed: #{error.class} - #{error.message}") + end + + # Standard logging methods + def self.debug(message) + instance.logger.debug(message) + end + + def self.info(message) + instance.logger.info(message) + end + + def self.warn(message) + instance.logger.warn(message) + end + + def self.error(message) + instance.logger.error(message) + end + + attr_reader :logger + + def debug? + @logger.debug? + end +end diff --git a/test-server/ruby-v2-server/lib/metadata_utils.rb b/test-server/ruby-v2-server/lib/metadata_utils.rb new file mode 100644 index 00000000..72015fcc --- /dev/null +++ b/test-server/ruby-v2-server/lib/metadata_utils.rb @@ -0,0 +1,50 @@ +# Utility class for handling metadata serialization/deserialization +# Matches the format used by Java and Python servers: [key]:[value],[key2]:[value2] +class MetadataUtils + # Convert metadata string to hash + # Input: "[user-defined-enc-ctx-key]:[user-defined-enc-ctx-value],[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]" + # Output: {"user-defined-enc-ctx-key" => "user-defined-enc-ctx-value", "user-defined-enc-ctx-key-2" => "user-defined-enc-ctx-value-2"} + def self.string_to_map(metadata_string) + return {} if metadata_string.nil? || metadata_string.empty? + + metadata = {} + entries = metadata_string.split(',') + + entries.each do |entry| + # Split on "]:[" to separate key and value + parts = entry.split(']:[') + if parts.length == 2 + # Remove remaining brackets from start and end + key = parts[0].delete_prefix("[") # Remove first character '[' + value = parts[1].delete_suffix("]") # Remove last character ']' + metadata[key] = value + else + raise "Malformed metadata list entry: #{entry}" + end + end + + metadata + end + + # Convert hash to metadata string + # Input: {"user-defined-enc-ctx-key" => "user-defined-enc-ctx-value", "user-defined-enc-ctx-key-2" => "user-defined-enc-ctx-value-2"} + # Output: "[user-defined-enc-ctx-key]:[user-defined-enc-ctx-value],[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]" + def self.map_to_string(metadata_hash) + return '' if metadata_hash.nil? || metadata_hash.empty? + + entries = metadata_hash.map do |key, value| + "[#{key}]:[#{value}]" + end + + entries.join(',') + end + + # Convert metadata hash to array format (for JSON responses) + def self.map_to_array(metadata_hash) + return [] if metadata_hash.nil? || metadata_hash.empty? + + metadata_hash.map do |key, value| + "[#{key}]:[#{value}]" + end + end +end diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk new file mode 160000 index 00000000..e129cf37 --- /dev/null +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -0,0 +1 @@ +Subproject commit e129cf37c170254ebb631782cae145040cde6d0b diff --git a/test-server/ruby-v3-server/.bundle/config b/test-server/ruby-v3-server/.bundle/config new file mode 100644 index 00000000..23692288 --- /dev/null +++ b/test-server/ruby-v3-server/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_PATH: "vendor/bundle" diff --git a/test-server/ruby-v3-server/.gitignore b/test-server/ruby-v3-server/.gitignore new file mode 100644 index 00000000..d20a29c0 --- /dev/null +++ b/test-server/ruby-v3-server/.gitignore @@ -0,0 +1,2 @@ +vendor +server.pid \ No newline at end of file diff --git a/test-server/ruby-v3-server/Gemfile b/test-server/ruby-v3-server/Gemfile new file mode 100644 index 00000000..2759a87b --- /dev/null +++ b/test-server/ruby-v3-server/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' + +ruby '~> 3.0' + +gem 'sinatra', '~> 3.0' +gem 'puma', '~> 6.0' +gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' +gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' +gem 'json', '~> 2.0' +gem 'concurrent-ruby', '~> 1.0' +gem 'nokogiri', '~> 1.13' + +group :development do + gem 'rubocop', '~> 1.0' +end diff --git a/test-server/ruby-v3-server/Gemfile.lock b/test-server/ruby-v3-server/Gemfile.lock new file mode 100644 index 00000000..ae04e5fd --- /dev/null +++ b/test-server/ruby-v3-server/Gemfile.lock @@ -0,0 +1,102 @@ +PATH + remote: local-ruby-sdk/gems/aws-sdk-kms + specs: + aws-sdk-kms (1.112.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + +PATH + remote: local-ruby-sdk/gems/aws-sdk-s3 + specs: + aws-sdk-s3 (1.199.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1161.0) + aws-sdk-core (3.232.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bigdecimal (3.2.3) + concurrent-ruby (1.3.5) + jmespath (1.6.2) + json (2.13.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + mustermann (3.0.4) + ruby2_keywords (~> 0.0.1) + nio4r (2.7.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + prism (1.5.1) + puma (6.6.1) + nio4r (~> 2.0) + racc (1.8.1) + rack (2.2.17) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rainbow (3.1.1) + regexp_parser (2.11.3) + rubocop (1.80.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sinatra (3.2.0) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.2.0) + tilt (~> 2.0) + tilt (2.6.1) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + +PLATFORMS + arm64-darwin-24 + +DEPENDENCIES + aws-sdk-kms! + aws-sdk-s3! + concurrent-ruby (~> 1.0) + json (~> 2.0) + nokogiri (~> 1.13) + puma (~> 6.0) + rubocop (~> 1.0) + sinatra (~> 3.0) + +RUBY VERSION + ruby 3.4.5p51 + +BUNDLED WITH + 2.6.9 diff --git a/test-server/ruby-v3-server/Makefile b/test-server/ruby-v3-server/Makefile new file mode 100644 index 00000000..e4492423 --- /dev/null +++ b/test-server/ruby-v3-server/Makefile @@ -0,0 +1,29 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8092 + +start-server: + @if [ -f $(PID_FILE) ]; then \ + echo "❌ Error: Server already running. Stop before starting."; \ + exit 1; \ + fi; + @echo "Starting Ruby V3 server..." + bundle install + 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" \ + bundle exec ruby app.rb & echo $$! > $(PID_FILE) + @echo "Ruby V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/ruby-v3-server/README.md b/test-server/ruby-v3-server/README.md new file mode 100644 index 00000000..0c27b3d8 --- /dev/null +++ b/test-server/ruby-v3-server/README.md @@ -0,0 +1,74 @@ +# Ruby S3 Encryption Client Test Server + +This is a Ruby implementation of the S3 Encryption Client test server +that provides an invariant interface around the S3 Encryption Client v3. +It's designed to work alongside other implementations of test servers for cross-language compatibility testing. + +## Overview + +The server provides a REST API that wraps the AWS S3 Encryption Client v3, +allowing tests to verify that all language implementations behave consistently. + +## Endpoints + +- `POST /client` - Create a new S3 encryption client instance +- `PUT /object/{bucket}/{key}` - Encrypt and store an object +- `GET /object/{bucket}/{key}` - Retrieve and decrypt an object +- `GET /health` - Health check endpoint + +## Configuration + +The server runs on port **8092** by default. + +## Setup + +1. Install Ruby 3.x +2. Install dependencies: + + ```bash + cd test-server/ruby-v2-server + bundle install + ``` + +3. Set up AWS credentials (via AWS CLI, environment variables, or IAM roles) + +4. Start the server: + + ```bash + ruby app.rb + # or using Rack + bundle exec rackup -p 8092 + ``` + +## Usage + +The server is designed to be used by the Java test suite in `test-server/java-tests/`. +The tests will automatically discover and use this server for cross-language compatibility testing. + +### Environment Variables + +- `TEST_SERVER_KMS_KEY_ARN` - KMS key ARN for encryption (defaults to test key) +- `TEST_SERVER_S3_BUCKET` - S3 bucket for testing (defaults to test bucket) + +## Architecture + +- `app.rb` - Main Sinatra application +- `lib/client_manager.rb` - Manages S3 encryption client instances +- `lib/metadata_utils.rb` - Handles metadata serialization/deserialization +- `lib/error_handlers.rb` - Smithy-compliant error responses + +## Error Handling + +The server returns errors in the format expected by the Smithy model: + +- `GenericServerError` - Internal server errors +- `S3EncryptionClientError` - Errors from the S3 Encryption Client + +## Compatibility + +This server is compatible with: + +- S3 Encryption Client v3 +- Legacy v1 clients (when `enableLegacyWrappingAlgorithms` is true) +- Legacy v2 clients (when `???` is true) +- Cross-language testing with other implementations diff --git a/test-server/ruby-v3-server/app.rb b/test-server/ruby-v3-server/app.rb new file mode 100644 index 00000000..abc25932 --- /dev/null +++ b/test-server/ruby-v3-server/app.rb @@ -0,0 +1,231 @@ +require 'sinatra' +require 'json' +require_relative 'lib/client_manager' +require_relative 'lib/metadata_utils' +require_relative 'lib/error_handlers' +require_relative 'lib/logger' + +class S3ECRubyServer < Sinatra::Base + configure do + set :port, 8092 + set :bind, '0.0.0.0' + set :show_exceptions, false + set :raise_errors, false + end + + def initialize + super + @client_manager = ClientManager.new + S3ECLogger.info("S3EC_SERVER: Ruby V3 server initialized on port #{settings.port}") + end + + # Request logging middleware + before do + @request_id = S3ECLogger.generate_request_id + S3ECLogger.log_request(request.request_method, request.path_info, request.env, @request_id) + end + + # Response logging middleware + after do + S3ECLogger.log_response(response.status, @request_id) + end + + # Health check endpoint + get '/health' do + content_type :json + { status: 'OK', server: 'Ruby V3 S3EC Test Server', port: settings.port.to_i }.to_json + end + + # POST /client - Create S3 encryption client + post '/client' do + begin + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Processing client creation request") + + # Parse request body + request_body = request.body.read + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Request body size: #{request_body.length} bytes") + + parsed_data = JSON.parse(request_body) + config = parsed_data['config'] || {} + + S3ECLogger.debug("CLIENT_ENDPOINT [#{@request_id}]: Parsed config: #{config.inspect}") + + # Create client using client manager + client_id = @client_manager.create_client(config) + + S3ECLogger.info("CLIENT_ENDPOINT [#{@request_id}]: Successfully created client #{client_id}") + + # Return client ID + content_type :json + { clientId: client_id }.to_json + + rescue JSON::ParserError => e + S3ECLogger.log_error(e, { endpoint: '/client', operation: 'JSON parsing' }, @request_id) + ErrorHandlers.send_generic_server_error(self, "Invalid JSON in request body", 400) + rescue => e + S3ECLogger.log_error(e, { endpoint: '/client', operation: 'client creation', config: config }, @request_id) + ErrorHandlers.send_s3_encryption_client_error(self, "Failed to create client: #{e.message}") + end + end + + # PUT /object/{bucket}/{key} - Encrypt and put object + put '/object/:bucket/:key' do + bucket = params[:bucket] + key = params[:key] + client_id = request.env['HTTP_CLIENTID'] + + begin + S3ECLogger.debug("PUT_ENDPOINT [#{@request_id}]: Processing PUT request for s3://#{bucket}/#{key}") + + # Validate client ID + unless client_id + S3ECLogger.log_validation_error('ClientID', 'missing', @request_id) + ErrorHandlers.send_generic_server_error(self, "ClientID header is required", 400) + end + + # Get client from cache + client = @client_manager.get_client(client_id) + unless client + S3ECLogger.log_validation_error('ClientID', client_id, @request_id) + ErrorHandlers.send_generic_server_error(self, "No client found for ClientID: #{client_id}", 404) + end + + # Get request body + body = request.body.read + S3ECLogger.debug("PUT_ENDPOINT [#{@request_id}]: Request body size: #{body.length} bytes") + + # Parse metadata from header + metadata_header = request.env['HTTP_CONTENT_METADATA'] || '' + encryption_context = MetadataUtils.string_to_map(metadata_header) + S3ECLogger.log_metadata_processing('parse', metadata_header, encryption_context) + + # Prepare S3 put_object parameters + put_params = { + bucket: bucket, + key: key, + body: body + } + + # Add encryption context if present + put_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + + # Log S3 operation + S3ECLogger.log_s3_operation('put', bucket, key, encryption_context, "ClientID: #{client_id}, BodySize: #{body.length}") + + # Make the put_object request + response = client.put_object(put_params) + + S3ECLogger.info("PUT_ENDPOINT [#{@request_id}]: Successfully put object s3://#{bucket}/#{key}") + + # Prepare response metadata + response_metadata = MetadataUtils.map_to_array(encryption_context) + S3ECLogger.log_metadata_processing('response', encryption_context, response_metadata) + + # Return response matching Smithy model + content_type :json + { + bucket: bucket, + key: key, + metadata: response_metadata + }.to_json + + rescue Aws::S3::EncryptionV2::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 + # Handle generic server errors (return as GenericServerError) + S3ECLogger.log_error(e, { endpoint: '/put', error_category: 'generic_server' }, @request_id) + status_code = e.respond_to?(:code) ? e.code : 500 + ErrorHandlers.send_generic_server_error(self, e.message, status_code) + end + end + + # GET /object/{bucket}/{key} - Get and decrypt object + get '/object/:bucket/:key' do + bucket = params[:bucket] + key = params[:key] + client_id = request.env['HTTP_CLIENTID'] + + begin + S3ECLogger.debug("GET_ENDPOINT [#{@request_id}]: Processing GET request for s3://#{bucket}/#{key}") + + # Validate client ID + unless client_id + S3ECLogger.log_validation_error('ClientID', 'missing', @request_id) + ErrorHandlers.send_generic_server_error(self, "ClientID header is required", 400) + end + + # Get client from cache + client = @client_manager.get_client(client_id) + unless client + S3ECLogger.log_validation_error('ClientID', client_id, @request_id) + ErrorHandlers.send_generic_server_error(self, "No client found for ClientID: #{client_id}", 404) + end + + # Parse metadata from header + metadata_header = request.env['HTTP_CONTENT_METADATA'] || '' + encryption_context = MetadataUtils.string_to_map(metadata_header) + S3ECLogger.log_metadata_processing('parse', metadata_header, encryption_context) + + # Prepare S3 get_object parameters + get_params = { + bucket: bucket, + key: key + } + + # Add encryption context if present + get_params[:kms_encryption_context] = encryption_context unless encryption_context.empty? + + # Log S3 operation + S3ECLogger.log_s3_operation('get', bucket, key, encryption_context, "ClientID: #{client_id}") + + # Make the get_object request + response = client.get_object(get_params) + + # Extract body and metadata + body = response.body.read + metadata = response.metadata || {} + + S3ECLogger.info("GET_ENDPOINT [#{@request_id}]: Successfully got object s3://#{bucket}/#{key}, BodySize: #{body.length}") + + # Set Content-Metadata header in response + metadata_str = MetadataUtils.map_to_string(metadata) + S3ECLogger.log_metadata_processing('response', metadata, metadata_str) + + headers['Content-Metadata'] = metadata_str unless metadata_str.empty? + + # Return the body as response + content_type 'application/octet-stream' + body + + rescue Aws::S3::EncryptionV2::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 + # Handle generic server errors (return as GenericServerError) + S3ECLogger.log_error(e, { endpoint: '/get', error_category: 'generic_server' }, @request_id) + status_code = e.respond_to?(:code) ? e.code : 500 + ErrorHandlers.send_generic_server_error(self, e.message, status_code) + end + end + + # Global error handler + error do + error = env['sinatra.error'] + context = { + endpoint: request.path_info, + method: request.request_method, + params: params, + error_type: 'global_error_handler' + } + + S3ECLogger.log_error(error, context, @request_id) + ErrorHandlers.send_generic_server_error(self, "Internal server error: #{error.message}") + end + + # Start server when run directly + if __FILE__ == $0 + S3ECLogger.info("S3EC_SERVER: Starting Ruby server on port #{settings.port}...") + run! + end +end diff --git a/test-server/ruby-v3-server/config.ru b/test-server/ruby-v3-server/config.ru new file mode 100644 index 00000000..99d3a689 --- /dev/null +++ b/test-server/ruby-v3-server/config.ru @@ -0,0 +1,3 @@ +require_relative 'app' + +run S3ECRubyServer diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb new file mode 100644 index 00000000..d3b12b23 --- /dev/null +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -0,0 +1,74 @@ +require 'concurrent-ruby' +require 'securerandom' +require 'aws-sdk-s3' +require 'aws-sdk-kms' +require_relative 'logger' + +# Manages S3 Encryption Client instances +class ClientManager + def initialize + @client_cache = Concurrent::Hash.new + @kms_client = Aws::KMS::Client.new(region: 'us-west-2') + S3ECLogger.info("CLIENT_MANAGER: Initialized with KMS client for us-west-2") + end + + # Create a new S3 encryption client and return its ID + def create_client(config) + # Extract configuration + kms_key_id = config.dig('keyMaterial', 'kmsKeyId') + enable_legacy_wrapping = config['enableLegacyWrappingAlgorithms'] || false + + 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, + content_encryption_schema: :aes_gcm_no_padding, + # Set security profile based on legacy wrapping algorithms setting + security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 + } + + # Create the S3 encryption client + s3_client = Aws::S3::Client.new(region: 'us-west-2') + encryption_client = Aws::S3::EncryptionV2::Client.new( + client: s3_client, + **encryption_config + ) + + # Generate client ID and store in cache + client_id = SecureRandom.uuid + @client_cache[client_id] = encryption_client + + # Log client creation + S3ECLogger.log_client_creation(config, client_id) + S3ECLogger.log_cache_stats(@client_cache.size) + + client_id + end + + # Get a client by ID + def get_client(client_id) + client = @client_cache[client_id] + if client + S3ECLogger.log_client_cache_hit(client_id) + else + S3ECLogger.log_client_cache_miss(client_id) + end + client + end + + # Remove a client from cache (optional cleanup) + def remove_client(client_id) + removed = @client_cache.delete(client_id) + S3ECLogger.info("CLIENT_CACHE: Removed client #{client_id} from cache") if removed + S3ECLogger.log_cache_stats(@client_cache.size) + removed + end + + # Get cache size (for debugging) + def cache_size + @client_cache.size + end +end diff --git a/test-server/ruby-v3-server/lib/error_handlers.rb b/test-server/ruby-v3-server/lib/error_handlers.rb new file mode 100644 index 00000000..234a9a55 --- /dev/null +++ b/test-server/ruby-v3-server/lib/error_handlers.rb @@ -0,0 +1,42 @@ +# Error handling utilities to match Smithy error types +require 'json' + +class ErrorHandlers + # Create a response that matches the GenericServerError type from the Smithy model + # Used for internal server errors + def self.create_generic_server_error(message, status_code = 500) + { + status: status_code, + headers: { 'Content-Type' => 'application/json' }, + body: { + '__type' => 'software.amazon.encryption.s3#GenericServerError', + 'message' => message + }.to_json + } + end + + # Create a response that matches the S3EncryptionClientError type from the Smithy model + # Used for errors thrown by the S3 Encryption Client + def self.create_s3_encryption_client_error(message, status_code = 500) + { + status: status_code, + headers: { 'Content-Type' => 'application/json' }, + body: { + '__type' => 'software.amazon.encryption.s3#S3EncryptionClientError', + 'message' => message + }.to_json + } + end + + # Helper method to send error response in Sinatra + def self.send_generic_server_error(app, message, status_code = 500) + error_response = create_generic_server_error(message, status_code) + app.halt error_response[:status], error_response[:headers], error_response[:body] + end + + # Helper method to send S3EC error response in Sinatra + def self.send_s3_encryption_client_error(app, message, status_code = 500) + error_response = create_s3_encryption_client_error(message, status_code) + app.halt error_response[:status], error_response[:headers], error_response[:body] + end +end diff --git a/test-server/ruby-v3-server/lib/logger.rb b/test-server/ruby-v3-server/lib/logger.rb new file mode 100644 index 00000000..2febcbab --- /dev/null +++ b/test-server/ruby-v3-server/lib/logger.rb @@ -0,0 +1,105 @@ +require 'logger' +require 'securerandom' + +# Centralized logging utility for the S3EC Ruby server +class S3ECLogger + def self.instance + @instance ||= new + end + + def initialize + @logger = Logger.new(STDOUT) + @logger.level = ENV['LOG_LEVEL'] ? Logger.const_get(ENV['LOG_LEVEL'].upcase) : Logger::INFO + @logger.formatter = proc do |severity, datetime, progname, msg| + "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" + end + end + + # Generate a unique request ID for correlation + def self.generate_request_id + SecureRandom.hex(8) + end + + # Request/Response logging + def self.log_request(method, path, headers = {}, request_id = nil) + client_id = headers['HTTP_CLIENTID'] || headers['ClientID'] || 'none' + content_metadata = headers['HTTP_CONTENT_METADATA'] || headers['Content-Metadata'] || 'none' + + instance.logger.info("REQUEST [#{request_id}] #{method} #{path} | ClientID: #{client_id} | Metadata: #{content_metadata}") + end + + def self.log_response(status, request_id = nil, additional_info = "") + info_str = additional_info.empty? ? "" : " | #{additional_info}" + instance.logger.info("RESPONSE [#{request_id}] Status: #{status}#{info_str}") + end + + # Operation-level logging + def self.log_client_creation(config, client_id) + kms_key = config.dig('keyMaterial', 'kmsKeyId') || 'unknown' + legacy_enabled = config['enableLegacyWrappingAlgorithms'] || false + instance.logger.info("CLIENT_CREATION: Created S3EC client #{client_id} | KMS Key: #{kms_key} | Legacy: #{legacy_enabled}") + end + + def self.log_client_cache_hit(client_id) + instance.logger.debug("CACHE_HIT: Found client #{client_id} in cache") + end + + def self.log_client_cache_miss(client_id) + instance.logger.warn("CACHE_MISS: Client #{client_id} not found in cache") + end + + def self.log_cache_stats(cache_size) + instance.logger.debug("CACHE_STATS: Current client cache size: #{cache_size}") + end + + def self.log_s3_operation(operation, bucket, key, encryption_context = {}, additional_info = "") + enc_ctx_str = encryption_context.empty? ? "none" : encryption_context.inspect + info_str = additional_info.empty? ? "" : " | #{additional_info}" + instance.logger.info("S3_OPERATION: #{operation.upcase} s3://#{bucket}/#{key} | EncCtx: #{enc_ctx_str}#{info_str}") + end + + def self.log_metadata_processing(operation, input, output) + instance.logger.debug("METADATA_#{operation.upcase}: Input: #{input.inspect} | Output: #{output.inspect}") + end + + # Enhanced error logging + def self.log_error(error, context = {}, request_id = nil) + error_context = context.empty? ? "" : " | Context: #{context.inspect}" + instance.logger.error("ERROR [#{request_id}] #{error.class}: #{error.message}#{error_context}") + + if error.backtrace && instance.debug? + instance.logger.debug("ERROR_BACKTRACE [#{request_id}]:\n#{error.backtrace.join("\n")}") + end + end + + def self.log_validation_error(field, value, request_id = nil) + instance.logger.warn("VALIDATION_ERROR [#{request_id}] Invalid #{field}: #{value}") + end + + def self.log_aws_error(error, operation, request_id = nil) + instance.logger.error("AWS_ERROR [#{request_id}] #{operation} failed: #{error.class} - #{error.message}") + end + + # Standard logging methods + def self.debug(message) + instance.logger.debug(message) + end + + def self.info(message) + instance.logger.info(message) + end + + def self.warn(message) + instance.logger.warn(message) + end + + def self.error(message) + instance.logger.error(message) + end + + attr_reader :logger + + def debug? + @logger.debug? + end +end diff --git a/test-server/ruby-v3-server/lib/metadata_utils.rb b/test-server/ruby-v3-server/lib/metadata_utils.rb new file mode 100644 index 00000000..72015fcc --- /dev/null +++ b/test-server/ruby-v3-server/lib/metadata_utils.rb @@ -0,0 +1,50 @@ +# Utility class for handling metadata serialization/deserialization +# Matches the format used by Java and Python servers: [key]:[value],[key2]:[value2] +class MetadataUtils + # Convert metadata string to hash + # Input: "[user-defined-enc-ctx-key]:[user-defined-enc-ctx-value],[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]" + # Output: {"user-defined-enc-ctx-key" => "user-defined-enc-ctx-value", "user-defined-enc-ctx-key-2" => "user-defined-enc-ctx-value-2"} + def self.string_to_map(metadata_string) + return {} if metadata_string.nil? || metadata_string.empty? + + metadata = {} + entries = metadata_string.split(',') + + entries.each do |entry| + # Split on "]:[" to separate key and value + parts = entry.split(']:[') + if parts.length == 2 + # Remove remaining brackets from start and end + key = parts[0].delete_prefix("[") # Remove first character '[' + value = parts[1].delete_suffix("]") # Remove last character ']' + metadata[key] = value + else + raise "Malformed metadata list entry: #{entry}" + end + end + + metadata + end + + # Convert hash to metadata string + # Input: {"user-defined-enc-ctx-key" => "user-defined-enc-ctx-value", "user-defined-enc-ctx-key-2" => "user-defined-enc-ctx-value-2"} + # Output: "[user-defined-enc-ctx-key]:[user-defined-enc-ctx-value],[user-defined-enc-ctx-key-2]:[user-defined-enc-ctx-value-2]" + def self.map_to_string(metadata_hash) + return '' if metadata_hash.nil? || metadata_hash.empty? + + entries = metadata_hash.map do |key, value| + "[#{key}]:[#{value}]" + end + + entries.join(',') + end + + # Convert metadata hash to array format (for JSON responses) + def self.map_to_array(metadata_hash) + return [] if metadata_hash.nil? || metadata_hash.empty? + + metadata_hash.map do |key, value| + "[#{key}]:[#{value}]" + end + end +end diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk new file mode 160000 index 00000000..e129cf37 --- /dev/null +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -0,0 +1 @@ +Subproject commit e129cf37c170254ebb631782cae145040cde6d0b From e2bab004e554d3a4c11335bf629b34ead2662f9f Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 22 Sep 2025 10:36:04 -0400 Subject: [PATCH 057/201] m --- test-server/cpp-v2-server/main.cpp | 1 - .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index ccc72d76..869e617b 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -57,7 +57,6 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; - Aws::KMS::KMSClient kms_client; auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); 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 ec834c70..cd67f2e0 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 @@ -561,7 +561,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { } catch (S3EncryptionClientError e) { if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2)) { assertTrue(e.getMessage().contains( - "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration V2." + "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { 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.")); From 38404feb523302ab1c2b9e44fe9664cc7418f688 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Mon, 22 Sep 2025 14:25:58 -0700 Subject: [PATCH 058/201] auto commit --- cdk/bin/cdk.ts | 7 +++++++ cdk/lib/cdk-stack.ts | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 cdk/bin/cdk.ts diff --git a/cdk/bin/cdk.ts b/cdk/bin/cdk.ts new file mode 100644 index 00000000..08214db5 --- /dev/null +++ b/cdk/bin/cdk.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { S3ECPythonGithub } from '../lib/cdk-stack'; + +const app = new cdk.App(); +new S3ECPythonGithub(app, 'S3ECPythonGithub'); diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index cdb7c489..43f72bff 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -106,16 +106,29 @@ export class S3ECPythonGithub extends cdk.Stack { resources: [ S3ECGithubTestS3Bucket.bucketArn + "/*", // object-level permissions need this extra path S3ECTestServerGithubBucket.bucketArn + "/*", // Add permissions for the new test-server bucket + "arn:aws:s3:::aws-net-sdk-*/*" // permission for object inside S3EC .net bucket ], }), new PolicyStatement({ effect: Effect.ALLOW, actions: [ "s3:ListBucket", + "s3:GetBucketAcl" ], resources: [ S3ECGithubTestS3Bucket.bucketArn, S3ECTestServerGithubBucket.bucketArn, // Add permissions for the new test-server bucket + "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket + ], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + "s3:CreateBucket", + "s3:DeleteBucket" + ], + resources: [ + "arn:aws:s3:::aws-net-sdk-*" ], }), ] @@ -155,7 +168,10 @@ export class S3ECPythonGithub extends cdk.Stack { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { - "token.actions.githubusercontent.com:sub": "repo:aws/amazon-s3-encryption-client-python:*" + "token.actions.githubusercontent.com:sub": [ + "repo:aws/amazon-s3-encryption-client-python:*", + "repo:aws/private-amazon-s3-encryption-client-dotnet-staging:*" + ] } }, "sts:AssumeRoleWithWebIdentity" From 143721c142a3009c703904b7af848ef2b3f585b4 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 11:31:37 -0700 Subject: [PATCH 059/201] auto commit --- cdk/lib/cdk-stack.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 43f72bff..1ccc05a0 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -74,7 +74,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECGithubTestS3Bucket", { - bucketName: "s3ec-python-github-test-bucket", + bucketName: "s3ec-python-github-test-bucket-" + this.account, // revert this blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) @@ -84,7 +84,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECTestServerGithubBucket", { - bucketName: "s3ec-test-server-github-bucket", + bucketName: "s3ec-test-server-github-bucket-" + this.account, // revert this blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) @@ -102,6 +102,7 @@ export class S3ECPythonGithub extends cdk.Stack { "s3:PutObject", "s3:GetObject", "s3:DeleteObject", + "s3:DeleteObjectVersion" ], resources: [ S3ECGithubTestS3Bucket.bucketArn + "/*", // object-level permissions need this extra path @@ -112,7 +113,10 @@ export class S3ECPythonGithub extends cdk.Stack { new PolicyStatement({ effect: Effect.ALLOW, actions: [ + "s3:CreateBucket", + "s3:DeleteBucket", "s3:ListBucket", + "s3:ListBucketVersions", "s3:GetBucketAcl" ], resources: [ @@ -121,16 +125,6 @@ export class S3ECPythonGithub extends cdk.Stack { "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket ], }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - "s3:CreateBucket", - "s3:DeleteBucket" - ], - resources: [ - "arn:aws:s3:::aws-net-sdk-*" - ], - }), ] }), } From d7de6f17cee099f21ccb43e4f50d7af70d4cd2d2 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 11:32:49 -0700 Subject: [PATCH 060/201] auto commit --- cdk/lib/cdk-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 1ccc05a0..456107da 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -74,7 +74,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECGithubTestS3Bucket", { - bucketName: "s3ec-python-github-test-bucket-" + this.account, // revert this + bucketName: "s3ec-python-github-test-bucket-", blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) @@ -84,7 +84,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECTestServerGithubBucket", { - bucketName: "s3ec-test-server-github-bucket-" + this.account, // revert this + bucketName: "s3ec-test-server-github-bucket-", blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) From 0346f16e8ea06815c5f76c36cbeaf71f6b73ef30 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 11:33:45 -0700 Subject: [PATCH 061/201] auto commit --- cdk/lib/cdk-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 456107da..85ff6eec 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -74,7 +74,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECGithubTestS3Bucket", { - bucketName: "s3ec-python-github-test-bucket-", + bucketName: "s3ec-python-github-test-bucket", blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) @@ -84,7 +84,7 @@ export class S3ECPythonGithub extends cdk.Stack { this, "S3ECTestServerGithubBucket", { - bucketName: "s3ec-test-server-github-bucket-", + bucketName: "s3ec-test-server-github-bucket", blockPublicAccess: new BlockPublicAccess(AccessConfiguration) } ) From c3855941793b3503c9d10f057c0de779e634bea2 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 11:47:06 -0700 Subject: [PATCH 062/201] auto commit --- cdk/lib/cdk-stack.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 85ff6eec..e9f3ebe9 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -102,27 +102,27 @@ export class S3ECPythonGithub extends cdk.Stack { "s3:PutObject", "s3:GetObject", "s3:DeleteObject", - "s3:DeleteObjectVersion" + "s3:DeleteObjectVersion" // For S3EC-NET ], resources: [ S3ECGithubTestS3Bucket.bucketArn + "/*", // object-level permissions need this extra path S3ECTestServerGithubBucket.bucketArn + "/*", // Add permissions for the new test-server bucket - "arn:aws:s3:::aws-net-sdk-*/*" // permission for object inside S3EC .net bucket + "arn:aws:s3:::aws-net-sdk-*/*" // permission for object inside S3EC .net bucket. For S3EC-NET ], }), new PolicyStatement({ effect: Effect.ALLOW, actions: [ - "s3:CreateBucket", - "s3:DeleteBucket", + "s3:CreateBucket", // For S3EC-NET + "s3:DeleteBucket", // For S3EC-NET "s3:ListBucket", - "s3:ListBucketVersions", - "s3:GetBucketAcl" + "s3:ListBucketVersions", // For S3EC-NET + "s3:GetBucketAcl" // For S3EC-NET ], resources: [ S3ECGithubTestS3Bucket.bucketArn, S3ECTestServerGithubBucket.bucketArn, // Add permissions for the new test-server bucket - "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket + "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket. For S3EC-NET ], }), ] @@ -164,7 +164,7 @@ export class S3ECPythonGithub extends cdk.Stack { "StringLike": { "token.actions.githubusercontent.com:sub": [ "repo:aws/amazon-s3-encryption-client-python:*", - "repo:aws/private-amazon-s3-encryption-client-dotnet-staging:*" + "repo:aws/private-amazon-s3-encryption-client-dotnet-staging:*" // For S3EC-NET ] } }, From 1c365c9e0759b3ed7187fb914ffe703fdc8a6ef2 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 11:48:54 -0700 Subject: [PATCH 063/201] auto commit --- cdk/lib/cdk-stack.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index e9f3ebe9..97b30088 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -102,27 +102,27 @@ export class S3ECPythonGithub extends cdk.Stack { "s3:PutObject", "s3:GetObject", "s3:DeleteObject", - "s3:DeleteObjectVersion" // For S3EC-NET + "s3:DeleteObjectVersion" // For S3EC-NET repo ], resources: [ S3ECGithubTestS3Bucket.bucketArn + "/*", // object-level permissions need this extra path S3ECTestServerGithubBucket.bucketArn + "/*", // Add permissions for the new test-server bucket - "arn:aws:s3:::aws-net-sdk-*/*" // permission for object inside S3EC .net bucket. For S3EC-NET + "arn:aws:s3:::aws-net-sdk-*/*" // permission for object inside S3EC .net bucket. For S3EC-NET repo ], }), new PolicyStatement({ effect: Effect.ALLOW, actions: [ - "s3:CreateBucket", // For S3EC-NET - "s3:DeleteBucket", // For S3EC-NET + "s3:CreateBucket", // For S3EC-NET repo + "s3:DeleteBucket", // For S3EC-NET repo "s3:ListBucket", - "s3:ListBucketVersions", // For S3EC-NET - "s3:GetBucketAcl" // For S3EC-NET + "s3:ListBucketVersions", // For S3EC-NET repo + "s3:GetBucketAcl" // For S3EC-NET repo ], resources: [ S3ECGithubTestS3Bucket.bucketArn, S3ECTestServerGithubBucket.bucketArn, // Add permissions for the new test-server bucket - "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket. For S3EC-NET + "arn:aws:s3:::aws-net-sdk-*", // permission for S3EC .net bucket. For S3EC-NET repo ], }), ] @@ -164,7 +164,7 @@ export class S3ECPythonGithub extends cdk.Stack { "StringLike": { "token.actions.githubusercontent.com:sub": [ "repo:aws/amazon-s3-encryption-client-python:*", - "repo:aws/private-amazon-s3-encryption-client-dotnet-staging:*" // For S3EC-NET + "repo:aws/private-amazon-s3-encryption-client-dotnet-staging:*" // For S3EC-NET repo ] } }, From 0e6cb7c5805289ecbbc3ed687ed28e221e601b14 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Sep 2025 12:56:31 -0700 Subject: [PATCH 064/201] auto commit --- test-server/cpp-v2-server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index e9156d64..ad5c951e 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -8,7 +8,7 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git - cd aws-sdk-cpp && git checkout --track remotes/origin/ajewell/ec-for-get-object + cd aws-sdk-cpp mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server From b8413d6a5aaf70c790fb8fa4a87f244f740c21d0 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 25 Sep 2025 10:47:56 -0700 Subject: [PATCH 065/201] refactor --- .../amazon/encryption/s3/RoundTripTests.java | 306 ++++-------------- .../amazon/encryption/s3/TestUtils.java | 235 ++++++++++++++ 2 files changed, 295 insertions(+), 246 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java 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 cd67f2e0..b2160eb1 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 @@ -9,19 +9,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.net.Socket; -import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import com.amazonaws.services.s3.model.KMSEncryptionMaterials; @@ -29,10 +21,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol; -import software.amazon.smithy.java.client.core.ClientConfig; -import software.amazon.smithy.java.client.core.ClientProtocol; -import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; import software.amazon.encryption.s3.client.S3ECTestServerClient; import software.amazon.encryption.s3.model.CreateClientInput; import software.amazon.encryption.s3.model.CreateClientOutput; @@ -41,206 +29,32 @@ 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.S3ECTestServerApiService; import software.amazon.encryption.s3.model.S3EncryptionClientError; -import software.amazon.smithy.java.http.api.HttpRequest; -import software.amazon.smithy.java.http.api.HttpResponse; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; 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.LanguageServerTarget; import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; public class RoundTripTests { - private static final String JAVA_V3 = "Java-V3"; - private static final String PYTHON_V3 = "Python-V3"; - private static final String GO_V3 = "Go-V3"; - private static final String CPP_V2 = "CPP-V2"; - private static final String NET_V2 = "NET-V2"; - private static final String NET_V3 = "NET-V3"; - private static final String PHP_V2 = "PHP-V2"; - private static final String PHP_V3 = "PHP-V3"; - private static final String RUBY_V2 = "Ruby-V2"; - private static final String RUBY_V3 = "Ruby-V3"; - - private static final Map serverMap; - - private static final String KMS_KEY_ARN = System.getenv("TEST_SERVER_KMS_KEY_ARN") != null ? - System.getenv("TEST_SERVER_KMS_KEY_ARN") : "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"; - private static final Region KMS_REGION = Region.getRegion(Regions.fromName("us-west-2")); - private static final String BUCKET = System.getenv("TEST_SERVER_S3_BUCKET") != null ? - System.getenv("TEST_SERVER_S3_BUCKET") : "s3ec-test-server-github-bucket"; - - static { - final Map servers = new LinkedHashMap<>(); - servers.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); - servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); - servers.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); - servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); - servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); - servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); - servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); - servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); - servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); - - serverMap = filterServers(servers); - } - - private static Map filterServers(Map allServers) { - - final String maybeFilter = System.getProperty("test.filter.servers"); - if (maybeFilter == null || maybeFilter.trim().isEmpty()) { - return allServers; // No filtering - use all servers - } - - final String[] filters = Arrays.stream(maybeFilter.split(",")) - .map(String::trim) - .map(String::toLowerCase) - .toArray(String[]::new); - - return allServers.entrySet().stream() - .filter(entry -> { - String key = entry.getKey().toLowerCase(); - return Arrays.stream(filters).anyMatch(key::contains); - }) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, // merge function (not really needed) - LinkedHashMap::new // preserve order - )); - } - - // Encryption context validation behavior varies by implementation: - // - Go: Does not validate encryption context on decrypt operations - // - .NET: Only validates against encryption context stored in the object metadata - // If the encryption context provided to getObject does not match the encryption context on the stored object, - // these implementations will not raise an error as expected. - // For now, skip tests that expect encryption context validation on decrypt. - private static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of(GO_V3, PHP_V2, PHP_V3, NET_V2, NET_V3); - - // S3EC .NET implementations does not accept encryption context (EC) during putObject operations. - // These tests are not configured to pass encryption context at client level but at encrypt, - // So, for .NET EC is not passed. - // For now, skip tests that expect encryption context validation on decrypt. - private static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = - Set.of(NET_V2, NET_V3); - - static public class LanguageServerTarget { - public String getLanguageName() { - return languageName; - } - - public URI getServerURI() { - return serverURI; - } - - private final String baseURI = "http://localhost"; - private String languageName; - private URI serverURI; - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - LanguageServerTarget that = (LanguageServerTarget) o; - return Objects.equals(languageName, that.languageName) && Objects.equals(serverURI, that.serverURI); - } - - @Override - public int hashCode() { - return Objects.hash(languageName, serverURI); - } - - LanguageServerTarget(String language, String port) { - languageName = language; - serverURI = URI.create(baseURI+ ":" + port); - } - - @Override - public String toString() { - return languageName; - } - } @BeforeAll public static void setup() { - // Wait for servers to start - for (LanguageServerTarget server : serverMap.values()) { - if (!serverListening(server.getServerURI())) { - throw new RuntimeException(String.format("Test Server for %s is not running at endpoint: %s", server.getLanguageName(), server.getServerURI())); - } - } - } - - public static boolean serverListening(URI uri) { - try (Socket ignored = new Socket(uri.getHost(), uri.getPort())) { - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - static S3ECTestServerClient testServerClientFor(LanguageServerTarget server) { - S3ECTestServerApiService apiService = S3ECTestServerApiService.instance(); - ClientProtocol rest = new RestJsonClientProtocol(apiService.schema().id()); - return S3ECTestServerClient.builder() - .endpointResolver(EndpointResolver.staticEndpoint(server.serverURI)) - .withConfiguration(ClientConfig.builder() - .service(apiService) - .protocol(rest) - .endpointResolver(EndpointResolver.staticEndpoint(server.serverURI)) - .build()) - .build(); - } - - static Stream clientsForTest() { - return serverMap.values().stream() - .map(LanguageServerTarget::getLanguageName) - .map(Arguments::of); - } - - static Stream crossLanguageClients() { - return serverMap.values().stream() - .flatMap(t1 -> serverMap.values().stream() - .flatMap(t2 -> Stream.of( - Arguments.of(t1, t2) - ))); - } - - /** - * Annoyingly, Smithy doesn't provide an interface for map types - * in HTTP headers, so we have to do the serde ourselves - * Servers need an equivalent utility. - * TODO: Move to a utilities class or something. - */ - private List metadataMapToList(Map md) { - List mdAsList = new ArrayList<>(md.size()); - for (Map.Entry keyValue : md.entrySet()) { - // Using ":" because Smithy will parse "," into a flattened list - mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); - } - return mdAsList; + TestUtils.validateServersRunning(); } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") - @MethodSource("crossLanguageClients") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTarget decLang) { - S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-" + encLang; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -250,10 +64,10 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = testServerClientFor(decLang); + S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -261,7 +75,7 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .build()); @@ -271,20 +85,20 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") - @MethodSource("crossLanguageClients") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + if (TestUtils.ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = metadataMapToList(encCtx); + final List mdAsList = TestUtils.metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -295,11 +109,11 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = testServerClientFor(decLang); + S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -307,7 +121,7 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .metadata(mdAsList) .build()); @@ -318,23 +132,23 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") - @MethodSource("crossLanguageClients") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + if (TestUtils.ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { return; } - if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + if (TestUtils.ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-subset-fails" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = metadataMapToList(encCtx); + final List mdAsList = TestUtils.metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -345,11 +159,11 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = testServerClientFor(decLang); + S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -358,12 +172,12 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa try { decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.languageName.equals(RUBY_V3) || decLang.languageName.equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(TestUtils.RUBY_V3) || decLang.getLanguageName().equals(TestUtils.RUBY_V2)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -372,20 +186,20 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") - @MethodSource("crossLanguageClients") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + if (TestUtils.ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-incorrect-fails" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = metadataMapToList(encCtx); + final List mdAsList = TestUtils.metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -396,11 +210,11 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = testServerClientFor(decLang); + S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -409,17 +223,17 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en final Map incorrectEncCtx = new HashMap<>(); incorrectEncCtx.put("this-is-wrong-ec-key", "bad-value"); - var incorrectMdAsList = metadataMapToList(incorrectEncCtx); + var incorrectMdAsList = TestUtils.metadataMapToList(incorrectEncCtx); try { decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .metadata(incorrectMdAsList) .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.languageName.equals(RUBY_V3) || decLang.languageName.equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(TestUtils.RUBY_V3) || decLang.getLanguageName().equals(TestUtils.RUBY_V2)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -428,13 +242,13 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en } @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") - @MethodSource("clientsForTest") + @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1Legacy(String language) { - S3ECTestServerClient client = testServerClientFor(serverMap.get(language)); + S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); final String objectKey = "test-key-kms-v1-" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -446,23 +260,23 @@ public void kmsV1Legacy(String language) { // Create the object using the old client // V1 Client - EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ARN); + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(KMS_REGION); + .withAwsKmsRegion(TestUtils.KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(BUCKET, objectKey, input); + v1Client.putObject(TestUtils.BUCKET, objectKey, input); GetObjectOutput output = client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .build()); @@ -470,13 +284,13 @@ public void kmsV1Legacy(String language) { } @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") - @MethodSource("clientsForTest") + @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyWithEncCtx(String language) { - S3ECTestServerClient client = testServerClientFor(serverMap.get(language)); + S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); final String objectKey = "test-key-kms-v1-with-enc-ctx-" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -490,42 +304,42 @@ public void kmsV1LegacyWithEncCtx(String language) { // V1 Client final String ecKey = "user-metadata-key"; final String ecValue = "user-metadata-value-v1"; - KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(KMS_KEY_ARN); + KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(TestUtils.KMS_KEY_ARN); kmsMaterials.addDescription(ecKey, ecValue); EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(kmsMaterials); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(KMS_REGION); + .withAwsKmsRegion(TestUtils.KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(BUCKET, objectKey, input); + v1Client.putObject(TestUtils.BUCKET, objectKey, input); final Map encCtx = new HashMap<>(); encCtx.put(ecKey, ecValue); GetObjectOutput output = client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) - .metadata(metadataMapToList(encCtx)) + .metadata(TestUtils.metadataMapToList(encCtx)) .build()); assertEquals(input, new String(output.getBody().array())); } @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") - @MethodSource("clientsForTest") + @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { - S3ECTestServerClient client = testServerClientFor(serverMap.get(language)); + S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); final String objectKey = "test-key-kms-v1-fails-disabled" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) + .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -537,33 +351,33 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { // Create the object using the old client // V1 Client - EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ARN); + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(KMS_REGION); + .withAwsKmsRegion(TestUtils.KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(BUCKET, objectKey, input); + v1Client.putObject(TestUtils.BUCKET, objectKey, input); try { client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(BUCKET) + .bucket(TestUtils.BUCKET) .key(objectKey) .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2)) { + if (language.equals(TestUtils.NET_V3) || language.equals(TestUtils.NET_V2) || language.equals(TestUtils.CPP_V2)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); - } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { + } else if (language.equals(TestUtils.RUBY_V3) || language.equals(TestUtils.RUBY_V2)) { 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.")); } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); 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 new file mode 100644 index 00000000..df701583 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java @@ -0,0 +1,235 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import java.net.Socket; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol; +import software.amazon.smithy.java.client.core.ClientConfig; +import software.amazon.smithy.java.client.core.ClientProtocol; +import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; +import software.amazon.encryption.s3.client.S3ECTestServerClient; +import software.amazon.encryption.s3.model.S3ECTestServerApiService; +import software.amazon.smithy.java.http.api.HttpRequest;c +import software.amazon.smithy.java.http.api.HttpResponse; + +public class TestUtils { + // Language constants + public static final String JAVA_V3 = "Java-V3"; + public static final String PYTHON_V3 = "Python-V3"; + public static final String GO_V3 = "Go-V3"; + public static final String CPP_V2 = "CPP-V2"; + public static final String NET_V2 = "NET-V2"; + public static final String NET_V3 = "NET-V3"; + public static final String PHP_V2 = "PHP-V2"; + public static final String PHP_V3 = "PHP-V3"; + public static final String RUBY_V2 = "Ruby-V2"; + public static final String RUBY_V3 = "Ruby-V3"; + + // Test configuration constants + public static final String KMS_KEY_ARN = System.getenv("TEST_SERVER_KMS_KEY_ARN") != null ? + System.getenv("TEST_SERVER_KMS_KEY_ARN") : "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"; + public static final Region KMS_REGION = Region.getRegion(Regions.fromName("us-west-2")); + public static final String BUCKET = System.getenv("TEST_SERVER_S3_BUCKET") != null ? + System.getenv("TEST_SERVER_S3_BUCKET") : "s3ec-test-server-github-bucket"; + + // Sets of unsupported features by language + public static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = + Set.of(GO_V3, PHP_V2, PHP_V3, NET_V2, NET_V3); + + public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = + Set.of(NET_V2, NET_V3); + + private static final Map serverMap; + + static { + final Map servers = new LinkedHashMap<>(); + servers.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); + servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); + servers.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); + servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); + servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); + servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); + servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); + servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); + servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); + servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + + serverMap = filterServers(servers); + } + + public static class LanguageServerTarget { + private final String baseURI = "http://localhost"; + private String languageName; + private URI serverURI; + + public LanguageServerTarget(String language, String port) { + languageName = language; + serverURI = URI.create(baseURI + ":" + port); + } + + public String getLanguageName() { + return languageName; + } + + public URI getServerURI() { + return serverURI; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LanguageServerTarget that = (LanguageServerTarget) o; + return Objects.equals(languageName, that.languageName) && Objects.equals(serverURI, that.serverURI); + } + + @Override + public int hashCode() { + return Objects.hash(languageName, serverURI); + } + + @Override + public String toString() { + return languageName; + } + } + + /** + * Filters the available servers based on system property test.filter.servers + * @param allServers Map of all available servers + * @return Filtered map of servers to use for testing + */ + private static Map filterServers(Map allServers) { + final String maybeFilter = System.getProperty("test.filter.servers"); + if (maybeFilter == null || maybeFilter.trim().isEmpty()) { + return allServers; // No filtering - use all servers + } + + final String[] filters = Arrays.stream(maybeFilter.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .toArray(String[]::new); + + return allServers.entrySet().stream() + .filter(entry -> { + String key = entry.getKey().toLowerCase(); + return Arrays.stream(filters).anyMatch(key::contains); + }) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, // merge function (not really needed) + LinkedHashMap::new // preserve order + )); + } + + /** + * Gets the map of available server targets for testing + * @return Map of language names to server targets + */ + public static Map getServerMap() { + return serverMap; + } + + /** + * Checks if a server is listening on the specified URI + * @param uri The URI to check + * @return true if server is listening, false otherwise + */ + public static boolean serverListening(URI uri) { + try (Socket ignored = new Socket(uri.getHost(), uri.getPort())) { + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * Creates a test server client for the specified language server target + * @param server The language server target + * @return Configured S3ECTestServerClient + */ + public static S3ECTestServerClient testServerClientFor(LanguageServerTarget server) { + S3ECTestServerApiService apiService = S3ECTestServerApiService.instance(); + ClientProtocol rest = new RestJsonClientProtocol(apiService.schema().id()); + return S3ECTestServerClient.builder() + .endpointResolver(EndpointResolver.staticEndpoint(server.serverURI)) + .withConfiguration(ClientConfig.builder() + .service(apiService) + .protocol(rest) + .endpointResolver(EndpointResolver.staticEndpoint(server.serverURI)) + .build()) + .build(); + } + + /** + * Converts a metadata map to a list format for Smithy serialization + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + * @param md The metadata map + * @return List representation of the metadata + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + // Using ":" because Smithy will parse "," into a flattened list + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; + } + + /** + * Validates that all servers in the server map are running + * @throws RuntimeException if any server is not running + */ + public static void validateServersRunning() { + for (LanguageServerTarget server : serverMap.values()) { + if (!serverListening(server.getServerURI())) { + throw new RuntimeException(String.format("Test Server for %s is not running at endpoint: %s", + server.getLanguageName(), server.getServerURI())); + } + } + } + + /** + * Provides a stream of arguments for parameterized tests that test individual clients + * @return Stream of Arguments containing language names for testing + */ + public static Stream clientsForTest() { + return serverMap.values().stream() + .map(LanguageServerTarget::getLanguageName) + .map(Arguments::of); + } + + /** + * Provides a stream of arguments for parameterized tests that test cross-language compatibility + * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption + */ + public static Stream crossLanguageClients() { + return serverMap.values().stream() + .flatMap(t1 -> serverMap.values().stream() + .flatMap(t2 -> Stream.of( + Arguments.of(t1, t2) + ))); + } +} From 6754f8c6a1c1650b1ff1ef2d84205a051a15e845 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 25 Sep 2025 11:20:12 -0700 Subject: [PATCH 066/201] m --- .../src/it/java/software/amazon/encryption/s3/TestUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df701583..2e78d9e5 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 @@ -26,7 +26,7 @@ import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; import software.amazon.encryption.s3.client.S3ECTestServerClient; import software.amazon.encryption.s3.model.S3ECTestServerApiService; -import software.amazon.smithy.java.http.api.HttpRequest;c +import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; public class TestUtils { From 6c84ef3931919050edacfbfbabcf226d4e9b2bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:26:53 -0700 Subject: [PATCH 067/201] chore: have php link to the local private submodule (#16) --- .gitmodules | 8 ++++++++ test-server/php-v2-server/composer.json | 16 ++++++++++++++-- test-server/php-v2-server/local-php-sdk | 1 + test-server/php-v3-server/composer.json | 20 ++++++++++++++++---- test-server/php-v3-server/local-php-sdk | 1 + 5 files changed, 40 insertions(+), 6 deletions(-) create mode 160000 test-server/php-v2-server/local-php-sdk create mode 160000 test-server/php-v3-server/local-php-sdk diff --git a/.gitmodules b/.gitmodules index b2b112a0..ce2abc73 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,11 @@ [submodule "test-server/ruby-v3-server/local-ruby-sdk"] path = test-server/ruby-v3-server/local-ruby-sdk url = git@github.com:aws/aws-sdk-ruby-staging.git +[submodule "test-server/php-v2-server/local-php-sdk"] + path = test-server/php-v2-server/local-php-sdk + url = git@github.com:aws/private-aws-sdk-php-staging.git + branch = s3ec/transitional +[submodule "test-server/php-v3-server/local-php-sdk"] + path = test-server/php-v3-server/local-php-sdk + url = git@github.com:aws/private-aws-sdk-php-staging.git + branch = s3ec/improved diff --git a/test-server/php-v2-server/composer.json b/test-server/php-v2-server/composer.json index e9c399ac..d5177951 100644 --- a/test-server/php-v2-server/composer.json +++ b/test-server/php-v2-server/composer.json @@ -3,9 +3,18 @@ "description": "PHP v2 implementation of the S3EC Test Server framework", "type": "project", "license": "Apache-2.0", + "repositories": [ + { + "type": "path", + "url": "./local-php-sdk", + "options": { + "symlink": true + } + } + ], "require": { "php": ">=7.4", - "aws/aws-sdk-php": "^3.356", + "aws/aws-sdk-php": "@dev", "ramsey/uuid": "^4.9" }, "autoload": { @@ -19,6 +28,9 @@ ] }, "config": { - "optimize-autoloader": true + "optimize-autoloader": true, + "platform": { + "php": "8.1" + } } } \ No newline at end of file diff --git a/test-server/php-v2-server/local-php-sdk b/test-server/php-v2-server/local-php-sdk new file mode 160000 index 00000000..d78bd3b2 --- /dev/null +++ b/test-server/php-v2-server/local-php-sdk @@ -0,0 +1 @@ +Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 diff --git a/test-server/php-v3-server/composer.json b/test-server/php-v3-server/composer.json index 7ed1daf3..32c2b00c 100644 --- a/test-server/php-v3-server/composer.json +++ b/test-server/php-v3-server/composer.json @@ -1,11 +1,20 @@ { - "name": "aws/s3ec-php-v2-test-server", - "description": "PHP v2 implementation of the S3EC Test Server framework", + "name": "aws/s3ec-php-v3-test-server", + "description": "PHP v3 implementation of the S3EC Test Server framework", "type": "project", "license": "Apache-2.0", + "repositories": [ + { + "type": "path", + "url": "./local-php-sdk", + "options": { + "symlink": true + } + } + ], "require": { "php": ">=7.4", - "aws/aws-sdk-php": "^3.356", + "aws/aws-sdk-php": "@dev", "ramsey/uuid": "^4.9" }, "autoload": { @@ -19,6 +28,9 @@ ] }, "config": { - "optimize-autoloader": true + "optimize-autoloader": true, + "platform": { + "php": "8.1" + } } } \ No newline at end of file diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk new file mode 160000 index 00000000..d78bd3b2 --- /dev/null +++ b/test-server/php-v3-server/local-php-sdk @@ -0,0 +1 @@ +Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 From 6e4b8473ef04601df82e9dbbea5315e2e460ada4 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 25 Sep 2025 15:08:56 -0700 Subject: [PATCH 068/201] m --- .../amazon/encryption/s3/RoundTripTests.java | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 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 b2160eb1..93f66450 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 @@ -36,7 +36,7 @@ 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.LanguageServerTarget; +import software.amazon.encryption.s3.TestUtils.*; import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; @@ -44,17 +44,17 @@ public class RoundTripTests { @BeforeAll public static void setup() { - TestUtils.validateServersRunning(); + validateServersRunning(); } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTarget decLang) { - S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); + S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-" + encLang; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -64,10 +64,10 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -75,7 +75,7 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .build()); @@ -87,18 +87,18 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (TestUtils.ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); + S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = TestUtils.metadataMapToList(encCtx); + final List mdAsList = metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -109,11 +109,11 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -121,7 +121,7 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .metadata(mdAsList) .build()); @@ -134,21 +134,21 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (TestUtils.ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { return; } - if (TestUtils.ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { + if (ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED.contains(encLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); + S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-subset-fails" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = TestUtils.metadataMapToList(encCtx); + final List mdAsList = metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -159,11 +159,11 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -172,12 +172,12 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa try { decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(TestUtils.RUBY_V3) || decLang.getLanguageName().equals(TestUtils.RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -188,18 +188,18 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget encLang, LanguageServerTarget decLang) { - if (TestUtils.ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { + if (ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED.contains(decLang.getLanguageName())) { return; } - S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); + S3ECTestServerClient encClient = testServerClientFor(encLang); final String objectKey = "cross-lang-test-key-kms-ec-incorrect-fails" + encLang; final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); - final List mdAsList = TestUtils.metadataMapToList(encCtx); + final List mdAsList = metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -210,11 +210,11 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en encClient.putObject(PutObjectInput.builder() .clientID(encS3ECId) .key(objectKey) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .metadata(mdAsList) .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) .build()); - S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn).build()) @@ -223,17 +223,17 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en final Map incorrectEncCtx = new HashMap<>(); incorrectEncCtx.put("this-is-wrong-ec-key", "bad-value"); - var incorrectMdAsList = TestUtils.metadataMapToList(incorrectEncCtx); + var incorrectMdAsList = metadataMapToList(incorrectEncCtx); try { decClient.getObject(GetObjectInput.builder() .clientID(decS3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .metadata(incorrectMdAsList) .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(TestUtils.RUBY_V3) || decLang.getLanguageName().equals(TestUtils.RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -244,11 +244,11 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1Legacy(String language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); + S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); final String objectKey = "test-key-kms-v1-" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -260,23 +260,23 @@ public void kmsV1Legacy(String language) { // Create the object using the old client // V1 Client - EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ARN); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(TestUtils.KMS_REGION); + .withAwsKmsRegion(KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(TestUtils.BUCKET, objectKey, input); + v1Client.putObject(BUCKET, objectKey, input); GetObjectOutput output = client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .build()); @@ -286,11 +286,11 @@ public void kmsV1Legacy(String language) { @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyWithEncCtx(String language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); + S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); final String objectKey = "test-key-kms-v1-with-enc-ctx-" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -304,29 +304,29 @@ public void kmsV1LegacyWithEncCtx(String language) { // V1 Client final String ecKey = "user-metadata-key"; final String ecValue = "user-metadata-value-v1"; - KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(TestUtils.KMS_KEY_ARN); + KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(KMS_KEY_ARN); kmsMaterials.addDescription(ecKey, ecValue); EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(kmsMaterials); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(TestUtils.KMS_REGION); + .withAwsKmsRegion(KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(TestUtils.BUCKET, objectKey, input); + v1Client.putObject(BUCKET, objectKey, input); final Map encCtx = new HashMap<>(); encCtx.put(ecKey, ecValue); GetObjectOutput output = client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) - .metadata(TestUtils.metadataMapToList(encCtx)) + .metadata(metadataMapToList(encCtx)) .build()); assertEquals(input, new String(output.getBody().array())); @@ -335,11 +335,11 @@ public void kmsV1LegacyWithEncCtx(String language) { @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { - S3ECTestServerClient client = TestUtils.testServerClientFor(TestUtils.getServerMap().get(language)); + S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); final String objectKey = "test-key-kms-v1-fails-disabled" + language; final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(TestUtils.KMS_KEY_ARN) + .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput output1 = client.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() @@ -351,33 +351,33 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { // Create the object using the old client // V1 Client - EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ARN); CryptoConfiguration v1Config = new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) .withStorageMode(CryptoStorageMode.ObjectMetadata) - .withAwsKmsRegion(TestUtils.KMS_REGION); + .withAwsKmsRegion(KMS_REGION); AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() .withCryptoConfiguration(v1Config) .withEncryptionMaterials(materialsProvider) .build(); - v1Client.putObject(TestUtils.BUCKET, objectKey, input); + v1Client.putObject(BUCKET, objectKey, input); try { client.getObject(GetObjectInput.builder() .clientID(s3ECId) - .bucket(TestUtils.BUCKET) + .bucket(BUCKET) .key(objectKey) .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(TestUtils.NET_V3) || language.equals(TestUtils.NET_V2) || language.equals(TestUtils.CPP_V2)) { + if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); - } else if (language.equals(TestUtils.RUBY_V3) || language.equals(TestUtils.RUBY_V2)) { + } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { 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.")); } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); From 8c2a2a0da6753271e46d7b1393b73a286f8569d8 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:25:26 -0700 Subject: [PATCH 069/201] chore(cdk): allow ToolsDevelopment role assume S3ECGithubTestRole (#18) --- cdk/lib/cdk-stack.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 97b30088..a56d6b26 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -10,6 +10,8 @@ import { PolicyDocument, PolicyStatement, FederatedPrincipal, + ArnPrincipal, + CompositePrincipal, ManagedPolicy, } from "aws-cdk-lib/aws-iam"; import { @@ -170,11 +172,21 @@ export class S3ECPythonGithub extends cdk.Stack { }, "sts:AssumeRoleWithWebIdentity" ) + + // ToolsDevelopment role principal + const ToolsDevelopmentPrincipal = new ArnPrincipal("arn:aws:iam::" + this.account + ":role/ToolsDevelopment") + + // Composite principal to allow both GitHub Actions and ToolsDevelopment to assume the role + const CompositePrincipalForRole = new CompositePrincipal( + GithubActionsPrincipal, + ToolsDevelopmentPrincipal + ) + const S3ECGithubTestRole = new Role( this, "s3-github-test-role", { - assumedBy: GithubActionsPrincipal, + assumedBy: CompositePrincipalForRole, roleName: "S3EC-Python-Github-test-role", description: " Grant GitHub S3 put and get and KMS encrypt, decrypt, and generate access for testing", path: "/", From 4f3d29973fbd38513e03a5f81cf9024c1228fe9b Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 25 Sep 2025 17:00:09 -0700 Subject: [PATCH 070/201] m --- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 1 + 1 file changed, 1 insertion(+) 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 93f66450..3b6b664d 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 @@ -8,6 +8,7 @@ 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.nio.ByteBuffer; import java.nio.charset.StandardCharsets; From ebb03b13beb866690e55a7f202819c671276048d Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 26 Sep 2025 12:07:45 -0400 Subject: [PATCH 071/201] chore(cpp): add v3 server --- test-server/cpp-v2-server/main.cpp | 8 +- test-server/cpp-v3-server/CMakeLists.txt | 39 +++ test-server/cpp-v3-server/Makefile | 31 ++ test-server/cpp-v3-server/README.md | 37 +++ test-server/cpp-v3-server/main.cpp | 289 ++++++++++++++++++ .../amazon/encryption/s3/RoundTripTests.java | 4 +- 6 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 test-server/cpp-v3-server/CMakeLists.txt create mode 100644 test-server/cpp-v3-server/Makefile create mode 100644 test-server/cpp-v3-server/README.md create mode 100644 test-server/cpp-v3-server/main.cpp diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 869e617b..41e1daf5 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -267,18 +267,18 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, int main() { Aws::SDKOptions options; Aws::InitAPI(options); - + int port = 8085; struct MHD_Daemon *daemon = - MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, 8085, NULL, NULL, + MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, &request_handler, NULL, MHD_OPTION_END); if (!daemon) { - fprintf(stderr, "Failed to start server on port 8085\n"); + fprintf(stderr, "Failed to start server on port %d\n", port]); Aws::ShutdownAPI(options); return 1; } - fprintf(stderr, "Server running on port 8085\n"); + fprintf(stderr, "Server running on port %d\n", port); sleep(10000); MHD_stop_daemon(daemon); diff --git a/test-server/cpp-v3-server/CMakeLists.txt b/test-server/cpp-v3-server/CMakeLists.txt new file mode 100644 index 00000000..b282dbc4 --- /dev/null +++ b/test-server/cpp-v3-server/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.16) +project(s3ec-cpp-v2-server) + +set(CMAKE_CXX_STANDARD 17) + +# Configure AWS SDK build options +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") + +# Add AWS SDK as subdirectory +add_subdirectory(aws-sdk-cpp) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) + +find_package(nlohmann_json REQUIRED) + +add_executable(s3ec-server main.cpp) + +target_include_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_INCLUDE_DIRS} + /opt/homebrew/include +) + +target_link_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_LIBRARY_DIRS} + /opt/homebrew/lib +) + +target_link_libraries(s3ec-server + ${LIBMICROHTTPD_LIBRARIES} + aws-cpp-sdk-core + aws-cpp-sdk-kms + aws-cpp-sdk-s3 + 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 new file mode 100644 index 00000000..46a9fcb3 --- /dev/null +++ b/test-server/cpp-v3-server/Makefile @@ -0,0 +1,31 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8085 + +build/s3ec-server: + brew install libmicrohttpd nlohmann-json ossp-uuid + git clone --recurse-submodules -b fire-egg-dev git@github.com:awslabs/aws-sdk-cpp-staging.git aws-sdk-cpp + cd aws-sdk-cpp + mkdir -p build && cd build && cmake .. + +start-server: | build/s3ec-server + @echo "Starting Cpp V2 server..." + cd build && make && \ + 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" \ + ./s3ec-server & echo $$! > $(PID_FILE) + @echo "Cpp V2 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v3-server/README.md b/test-server/cpp-v3-server/README.md new file mode 100644 index 00000000..8e77feda --- /dev/null +++ b/test-server/cpp-v3-server/README.md @@ -0,0 +1,37 @@ +# C++ S3 Encryption Test Server + +Minimal C++ implementation of the S3 Encryption test server. + +## Dependencies + +- libmicrohttpd +- AWS SDK for C++ +- nlohmann/json +- uuid + +On MacOS you can +```bash +brew install libmicrohttpd nlohmann-json ossp-uuid +``` + +## Build + +```bash +mkdir build && cd build +cmake .. +make +``` + +## Run + +```bash +./s3ec-server +``` + +Server runs on localhost:8085 + +## API Endpoints + +- `POST /client` - Create S3 encryption client +- `GET /object/{bucket}/{key}` - Get encrypted object +- `PUT /object/{bucket}/{key}` - Put encrypted object \ No newline at end of file diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp new file mode 100644 index 00000000..b09fc56b --- /dev/null +++ b/test-server/cpp-v3-server/main.cpp @@ -0,0 +1,289 @@ +#include +#include +#include +#include +#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; + +std::string generate_uuid() { + uuid_t uuid; + uuid_generate(uuid); + char uuid_str[37]; + uuid_unparse(uuid, uuid_str); + return std::string(uuid_str); +} + +std::string get_header_value(struct MHD_Connection *connection, + const char *key) { + const char *value = + MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); + return value ? std::string(value) : ""; +} + +MHD_Result send_response(struct MHD_Connection *connection, int status_code, + const std::string &content) { + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); + MHD_Result ret = MHD_queue_response(connection, status_code, response); + MHD_destroy_response(response); + return ret; +} + +std::string make_error(const std::string &message, int status_code) { + return "{\"__type\": " + "\"software.amazon.encryption.s3#S3EncryptionClientError\", " + "\"message\": \"" + + message + "\"}"; +} + +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"]; + bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; + + auto materials = + std::make_shared(kms_key_id); + CryptoConfigurationV2 config(materials); + if (legacy) { + config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + } else { + config.SetSecurityProfile(SecurityProfile::V2); + } + + auto encryption_client = std::make_shared(config); + + std::string client_id = generate_uuid(); + client_cache[client_id] = encryption_client; + + json response = {{"clientId", client_id}}; + return send_response(connection, 200, response.dump()); + } catch (const std::exception &e) { + return send_response(connection, 500, + "{\"error\":\"An exception was thrown.\"}"); + } catch (...) { + return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); + } +} + +void fill_context(Aws::Map &map, + const std::string &metadata) { + if (metadata.empty()) { + return; + } + + // Parse metadata format: [key1]:[value1],[key2]:[value2],... + // or single pair: [key]:[value] + std::string current = metadata; + size_t pos = 0; + + while (pos < current.length()) { + // Find opening bracket for key + size_t key_start = current.find('[', pos); + if (key_start == std::string::npos) + break; + + // Find closing bracket for key + size_t key_end = current.find(']', key_start); + if (key_end == std::string::npos) + break; + + // Find colon separator + size_t colon = current.find(':', key_end); + if (colon == std::string::npos) + break; + + // Find opening bracket for value + size_t value_start = current.find('[', colon); + if (value_start == std::string::npos) + break; + + // Find closing bracket for value + size_t value_end = current.find(']', value_start); + if (value_end == std::string::npos) + break; + + // Extract key and value + std::string key = current.substr(key_start + 1, key_end - key_start - 1); + std::string value = + current.substr(value_start + 1, value_end - value_start - 1); + + // Add to map + map.emplace(key, value); + + // Move to next pair (look for comma or next opening bracket) + pos = value_end + 1; + size_t comma = current.find(',', pos); + if (comma != std::string::npos) { + pos = comma + 1; + } + } +} + +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()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + + try { + Aws::S3::Model::GetObjectRequest request; + 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); + + if (outcome.IsSuccess()) { + auto &stream = outcome.GetResult().GetBody(); + std::string content((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + return send_response(connection, 200, content); + } else { + auto msg = make_error(outcome.GetError().GetMessage(), 500); + return send_response(connection, 500, msg); + } + } catch (const std::exception &e) { + auto msg = make_error("An exception was thrown", 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) { + auto it = client_cache.find(client_id); + if (it == client_cache.end()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + + try { + Aws::Map kmsContextMap; + fill_context(kmsContextMap, metadata); + + Aws::S3::Model::PutObjectRequest request; + request.SetBucket(bucket); + request.SetKey(key); + + auto stream = std::make_shared(body); + request.SetBody(stream); + + auto outcome = it->second->PutObject(request, kmsContextMap); + if (outcome.IsSuccess()) { + json response = {{"bucket", bucket}, {"key", key}}; + return send_response(connection, 200, response.dump()); + } else { + auto msg = make_error(outcome.GetError().GetMessage(), 500); + return send_response(connection, 500, msg); + } + } catch (const std::exception &e) { + auto msg = make_error(e.what(), 500); + return send_response(connection, 500, msg); + } +} + +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; + } + return MHD_YES; + } + if (is_push && *upload_data_size) { + 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); + + if (is_push && url_str == "/client") { + std::unique_ptr body(static_cast(*con_cls)); + return handle_create_client(connection, *body); + } + + if (url_str.find("/object/") == 0) { + 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"); + 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; + return handle_put_object(connection, bucket, key, client_id, *body, metadata); + } + } + } + + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); +} + +int main() { + Aws::SDKOptions options; + Aws::InitAPI(options); + int port = 8091; + + struct MHD_Daemon *daemon = + MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, + &request_handler, NULL, MHD_OPTION_END); + + if (!daemon) { + fprintf(stderr, "Failed to start server on port %d\n", port); + Aws::ShutdownAPI(options); + return 1; + } + + fprintf(stderr, "Server running on port %d\n", port); + sleep(10000); + + MHD_stop_daemon(daemon); + Aws::ShutdownAPI(options); + fprintf(stderr, "Ending server\n"); + return 0; +} 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 cd67f2e0..893ab41b 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 @@ -61,6 +61,7 @@ public class RoundTripTests { private static final String PYTHON_V3 = "Python-V3"; private static final String GO_V3 = "Go-V3"; private static final String CPP_V2 = "CPP-V2"; + private static final String CPP_V3 = "CPP-V3"; private static final String NET_V2 = "NET-V2"; private static final String NET_V3 = "NET-V3"; private static final String PHP_V2 = "PHP-V2"; @@ -84,6 +85,7 @@ public class RoundTripTests { servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); + servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); @@ -559,7 +561,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2)) { + if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2) || language.equals(CPP_V3)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); From f4bb4b741cf18db4c9c5879ce7ce36018c6f5aba Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 26 Sep 2025 12:31:22 -0400 Subject: [PATCH 072/201] m --- .../src/it/java/software/amazon/encryption/s3/TestUtils.java | 2 ++ 1 file changed, 2 insertions(+) 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 2e78d9e5..2bb4eee6 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 @@ -35,6 +35,7 @@ public class TestUtils { public static final String PYTHON_V3 = "Python-V3"; public static final String GO_V3 = "Go-V3"; public static final String CPP_V2 = "CPP-V2"; + public static final String CPP_V3 = "CPP-V3"; public static final String NET_V2 = "NET-V2"; public static final String NET_V3 = "NET-V3"; public static final String PHP_V2 = "PHP-V2"; @@ -66,6 +67,7 @@ public class TestUtils { servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); + servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); From b20947c18de2f3d32ccf9e83a8148b93a52d200d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 26 Sep 2025 11:27:39 -0700 Subject: [PATCH 073/201] m --- .../amazon/encryption/s3/TestUtils.java | 119 +++++++++++++++--- 1 file changed, 101 insertions(+), 18 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 2e78d9e5..134e631d 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 @@ -30,18 +30,40 @@ import software.amazon.smithy.java.http.api.HttpResponse; public class TestUtils { - // Language constants - public static final String JAVA_V3 = "Java-V3"; + + // Version name constants + // Each language can have up to 3 versions: + // vN-Current: Currently released version. Does not support setting commitment policy. + // vN-Transition: Proposed feature release version. Supports reading messages encrypted with key commitment. + // vN+1: Proposed breaking release version. Supports reading/writing messages encrypted with key commitment. + + public static final String JAVA_V3_CURRENT = "Java-V3-Current"; + public static final String JAVA_V3_TRANSITION = "Java-V3-Transition"; + public static final String JAVA_V4 = "Java-V4"; + + // No Python S3EC versions are released. Only test V3 as the "vN+1" version. public static final String PYTHON_V3 = "Python-V3"; - public static final String GO_V3 = "Go-V3"; - public static final String CPP_V2 = "CPP-V2"; - public static final String NET_V2 = "NET-V2"; + + public static final String GO_V3_CURRENT = "Go-V3-Current"; + public static final String GO_V3_TRANSITION = "Go-V3-Transition"; + public static final String GO_V4 = "Go-V4"; + + public static final String NET_V2_CURRENT = "NET-V2-Current"; + public static final String NET_V2_TRANSITION = "NET-V2-Transition"; public static final String NET_V3 = "NET-V3"; - public static final String PHP_V2 = "PHP-V2"; - public static final String PHP_V3 = "PHP-V3"; - public static final String RUBY_V2 = "Ruby-V2"; + + public static final String CPP_V2_CURRENT = "CPP-V2-Current"; + public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; + public static final String CPP_V3 = "CPP-V3"; + + public static final String RUBY_V2_CURRENT = "Ruby-V2-Current"; + public static final String RUBY_V2_TRANSITION = "Ruby-V2-Transition"; public static final String RUBY_V3 = "Ruby-V3"; - + + public static final String PHP_V2_CURRENT = "PHP-V2-Current"; + public static final String PHP_V2_TRANSITION = "PHP-V2-Transition"; + public static final String PHP_V3 = "PHP-V3"; + // Test configuration constants public static final String KMS_KEY_ARN = System.getenv("TEST_SERVER_KMS_KEY_ARN") != null ? System.getenv("TEST_SERVER_KMS_KEY_ARN") : "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"; @@ -51,25 +73,62 @@ public class TestUtils { // Sets of unsupported features by language public static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of(GO_V3, PHP_V2, PHP_V3, NET_V2, NET_V3); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V3, NET_V2_CURRENT, NET_V3); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = - Set.of(NET_V2, NET_V3); + Set.of(NET_V2_CURRENT, NET_V3); + + public static final Set CURRENT_VERSIONS = + Set.of( + JAVA_V3_CURRENT, + GO_V3_CURRENT, + NET_V2_CURRENT, + CPP_V2_CURRENT, + RUBY_V2_CURRENT, + PHP_V2_CURRENT + ); + + public static final Set TRANSITION_VERSIONS = + Set.of( + JAVA_V3_TRANSITION, + GO_V3_TRANSITION, + NET_V2_TRANSITION, + CPP_V2_TRANSITION, + RUBY_V2_TRANSITION, + PHP_V2_TRANSITION + ); + + public static final Set IMPROVED_VERSIONS = + Set.of( + JAVA_V4, + PYTHON_V3, + GO_V4, + NET_V3, + CPP_V3, + RUBY_V3, + PHP_V3 + ); private static final Map serverMap; static { final Map servers = new LinkedHashMap<>(); - servers.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); + servers.put(JAVA_V3_CURRENT, new LanguageServerTarget(JAVA_V3_CURRENT, "8080")); servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); - servers.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); - servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); + servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); + servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); - servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); - servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); - servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); + servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); + servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); + servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); serverMap = filterServers(servers); } @@ -221,6 +280,30 @@ public static Stream clientsForTest() { .map(Arguments::of); } + /** + * Get stream of arguments for current version clients for testing. + */ + public static Stream currentClientsForTest() { + return clientsForTest() + .filter(arg -> CURRENT_VERSIONS.contains(arg.get()[0])); + } + + /** + * Get stream of arguments for transition version clients for testing. + */ + public static Stream transitionClientsForTest() { + return clientsForTest() + .filter(arg -> TRANSITION_VERSIONS.contains(arg.get()[0])); + } + + /** + * Get stream of arguments for improved version clients for testing. + */ + public static Stream improvedClientsForTest() { + return clientsForTest() + .filter(arg -> IMPROVED_VERSIONS.contains(arg.get()[0])); + } + /** * Provides a stream of arguments for parameterized tests that test cross-language compatibility * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption From d4fe693aeaf3fe59f04261c8907d0206d8af2fc8 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 26 Sep 2025 13:07:20 -0700 Subject: [PATCH 074/201] m --- .../software/amazon/encryption/s3/RoundTripTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 3b6b664d..1c8f839e 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 @@ -178,7 +178,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -234,7 +234,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -374,11 +374,11 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2)) { + if (language.equals(NET_V3) || language.equals(NET_V2_CURRENT) || language.equals(CPP_V2_CURRENT)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); - } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { + } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2_CURRENT)) { 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.")); } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); From 9c22750370b0fc7ff5af70ddfe2c9b1a5f4e2fb7 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 26 Sep 2025 16:31:14 -0400 Subject: [PATCH 075/201] m --- .github/workflows/test.yml | 11 ++++++++++- test-server/Makefile | 3 ++- test-server/cpp-v3-server/Makefile | 1 - 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49b125ef..3e8cda29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,19 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + + - name: Checkout CPP code + uses: actions/checkout@v5 + with: + submodules: true + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v3-server/aws-sdk-cpp/ - name: Set up Python uses: actions/setup-python@v5 diff --git a/test-server/Makefile b/test-server/Makefile index f23b738d..dd95d2da 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,7 +8,8 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +# SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 46a9fcb3..c641ed0a 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -7,7 +7,6 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid - git clone --recurse-submodules -b fire-egg-dev git@github.com:awslabs/aws-sdk-cpp-staging.git aws-sdk-cpp cd aws-sdk-cpp mkdir -p build && cd build && cmake .. From f52c5ec3dce5fd1724a4ede599f1e34e8d9c6d5b Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Fri, 26 Sep 2025 16:43:30 -0400 Subject: [PATCH 076/201] m --- test-server/Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index dd95d2da..f23b738d 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,8 +8,7 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -# SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -SERVER_DIRS := cpp-v3-server +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) From 57fcd30bb147ec57683ed17d8d88239e70329038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:24:48 -0700 Subject: [PATCH 077/201] chore: update php installation in ci (#22) --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49b125ef..0d067916 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,8 +37,7 @@ jobs: - name: Set up PHP with Composer uses: shivammathur/setup-php@v2 with: - php-version: '8.4' - tools: composer:v2 + php-version: '8.1' - name: Install PHP V2 dependencies working-directory: ./test-server/php-v2-server From 2c23d4d4c3bd4c49618358e4c790059672e80337 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 26 Sep 2025 15:32:28 -0700 Subject: [PATCH 078/201] m --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49b125ef..d2c1db4d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,3 +105,9 @@ jobs: TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} TEST_SERVER_KMS_KEY_ARN: ${{ vars.TEST_SERVER_KMS_KEY_ARN }} GRADLE_OPTS: "-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" + + - name: Upload results + uses: actions/upload-artifact@v4 + with: + name: results + path: test-server/java-tests/build/reports/tests/integ From 43dbcd8ea071522fcf1656f40b3d90007625861d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 26 Sep 2025 15:35:45 -0700 Subject: [PATCH 079/201] m --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2c1db4d..6994c286 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,6 +107,7 @@ jobs: GRADLE_OPTS: "-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" - name: Upload results + if: always() uses: actions/upload-artifact@v4 with: name: results From 04a960acab18c28008bba4ec6eb179c214412f4a Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 08:43:31 -0400 Subject: [PATCH 080/201] m --- test-server/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-server/Makefile b/test-server/Makefile index f23b738d..dd95d2da 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,7 +8,8 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +# SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) From 7c67bae3851b527f773690b26cdff9bf16c8e548 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 09:29:39 -0400 Subject: [PATCH 081/201] m --- test-server/cpp-v3-server/Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index c641ed0a..de4a319c 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -8,6 +8,11 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid cd aws-sdk-cpp + ls crt + ls crt/aws-crt-cpp/ + ls crt/aws-crt-cpp/crt/aws-c-common/ + ls crt/aws-crt-cpp/crt/aws-c-common/cmake/ + ls crt/aws-crt-cpp/crt/aws-c-common/cmake/AwsFindPackage.cmake mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server From 5879cb8700fc8b4ade18aea9f2b647a8567b7d41 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 10:36:07 -0400 Subject: [PATCH 082/201] m --- test-server/cpp-v3-server/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index de4a319c..1d72c048 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -8,6 +8,7 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid cd aws-sdk-cpp + pwd ls crt ls crt/aws-crt-cpp/ ls crt/aws-crt-cpp/crt/aws-c-common/ From 5c7601708cb23bcdd51e2e2301e6d150f144d902 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 10:36:24 -0400 Subject: [PATCH 083/201] m --- test-server/cpp-v3-server/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 1d72c048..3f21cf12 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -9,6 +9,7 @@ build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid cd aws-sdk-cpp pwd + ls ls crt ls crt/aws-crt-cpp/ ls crt/aws-crt-cpp/crt/aws-c-common/ From f541357ecef2040d9a54c83576ca17aec4163d77 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 11:03:30 -0400 Subject: [PATCH 084/201] m --- test-server/cpp-v2-server/Makefile | 2 +- test-server/cpp-v3-server/Makefile | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index ad5c951e..9e0f04b1 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -8,7 +8,7 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git - cd aws-sdk-cpp + cd aws-sdk-cpp mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 3f21cf12..fb5367e3 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -7,14 +7,13 @@ PORT := 8085 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid - cd aws-sdk-cpp pwd - ls - ls crt - ls crt/aws-crt-cpp/ - ls crt/aws-crt-cpp/crt/aws-c-common/ - ls crt/aws-crt-cpp/crt/aws-c-common/cmake/ - ls crt/aws-crt-cpp/crt/aws-c-common/cmake/AwsFindPackage.cmake + ls aws-sdk-cpp/ + ls aws-sdk-cpp/crt + ls aws-sdk-cpp/crt/aws-crt-cpp/ + ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/ + ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/cmake/ + ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/cmake/AwsFindPackage.cmake mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server From 0f1937863a4f4e87c93e3c9a4897f9263d76929a Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 11:19:04 -0400 Subject: [PATCH 085/201] m --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78213ada..fd2b2916 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - name: Checkout CPP code uses: actions/checkout@v5 with: - submodules: true + submodules: recursive token: ${{ secrets.PAT_FOR_CPP }} repository: awslabs/aws-sdk-cpp-staging ref: fire-egg-dev From a306717ea6cd269ae633f2fe3efff3dca7a1bd81 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Sat, 27 Sep 2025 12:30:46 -0400 Subject: [PATCH 086/201] m --- test-server/Makefile | 4 ++-- test-server/cpp-v3-server/Makefile | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index dd95d2da..82722d37 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,8 +8,8 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -# SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) -SERVER_DIRS := cpp-v3-server +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +# SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index fb5367e3..afbd490e 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -3,17 +3,10 @@ .PHONY: start-server stop-server wait-for-server PID_FILE := server.pid -PORT := 8085 +PORT := 8091 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid - pwd - ls aws-sdk-cpp/ - ls aws-sdk-cpp/crt - ls aws-sdk-cpp/crt/aws-crt-cpp/ - ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/ - ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/cmake/ - ls aws-sdk-cpp/crt/aws-crt-cpp/crt/aws-c-common/cmake/AwsFindPackage.cmake mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server From dab4eea5313f9a315a6314b358b9f1bd23271a48 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 11:32:07 -0400 Subject: [PATCH 087/201] m --- test-server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/Makefile b/test-server/Makefile index 82722d37..c873f7eb 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -57,7 +57,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 --parallel integ -Dtest.filter.servers="$(FILTER)" + ./gradlew --build-cache --info --parallel integ -Dtest.filter.servers="$(FILTER)" @echo "Tests completed successfully" # Stop the servers From 35ea9bf85a30621bfea40819b8a1bf70e8723b06 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 12:15:04 -0400 Subject: [PATCH 088/201] m --- test-server/cpp-v2-server/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 41e1daf5..9be401f8 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -273,7 +273,7 @@ int main() { &request_handler, NULL, MHD_OPTION_END); if (!daemon) { - fprintf(stderr, "Failed to start server on port %d\n", port]); + fprintf(stderr, "Failed to start server on port %d\n", port); Aws::ShutdownAPI(options); return 1; } From 2661f226ea1c1a394594c2954a2c55e22026752d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 29 Sep 2025 09:24:44 -0700 Subject: [PATCH 089/201] m --- .../software/amazon/encryption/s3/TestUtils.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 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 134e631d..395d11c7 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 @@ -123,12 +123,13 @@ public class TestUtils { servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); - servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); - servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); + // TODO: Create and add transition servers + // 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + // servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + // servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); serverMap = filterServers(servers); } From acde3c548999ff342c2faf8b4f0a177e199a6dbe Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 13:50:34 -0400 Subject: [PATCH 090/201] m --- .github/workflows/test.yml | 2 +- .../CMakeLists.txt | 0 .../Makefile | 0 .../README.md | 0 .../main.cpp | 0 .../amazon/encryption/s3/RoundTripTests.java | 9 +- .../amazon/encryption/s3/TestUtils.java | 122 +++++++++++++++--- 7 files changed, 108 insertions(+), 25 deletions(-) rename test-server/{cpp-v3-server => cpp-v2-transition-server}/CMakeLists.txt (100%) rename test-server/{cpp-v3-server => cpp-v2-transition-server}/Makefile (100%) rename test-server/{cpp-v3-server => cpp-v2-transition-server}/README.md (100%) rename test-server/{cpp-v3-server => cpp-v2-transition-server}/main.cpp (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd2b2916..52c068ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: token: ${{ secrets.PAT_FOR_CPP }} repository: awslabs/aws-sdk-cpp-staging ref: fire-egg-dev - path: test-server/cpp-v3-server/aws-sdk-cpp/ + path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ - name: Set up Python uses: actions/setup-python@v5 diff --git a/test-server/cpp-v3-server/CMakeLists.txt b/test-server/cpp-v2-transition-server/CMakeLists.txt similarity index 100% rename from test-server/cpp-v3-server/CMakeLists.txt rename to test-server/cpp-v2-transition-server/CMakeLists.txt diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v2-transition-server/Makefile similarity index 100% rename from test-server/cpp-v3-server/Makefile rename to test-server/cpp-v2-transition-server/Makefile diff --git a/test-server/cpp-v3-server/README.md b/test-server/cpp-v2-transition-server/README.md similarity index 100% rename from test-server/cpp-v3-server/README.md rename to test-server/cpp-v2-transition-server/README.md diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp similarity index 100% rename from test-server/cpp-v3-server/main.cpp rename to test-server/cpp-v2-transition-server/main.cpp 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 29120090..6a16ba7b 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 @@ -178,7 +178,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -234,7 +234,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { assertTrue(e.getMessage().contains("Value of encryption context from envelope does not match the provided encryption context")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -374,11 +374,12 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(NET_V3) || language.equals(NET_V2) || language.equals(CPP_V2) || language.equals(CPP_V3)) { + if (language.equals(NET_V3) || language.equals(NET_V2_CURRENT) + || language.equals(CPP_V2_CURRENT) || language.equals(CPP_V2_TRANSITION) || language.equals(CPP_V3)) { assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); - } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2)) { + } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2_CURRENT)) { 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.")); } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); 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 2bb4eee6..80939d0f 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 @@ -30,19 +30,40 @@ import software.amazon.smithy.java.http.api.HttpResponse; public class TestUtils { - // Language constants - public static final String JAVA_V3 = "Java-V3"; + + // Version name constants + // Each language can have up to 3 versions: + // vN-Current: Currently released version. Does not support setting commitment policy. + // vN-Transition: Proposed feature release version. Supports reading messages encrypted with key commitment. + // vN+1: Proposed breaking release version. Supports reading/writing messages encrypted with key commitment. + + public static final String JAVA_V3_CURRENT = "Java-V3-Current"; + public static final String JAVA_V3_TRANSITION = "Java-V3-Transition"; + public static final String JAVA_V4 = "Java-V4"; + + // No Python S3EC versions are released. Only test V3 as the "vN+1" version. public static final String PYTHON_V3 = "Python-V3"; - public static final String GO_V3 = "Go-V3"; - public static final String CPP_V2 = "CPP-V2"; - public static final String CPP_V3 = "CPP-V3"; - public static final String NET_V2 = "NET-V2"; + + public static final String GO_V3_CURRENT = "Go-V3-Current"; + public static final String GO_V3_TRANSITION = "Go-V3-Transition"; + public static final String GO_V4 = "Go-V4"; + + public static final String NET_V2_CURRENT = "NET-V2-Current"; + public static final String NET_V2_TRANSITION = "NET-V2-Transition"; public static final String NET_V3 = "NET-V3"; - public static final String PHP_V2 = "PHP-V2"; - public static final String PHP_V3 = "PHP-V3"; - public static final String RUBY_V2 = "Ruby-V2"; + + public static final String CPP_V2_CURRENT = "CPP-V2-Current"; + public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; + public static final String CPP_V3 = "CPP-V3"; + + public static final String RUBY_V2_CURRENT = "Ruby-V2-Current"; + public static final String RUBY_V2_TRANSITION = "Ruby-V2-Transition"; public static final String RUBY_V3 = "Ruby-V3"; - + + public static final String PHP_V2_CURRENT = "PHP-V2-Current"; + public static final String PHP_V2_TRANSITION = "PHP-V2-Transition"; + public static final String PHP_V3 = "PHP-V3"; + // Test configuration constants public static final String KMS_KEY_ARN = System.getenv("TEST_SERVER_KMS_KEY_ARN") != null ? System.getenv("TEST_SERVER_KMS_KEY_ARN") : "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"; @@ -52,26 +73,63 @@ public class TestUtils { // Sets of unsupported features by language public static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = - Set.of(GO_V3, PHP_V2, PHP_V3, NET_V2, NET_V3); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V3, NET_V2_CURRENT, NET_V3); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = - Set.of(NET_V2, NET_V3); + Set.of(NET_V2_CURRENT, NET_V3); + + public static final Set CURRENT_VERSIONS = + Set.of( + JAVA_V3_CURRENT, + GO_V3_CURRENT, + NET_V2_CURRENT, + CPP_V2_CURRENT, + RUBY_V2_CURRENT, + PHP_V2_CURRENT + ); + + public static final Set TRANSITION_VERSIONS = + Set.of( + JAVA_V3_TRANSITION, + GO_V3_TRANSITION, + NET_V2_TRANSITION, + CPP_V2_TRANSITION, + RUBY_V2_TRANSITION, + PHP_V2_TRANSITION + ); + + public static final Set IMPROVED_VERSIONS = + Set.of( + JAVA_V4, + PYTHON_V3, + GO_V4, + NET_V3, + CPP_V3, + RUBY_V3, + PHP_V3 + ); private static final Map serverMap; static { final Map servers = new LinkedHashMap<>(); - servers.put(JAVA_V3, new LanguageServerTarget(JAVA_V3, "8080")); + servers.put(JAVA_V3_CURRENT, new LanguageServerTarget(JAVA_V3_CURRENT, "8080")); servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); - servers.put(GO_V3, new LanguageServerTarget(GO_V3, "8082")); - servers.put(NET_V2, new LanguageServerTarget(NET_V2, "8083")); + servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); + servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - servers.put(CPP_V2, new LanguageServerTarget(CPP_V2, "8085")); - servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); - servers.put(PHP_V2, new LanguageServerTarget(PHP_V2, "8087")); - servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); - servers.put(RUBY_V2, new LanguageServerTarget(RUBY_V2, "8086")); + servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); + servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); + servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); + // TODO: Create and add transition servers + // 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + // servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + // servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); serverMap = filterServers(servers); } @@ -223,6 +281,30 @@ public static Stream clientsForTest() { .map(Arguments::of); } + /** + * Get stream of arguments for current version clients for testing. + */ + public static Stream currentClientsForTest() { + return clientsForTest() + .filter(arg -> CURRENT_VERSIONS.contains(arg.get()[0])); + } + + /** + * Get stream of arguments for transition version clients for testing. + */ + public static Stream transitionClientsForTest() { + return clientsForTest() + .filter(arg -> TRANSITION_VERSIONS.contains(arg.get()[0])); + } + + /** + * Get stream of arguments for improved version clients for testing. + */ + public static Stream improvedClientsForTest() { + return clientsForTest() + .filter(arg -> IMPROVED_VERSIONS.contains(arg.get()[0])); + } + /** * Provides a stream of arguments for parameterized tests that test cross-language compatibility * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption From b3e19793fa7ebcfb9f11a6bde6aaadcae3a2e3a5 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 14:09:24 -0400 Subject: [PATCH 091/201] m --- test-server/cpp-v2-transition-server/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index b09fc56b..32735d60 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -267,7 +267,7 @@ MHD_Result request_handler(void *cls, struct MHD_Connection *connection, int main() { Aws::SDKOptions options; Aws::InitAPI(options); - int port = 8091; + int port = 8097; struct MHD_Daemon *daemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, From 2e6c0f2a7a6f087abd12a64df5d39a76370bda3f Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 16:18:12 -0400 Subject: [PATCH 092/201] m --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ac8f941..b41cf1ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,8 +80,6 @@ jobs: uses: actions/cache@v3 with: path: | - ~/.gradle/caches - ~/.gradle/wrapper test-server/java-v3-server/.gradle test-server/java-tests/.gradle key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} From 31f1cbdaaa2fa61ac57610ca0fdf5000cb608699 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 16:21:18 -0400 Subject: [PATCH 093/201] m --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b41cf1ee..b3b8b705 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,7 @@ jobs: # Cache Gradle dependencies and build outputs - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | test-server/java-v3-server/.gradle From eb58046b39601e0f46f9c057f95b685b49eca59b Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 29 Sep 2025 13:54:14 -0700 Subject: [PATCH 094/201] Adding duvet for a specific Ruby test server (#26) Adding duvet for a specific Ruby test server --- .gitmodules | 3 + test-server/ruby-v2-server/.duvet/.gitignore | 2 + test-server/ruby-v2-server/.duvet/config.toml | 18 ++++ .../ruby-v2-server/.duvet/snapshot.txt | 83 +++++++++++++++++++ test-server/ruby-v2-server/Makefile | 8 +- test-server/specification | 1 + 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 test-server/ruby-v2-server/.duvet/.gitignore create mode 100644 test-server/ruby-v2-server/.duvet/config.toml create mode 100644 test-server/ruby-v2-server/.duvet/snapshot.txt create mode 160000 test-server/specification diff --git a/.gitmodules b/.gitmodules index ce2abc73..9af1f468 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,6 @@ path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git branch = s3ec/improved +[submodule "test-server/specification"] + path = test-server/specification + url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git diff --git a/test-server/ruby-v2-server/.duvet/.gitignore b/test-server/ruby-v2-server/.duvet/.gitignore new file mode 100644 index 00000000..0745fbc6 --- /dev/null +++ b/test-server/ruby-v2-server/.duvet/.gitignore @@ -0,0 +1,2 @@ +reports/ +requirements/ \ No newline at end of file diff --git a/test-server/ruby-v2-server/.duvet/config.toml b/test-server/ruby-v2-server/.duvet/config.toml new file mode 100644 index 00000000..0bb7d893 --- /dev/null +++ b/test-server/ruby-v2-server/.duvet/config.toml @@ -0,0 +1,18 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-ruby-sdk/gems/aws-sdk-s3/lib/**/*.rb" +comment-style = { meta = "##=", content = "##%" } + +# Include required specifications here +[[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 = true diff --git a/test-server/ruby-v2-server/.duvet/snapshot.txt b/test-server/ruby-v2-server/.duvet/snapshot.txt new file mode 100644 index 00000000..9c23c073 --- /dev/null +++ b/test-server/ruby-v2-server/.duvet/snapshot.txt @@ -0,0 +1,83 @@ +SPECIFICATION: [Content Metadata](../specification/s3-encryption/data-format/content-metadata.md) + SECTION: [Content Metadata MapKeys](#content-metadata-mapkeys) + TEXT[!MUST]: The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. + TEXT[!MUST]: The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. + TEXT[!SHOULD]: - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V1 format objects. + TEXT[!MUST]: - The mapkey "x-amz-key" MUST be present for V1 format objects. + TEXT[!MUST]: - The mapkey "x-amz-matdesc" MUST be present for V1 format objects. + TEXT[!MUST]: - The mapkey "x-amz-iv" MUST be present for V1 format objects. + TEXT[!MUST]: - The mapkey "x-amz-key-v2" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-matdesc" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-iv" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-wrap-alg" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-tag-len" MUST be present for V2 format objects. + TEXT[!MUST]: - The mapkey "x-amz-c" MUST be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-c") SHOULD be represented by a constant named "CONTENT_CIPHER_V3" or similar in the implementation code. + TEXT[!MUST]: - The mapkey "x-amz-3" MUST be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-3") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_V3" or similar in the implementation code. + TEXT[!SHOULD]: - The mapkey "x-amz-m" SHOULD be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-m") SHOULD be represented by a constant named "MAT_DESC_V3" or similar in the implementation code. + TEXT[!SHOULD]: - The mapkey "x-amz-t" SHOULD be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-t") SHOULD be represented by a constant named "ENCRYPTION_CONTEXT_V3" or similar in the implementation code. + TEXT[!MUST]: - The mapkey "x-amz-w" MUST be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-w") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_ALGORITHM_V3" or similar in the implementation code. + TEXT[!MUST]: - The mapkey "x-amz-d" MUST be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-d") SHOULD be represented by a constant named "KEY_COMMITMENT_V3" or similar in the implementation code. + TEXT[!MUST]: - The mapkey "x-amz-i" MUST be present for V3 format objects. + TEXT[!SHOULD]: - This mapkey ("x-amz-i") SHOULD be represented by a constant named "MESSAGE_ID_V3" or similar in the implementation code. + TEXT[!MUST]: In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata. + + SECTION: [Determining S3EC Object Status](#determining-s3ec-object-status) + TEXT[!MUST]: - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. + TEXT[!MUST]: - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. + TEXT[!MUST]: - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. + TEXT[!MUST]: If the object matches none of the V1/V2/V3 formats, the S3EC MUST attempt to get the instruction file. + TEXT[!SHOULD]: If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + TEXT[!SHOULD]: In general, if there is any deviation from the above format, with the exception of additional unrelated mapkeys, then the S3EC SHOULD throw an exception. + + SECTION: [V1/V2 Shared](#v1-v2-shared) + TEXT[!MAY]: This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + + SECTION: [V3 Only](#v3-only) + TEXT[!MAY]: This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + TEXT[!MUST]: The Material Description MUST only be read when there is no Encryption Context. + TEXT[!MUST]: The default Material Description value MUST be set to an empty map (`{}`). + TEXT[!MUST]: The Encryption Context value MUST take precedence over Material Description when decoding. + TEXT[!MUST]: - The wrapping algorithm value "01" MUST be translated to AESWrap upon retrieval, and vice versa on write. + TEXT[!MUST]: - The wrapping algorithm value "02" MUST be translated to AES/GCM upon retrieval, and vice versa on write. + TEXT[!MUST]: - The wrapping algorithm value "11" MUST be translated to kms upon retrieval, and vice versa on write. + TEXT[!MUST]: - The wrapping algorithm value "12" MUST be translated to kms+context upon retrieval, and vice versa on write. + TEXT[!MUST]: - The wrapping algorithm value "21" MUST be translated to RSA/ECB/OAEPWithSHA-256AndMGF1Padding upon retrieval, and vice versa on write. + TEXT[!MUST]: - The wrapping algorithm value "22" MUST be translated to RSA-OAEP-SHA1 upon retrieval, and vice versa on write. + +SPECIFICATION: [Content Metadata Strategy](../specification/s3-encryption/data-format/metadata-strategy.md) + SECTION: [Object Metadata](#object-metadata) + TEXT[!MUST]: By default, the S3EC MUST store content metadata in the S3 Object Metadata. + TEXT[!SHOULD]: The S3EC SHOULD support decoding the S3 Server's "double encoding". + TEXT[!MUST]: If the S3EC does not support decoding the S3 Server's "double encoding" then it MUST return the content metadata untouched. + + SECTION: [Instruction File](#instruction-file) + TEXT[!MUST]: The S3EC MUST support writing some or all (depending on format) content metadata to an Instruction File. + TEXT[!MUST]: The content metadata stored in the Instruction File MUST be serialized to a JSON string. + TEXT[!MUST]: The serialized JSON string MUST be the only contents of the Instruction File. + TEXT[!MUST]: Instruction File writes MUST NOT be enabled by default. + TEXT[!MUST]: Instruction File writes MUST be optionally configured during client creation or on each PutObject request. + TEXT[!MAY]: The S3EC MAY support re-encryption/key rotation via Instruction Files. + TEXT[!MUST]: The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. + TEXT[!SHOULD]: The S3EC SHOULD support providing a custom Instruction File suffix on GetObject requests, regardless of whether or not re-encryption is supported. + + SECTION: [V1/V2 Instruction Files](#v1-v2-instruction-files) + TEXT[!MUST]: In the V1/V2 message format, all of the content metadata MUST be stored in the Instruction File. + + SECTION: [V3 Instruction Files](#v3-instruction-files) + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-c" and its value in the Object Metadata when writing with an Instruction File. + TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-c" and its value in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-d" and its value in the Object Metadata when writing with an Instruction File. + TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-d" and its value in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-i" and its value in the Object Metadata when writing with an Instruction File. + TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-i" and its value in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-3" and its value in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-w" and its value in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File. + TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-t" and its value (when present in the content metadata) in the Instruction File. diff --git a/test-server/ruby-v2-server/Makefile b/test-server/ruby-v2-server/Makefile index 5d552aac..15751f6a 100644 --- a/test-server/ruby-v2-server/Makefile +++ b/test-server/ruby-v2-server/Makefile @@ -26,4 +26,10 @@ stop-server: fi wait-for-server: - $(MAKE) -C .. wait-for-port PORT=8086 \ No newline at end of file + $(MAKE) -C .. wait-for-port PORT=8086 + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/specification b/test-server/specification new file mode 160000 index 00000000..e82ef6b9 --- /dev/null +++ b/test-server/specification @@ -0,0 +1 @@ +Subproject commit e82ef6b9c29a550f89b76cd790381743b8c07ad5 From d3d1e5254e2da62a923eec7a45c0ed23fa459696 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 17:02:43 -0400 Subject: [PATCH 095/201] m --- test-server/cpp-v2-transition-server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index afbd490e..05803c78 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -3,7 +3,7 @@ .PHONY: start-server stop-server wait-for-server PID_FILE := server.pid -PORT := 8091 +PORT := 8097 build/s3ec-server: brew install libmicrohttpd nlohmann-json ossp-uuid From f4950a39ed461159151d26c20f12a0cc0ee87fe5 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Mon, 29 Sep 2025 17:03:38 -0400 Subject: [PATCH 096/201] m --- test-server/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index c873f7eb..7fd66285 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -107,7 +107,7 @@ wait-for-port: exit 1; \ fi; \ echo "Waiting for $$PORT to start ($$i/$(TIMEOUT))..."; \ - sleep 1; \ + sleep 3; \ done # Here are some helpful curl commands @@ -118,4 +118,4 @@ test-create-client: -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":false,"enableDelayedAuthenticationMode":false,"enableLegacyWrappingAlgorithms":false,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}}}' \ - http://localhost:$(PORT)/client \ No newline at end of file + http://localhost:$(PORT)/client From 4f0fdaa1f0d2f2d0cd5bcaf487a49f5d1ad246af Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 07:40:24 -0400 Subject: [PATCH 097/201] m --- .github/workflows/test.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b3b8b705..6db9866b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,15 +76,17 @@ jobs: run: pip install uv # Cache Gradle dependencies and build outputs - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - test-server/java-v3-server/.gradle - test-server/java-tests/.gradle - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- + # - name: Cache Gradle packages + # uses: actions/cache@v4 + # with: + # path: | + # ~/.gradle/caches + # ~/.gradle/wrapper + # test-server/java-v3-server/.gradle + # test-server/java-tests/.gradle + # key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + # restore-keys: | + # ${{ runner.os }}-gradle- - name: Install dependencies run: make install From 631937b4c614a892af7323a36b01f71ea1278da3 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 10:01:49 -0400 Subject: [PATCH 098/201] m --- .github/workflows/test.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6db9866b..d1fa4daf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,17 +76,17 @@ jobs: run: pip install uv # Cache Gradle dependencies and build outputs - # - name: Cache Gradle packages - # uses: actions/cache@v4 - # with: - # path: | - # ~/.gradle/caches - # ~/.gradle/wrapper - # test-server/java-v3-server/.gradle - # test-server/java-tests/.gradle - # key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - # restore-keys: | - # ${{ runner.os }}-gradle- + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + test-server/java-v3-server/.gradle + test-server/java-tests/.gradle + key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-v3-server/**/*.gradle*', 'test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*', 'test-server/java-v3-server/**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- - name: Install dependencies run: make install From 40fcdd5116df25918a7e5af15177f8ea12f34ca1 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 10:42:53 -0400 Subject: [PATCH 099/201] m --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1fa4daf..3b1ba8a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,9 @@ jobs: - name: Set up PHP with Composer uses: shivammathur/setup-php@v2 + env: + runner: self-hosted + fail-fast: true with: php-version: '8.1' From 1baaccaec9d9be2b3bc43b07b99ef46872749ffa Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 10:50:24 -0400 Subject: [PATCH 100/201] m --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b1ba8a4..43dd3bc5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,6 @@ jobs: - name: Set up PHP with Composer uses: shivammathur/setup-php@v2 env: - runner: self-hosted fail-fast: true with: php-version: '8.1' From b0c87dc1b63598490fa46eaeef0f5578c309684d Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 11:01:01 -0400 Subject: [PATCH 101/201] m --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43dd3bc5..c65c32fc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: env: fail-fast: true with: - php-version: '8.1' + php-version: '8.4' - name: Install PHP V2 dependencies working-directory: ./test-server/php-v2-server From 770fb9e7e18eb7c8cb384f7998d751ea05198039 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 12:49:28 -0400 Subject: [PATCH 102/201] m --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c65c32fc..9162a31f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: ruby-version: '3.4' - name: Set up PHP with Composer - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@verbose env: fail-fast: true with: From 7d9308e27a7e7b298d93719beb80b6ffcd8cb9e6 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 13:51:30 -0400 Subject: [PATCH 103/201] m --- .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 9162a31f..4aef8cde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,10 +45,8 @@ jobs: - name: Set up PHP with Composer uses: shivammathur/setup-php@verbose - env: - fail-fast: true with: - php-version: '8.4' + php-version: '8.1' - name: Install PHP V2 dependencies working-directory: ./test-server/php-v2-server From cc83885ef7d224d0957a37abb8438b79fa741ad4 Mon Sep 17 00:00:00 2001 From: Andy Jewell Date: Tue, 30 Sep 2025 15:32:45 -0400 Subject: [PATCH 104/201] m --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4aef8cde..a07c9a96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ on: jobs: test: - runs-on: macos-13 + runs-on: macos-14 permissions: id-token: write contents: read From b13fd0b289e0494ce39c0b0463be1604c7a1aca1 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:49:55 -0700 Subject: [PATCH 105/201] chore: add s3ec-java (transition & improved) test server (#25) --- .gitmodules | 8 + .../amazon/encryption/s3/TestUtils.java | 4 +- .../java-v3-transition-server/Makefile | 30 +++ .../java-v3-transition-server/README.md | 23 ++ .../build.gradle.kts | 55 ++++ .../gradle.properties | 11 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + test-server/java-v3-transition-server/gradlew | 249 ++++++++++++++++++ .../java-v3-transition-server/gradlew.bat | 92 +++++++ .../java-v3-transition-server/license.txt | 4 + .../java-v3-transition-server/s3ec-staging | 1 + .../settings.gradle.kts | 19 ++ .../smithy-build.json | 11 + .../s3/CreateClientOperationImpl.java | 109 ++++++++ .../encryption/s3/GetObjectOperationImpl.java | 72 +++++ .../amazon/encryption/s3/MetadataUtils.java | 43 +++ .../encryption/s3/PutObjectOperationImpl.java | 55 ++++ .../encryption/s3/S3ECJavaTestServer.java | 53 ++++ test-server/java-v4-server/Makefile | 30 +++ test-server/java-v4-server/README.md | 23 ++ test-server/java-v4-server/build.gradle.kts | 55 ++++ test-server/java-v4-server/gradle.properties | 11 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + test-server/java-v4-server/gradlew | 249 ++++++++++++++++++ test-server/java-v4-server/gradlew.bat | 92 +++++++ test-server/java-v4-server/license.txt | 4 + test-server/java-v4-server/s3ec-staging | 1 + .../java-v4-server/settings.gradle.kts | 19 ++ test-server/java-v4-server/smithy-build.json | 11 + .../s3/CreateClientOperationImpl.java | 109 ++++++++ .../encryption/s3/GetObjectOperationImpl.java | 72 +++++ .../amazon/encryption/s3/MetadataUtils.java | 43 +++ .../encryption/s3/PutObjectOperationImpl.java | 55 ++++ .../encryption/s3/S3ECJavaTestServer.java | 53 ++++ 36 files changed, 1678 insertions(+), 2 deletions(-) create mode 100644 test-server/java-v3-transition-server/Makefile create mode 100644 test-server/java-v3-transition-server/README.md create mode 100644 test-server/java-v3-transition-server/build.gradle.kts create mode 100644 test-server/java-v3-transition-server/gradle.properties create mode 100644 test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar create mode 100644 test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.properties create mode 100755 test-server/java-v3-transition-server/gradlew create mode 100644 test-server/java-v3-transition-server/gradlew.bat create mode 100644 test-server/java-v3-transition-server/license.txt create mode 160000 test-server/java-v3-transition-server/s3ec-staging create mode 100644 test-server/java-v3-transition-server/settings.gradle.kts create mode 100644 test-server/java-v3-transition-server/smithy-build.json create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java create mode 100644 test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java create mode 100644 test-server/java-v4-server/Makefile create mode 100644 test-server/java-v4-server/README.md create mode 100644 test-server/java-v4-server/build.gradle.kts create mode 100644 test-server/java-v4-server/gradle.properties create mode 100644 test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar create mode 100644 test-server/java-v4-server/gradle/wrapper/gradle-wrapper.properties create mode 100755 test-server/java-v4-server/gradlew create mode 100644 test-server/java-v4-server/gradlew.bat create mode 100644 test-server/java-v4-server/license.txt create mode 160000 test-server/java-v4-server/s3ec-staging create mode 100644 test-server/java-v4-server/settings.gradle.kts create mode 100644 test-server/java-v4-server/smithy-build.json create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java create mode 100644 test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java diff --git a/.gitmodules b/.gitmodules index 9af1f468..a2e3a730 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,6 +12,14 @@ path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git branch = s3ec/improved +[submodule "test-server/java-v3-transition-server/s3ec-staging"] + path = test-server/java-v3-transition-server/s3ec-staging + url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git + branch = s3ec/transitional +[submodule "test-server/java-v4-server/s3ec-staging"] + path = test-server/java-v4-server/s3ec-staging + url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git + branch = s3ec/improved [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git 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 80939d0f..3237ee72 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 @@ -124,13 +124,13 @@ public class TestUtils { servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers - // servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); + 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); // 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, "8090")); serverMap = filterServers(servers); } diff --git a/test-server/java-v3-transition-server/Makefile b/test-server/java-v3-transition-server/Makefile new file mode 100644 index 00000000..b4371d0a --- /dev/null +++ b/test-server/java-v3-transition-server/Makefile @@ -0,0 +1,30 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server build-s3ec + +PID_FILE := server.pid +PORT := 8094 + +build-s3ec: + @echo "Building S3EC from source..." + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile + cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "S3EC build completed." + +start-server: build-s3ec + @echo "Starting Java V3 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" \ + ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + @echo "Java V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v3-transition-server/README.md b/test-server/java-v3-transition-server/README.md new file mode 100644 index 00000000..5f08cc1c --- /dev/null +++ b/test-server/java-v3-transition-server/README.md @@ -0,0 +1,23 @@ +# S3EC Java V3 Test Server + +This is the Java implementation of the S3ECTestServer framework for S3EC Java V3. It provides a server implementation for testing Java S3 Encryption Client V3 functionality. + +## Overview + +The S3ECJavaTestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +gradle run +``` + +This will start the server running on port `8094`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v3-transition-server/build.gradle.kts b/test-server/java-v3-transition-server/build.gradle.kts new file mode 100644 index 00000000..5b3a9234 --- /dev/null +++ b/test-server/java-v3-transition-server/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") + application +} + +dependencies { + val smithyJavaVersion: String by project + + smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion") + + implementation("software.amazon.smithy:smithy-rules-engine:1.59.0") + implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") + implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") + + // S3EC from local Maven repository (installed by mvn install) + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-TRANSITION") +} + +// Use that application plugin to start the service via the `run` task. +application { + mainClass = "software.amazon.encryption.s3.S3ECJavaTestServer" +} + +// Add generated Java files to the main sourceSet +afterEvaluate { + val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen") + sourceSets { + main { + java { + srcDir(serverPath) + } + } + } +} + +tasks { + compileJava { + dependsOn(smithyBuild) + } +} + +// Helps Intellij IDE's discover smithy models +sourceSets { + main { + java { + srcDir("../model") + } + } +} + +repositories { + mavenLocal() + mavenCentral() +} diff --git a/test-server/java-v3-transition-server/gradle.properties b/test-server/java-v3-transition-server/gradle.properties new file mode 100644 index 00000000..08afce82 --- /dev/null +++ b/test-server/java-v3-transition-server/gradle.properties @@ -0,0 +1,11 @@ +# Smithy versions +smithyJavaVersion=[0,1] +smithyGradleVersion=1.1.0 +smithyVersion=[1,2] + +# Performance optimization settings +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 diff --git a/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/test-server/java-v3-transition-server/gradlew.bat b/test-server/java-v3-transition-server/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/test-server/java-v3-transition-server/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test-server/java-v3-transition-server/license.txt b/test-server/java-v3-transition-server/license.txt new file mode 100644 index 00000000..2dd564b3 --- /dev/null +++ b/test-server/java-v3-transition-server/license.txt @@ -0,0 +1,4 @@ +/* + * Example file license header. + * File header line two + */ \ No newline at end of file diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging new file mode 160000 index 00000000..d20064ea --- /dev/null +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -0,0 +1 @@ +Subproject commit d20064ea735016288b362bfbf9b0d7cd12115feb diff --git a/test-server/java-v3-transition-server/settings.gradle.kts b/test-server/java-v3-transition-server/settings.gradle.kts new file mode 100644 index 00000000..e7c41714 --- /dev/null +++ b/test-server/java-v3-transition-server/settings.gradle.kts @@ -0,0 +1,19 @@ +/** + * Basic usage of generated server stubs. + */ + +pluginManagement { + val smithyGradleVersion: String by settings + + plugins { + id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + } + + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "S3ECJavaTestServer" diff --git a/test-server/java-v3-transition-server/smithy-build.json b/test-server/java-v3-transition-server/smithy-build.json new file mode 100644 index 00000000..a0fcb8e5 --- /dev/null +++ b/test-server/java-v3-transition-server/smithy-build.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "plugins": { + "java-server-codegen": { + "service": "software.amazon.encryption.s3#S3ECTestServer", + "namespace": "software.amazon.encryption.s3", + "headerFile": "license.txt" + } + }, + "sources": ["../model"] +} 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 new file mode 100644 index 00000000..d992c435 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -0,0 +1,109 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.traits.Trait; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.S3EncryptionClient; +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.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.service.CreateClientOperation; + +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CreateClientOperationImpl implements CreateClientOperation { + private Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } + } + + return haveOneNonNull; + } + + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { + try { + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw new RuntimeException(nse); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + S3Client s3Client = S3EncryptionClient.builder() + .keyring(keyring) + .build(); + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception 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/GetObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java new file mode 100644 index 00000000..e7c5493f --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -0,0 +1,72 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.GetObjectInput; +import software.amazon.encryption.s3.model.GetObjectOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.GetObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; +import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; + +public class GetObjectOperationImpl implements GetObjectOperation { + private Map clientCache_; + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + 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))); + + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } 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/MetadataUtils.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java new file mode 100644 index 00000000..036289ec --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -0,0 +1,43 @@ +package software.amazon.encryption.s3; + +import software.amazon.encryption.s3.model.GenericServerError; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MetadataUtils { + + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; + } + + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; + } + +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java new file mode 100644 index 00000000..4c772673 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -0,0 +1,55 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.PutObjectInput; +import software.amazon.encryption.s3.model.PutObjectOutput; +import software.amazon.encryption.s3.service.PutObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; + +public class PutObjectOperationImpl implements PutObjectOperation { + + private Map clientCache_; + + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception 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 new file mode 100644 index 00000000..32af5fc1 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import software.amazon.smithy.java.server.Server; +import software.amazon.encryption.s3.service.S3ECTestServer; + +public class S3ECJavaTestServer implements Runnable { + static final URI endpoint = URI.create("http://localhost:8094"); + + public static void main(String[] args) { + new S3ECJavaTestServer().run(); + } + + @Override + public void run() { + // All the S3EC instances live here. + // 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<>(); + + Server server = Server.builder() + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); + System.out.println("Starting server..."); + server.start(); + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + System.out.println("Stopping server..."); + try { + server.shutdown().get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + } +} diff --git a/test-server/java-v4-server/Makefile b/test-server/java-v4-server/Makefile new file mode 100644 index 00000000..922499f7 --- /dev/null +++ b/test-server/java-v4-server/Makefile @@ -0,0 +1,30 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server build-s3ec + +PID_FILE := server.pid +PORT := 8090 + +build-s3ec: + @echo "Building S3EC from source..." + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile + cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "S3EC build completed." + +start-server: build-s3ec + @echo "Starting Java V4 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" \ + ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + @echo "Java V4 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v4-server/README.md b/test-server/java-v4-server/README.md new file mode 100644 index 00000000..d011daa2 --- /dev/null +++ b/test-server/java-v4-server/README.md @@ -0,0 +1,23 @@ +# S3EC Java V4 (Improved) Test Server + +This is the Java implementation of the S3ECTestServer framework for S3EC Java V4 (Improved). It provides a server implementation for testing Java S3 Encryption Client V4 (Improved) functionality. + +## Overview + +The S3ECJavaTestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +gradle run +``` + +This will start the server running on port `8090`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v4-server/build.gradle.kts b/test-server/java-v4-server/build.gradle.kts new file mode 100644 index 00000000..de3ea1f3 --- /dev/null +++ b/test-server/java-v4-server/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") + application +} + +dependencies { + val smithyJavaVersion: String by project + + smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion") + + implementation("software.amazon.smithy:smithy-rules-engine:1.59.0") + implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") + implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") + + // S3EC from local Maven repository (installed by mvn install) + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-IMPROVED") +} + +// Use that application plugin to start the service via the `run` task. +application { + mainClass = "software.amazon.encryption.s3.S3ECJavaTestServer" +} + +// Add generated Java files to the main sourceSet +afterEvaluate { + val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen") + sourceSets { + main { + java { + srcDir(serverPath) + } + } + } +} + +tasks { + compileJava { + dependsOn(smithyBuild) + } +} + +// Helps Intellij IDE's discover smithy models +sourceSets { + main { + java { + srcDir("../model") + } + } +} + +repositories { + mavenLocal() + mavenCentral() +} diff --git a/test-server/java-v4-server/gradle.properties b/test-server/java-v4-server/gradle.properties new file mode 100644 index 00000000..08afce82 --- /dev/null +++ b/test-server/java-v4-server/gradle.properties @@ -0,0 +1,11 @@ +# Smithy versions +smithyJavaVersion=[0,1] +smithyGradleVersion=1.1.0 +smithyVersion=[1,2] + +# Performance optimization settings +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 diff --git a/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/test-server/java-v4-server/gradlew.bat b/test-server/java-v4-server/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/test-server/java-v4-server/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test-server/java-v4-server/license.txt b/test-server/java-v4-server/license.txt new file mode 100644 index 00000000..2dd564b3 --- /dev/null +++ b/test-server/java-v4-server/license.txt @@ -0,0 +1,4 @@ +/* + * Example file license header. + * File header line two + */ \ No newline at end of file diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging new file mode 160000 index 00000000..a48d2b8d --- /dev/null +++ b/test-server/java-v4-server/s3ec-staging @@ -0,0 +1 @@ +Subproject commit a48d2b8d951246fef0363dd3ef2bd82c4bf04988 diff --git a/test-server/java-v4-server/settings.gradle.kts b/test-server/java-v4-server/settings.gradle.kts new file mode 100644 index 00000000..e7c41714 --- /dev/null +++ b/test-server/java-v4-server/settings.gradle.kts @@ -0,0 +1,19 @@ +/** + * Basic usage of generated server stubs. + */ + +pluginManagement { + val smithyGradleVersion: String by settings + + plugins { + id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + } + + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "S3ECJavaTestServer" diff --git a/test-server/java-v4-server/smithy-build.json b/test-server/java-v4-server/smithy-build.json new file mode 100644 index 00000000..a0fcb8e5 --- /dev/null +++ b/test-server/java-v4-server/smithy-build.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "plugins": { + "java-server-codegen": { + "service": "software.amazon.encryption.s3#S3ECTestServer", + "namespace": "software.amazon.encryption.s3", + "headerFile": "license.txt" + } + }, + "sources": ["../model"] +} 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 new file mode 100644 index 00000000..d992c435 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -0,0 +1,109 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.traits.Trait; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.S3EncryptionClient; +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.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.service.CreateClientOperation; + +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CreateClientOperationImpl implements CreateClientOperation { + private Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } + } + + return haveOneNonNull; + } + + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { + try { + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw new RuntimeException(nse); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + S3Client s3Client = S3EncryptionClient.builder() + .keyring(keyring) + .build(); + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception 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-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 new file mode 100644 index 00000000..e7c5493f --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -0,0 +1,72 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.GetObjectInput; +import software.amazon.encryption.s3.model.GetObjectOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.GetObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; +import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; + +public class GetObjectOperationImpl implements GetObjectOperation { + private Map clientCache_; + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + 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))); + + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } 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-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java new file mode 100644 index 00000000..036289ec --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -0,0 +1,43 @@ +package software.amazon.encryption.s3; + +import software.amazon.encryption.s3.model.GenericServerError; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MetadataUtils { + + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; + } + + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; + } + +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java new file mode 100644 index 00000000..4c772673 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -0,0 +1,55 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.PutObjectInput; +import software.amazon.encryption.s3.model.PutObjectOutput; +import software.amazon.encryption.s3.service.PutObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; + +public class PutObjectOperationImpl implements PutObjectOperation { + + private Map clientCache_; + + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception 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-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 new file mode 100644 index 00000000..7b73ffd1 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import software.amazon.smithy.java.server.Server; +import software.amazon.encryption.s3.service.S3ECTestServer; + +public class S3ECJavaTestServer implements Runnable { + static final URI endpoint = URI.create("http://localhost:8090"); + + public static void main(String[] args) { + new S3ECJavaTestServer().run(); + } + + @Override + public void run() { + // All the S3EC instances live here. + // 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<>(); + + Server server = Server.builder() + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); + System.out.println("Starting server..."); + server.start(); + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + System.out.println("Stopping server..."); + try { + server.shutdown().get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + } +} From 458bf9fba27b5839c42830e5764eb2b4ffaabd43 Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 30 Sep 2025 15:31:50 -0700 Subject: [PATCH 106/201] add duvet everywhere (#28) Along with instructions for how to install and run --- .github/workflows/duvet.yml | 45 ++++++++++ .github/workflows/main.yml | 5 ++ .gitmodules | 1 + test-server/Makefile | 7 ++ test-server/README.md | 26 ++++++ test-server/cpp-v2-server/.duvet/.gitignore | 3 + test-server/cpp-v2-server/.duvet/config.toml | 24 ++++++ test-server/cpp-v2-server/Makefile | 6 ++ test-server/go-v3-server/.duvet/.gitignore | 3 + test-server/go-v3-server/.duvet/config.toml | 21 +++++ test-server/go-v3-server/Makefile | 6 ++ test-server/java-v3-server/.duvet/.gitignore | 3 + test-server/java-v3-server/.duvet/config.toml | 21 +++++ test-server/java-v3-server/.gitignore | 1 + test-server/java-v3-server/Makefile | 6 ++ .../net-v2-v3-server/.duvet/.gitignore | 3 + .../net-v2-v3-server/.duvet/config.toml | 21 +++++ test-server/net-v2-v3-server/Makefile | 8 +- test-server/php-v2-server/.duvet/.gitignore | 3 + test-server/php-v2-server/.duvet/config.toml | 24 ++++++ test-server/php-v2-server/Makefile | 6 ++ test-server/php-v3-server/.duvet/.gitignore | 3 + test-server/php-v3-server/.duvet/config.toml | 24 ++++++ test-server/php-v3-server/Makefile | 6 ++ .../python-v3-server/.duvet/.gitignore | 3 + .../python-v3-server/.duvet/config.toml | 22 +++++ test-server/python-v3-server/Makefile | 6 ++ test-server/ruby-v2-server/.duvet/.gitignore | 3 +- test-server/ruby-v2-server/.duvet/config.toml | 6 +- .../ruby-v2-server/.duvet/snapshot.txt | 83 ------------------- test-server/ruby-v3-server/.duvet/.gitignore | 3 + test-server/ruby-v3-server/.duvet/config.toml | 22 +++++ test-server/ruby-v3-server/Makefile | 6 ++ test-server/specification | 2 +- 34 files changed, 345 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/duvet.yml create mode 100644 test-server/cpp-v2-server/.duvet/.gitignore create mode 100644 test-server/cpp-v2-server/.duvet/config.toml create mode 100644 test-server/go-v3-server/.duvet/.gitignore create mode 100644 test-server/go-v3-server/.duvet/config.toml create mode 100644 test-server/java-v3-server/.duvet/.gitignore create mode 100644 test-server/java-v3-server/.duvet/config.toml create mode 100644 test-server/java-v3-server/.gitignore create mode 100644 test-server/net-v2-v3-server/.duvet/.gitignore create mode 100644 test-server/net-v2-v3-server/.duvet/config.toml create mode 100644 test-server/php-v2-server/.duvet/.gitignore create mode 100644 test-server/php-v2-server/.duvet/config.toml create mode 100644 test-server/php-v3-server/.duvet/.gitignore create mode 100644 test-server/php-v3-server/.duvet/config.toml create mode 100644 test-server/python-v3-server/.duvet/.gitignore create mode 100644 test-server/python-v3-server/.duvet/config.toml delete mode 100644 test-server/ruby-v2-server/.duvet/snapshot.txt create mode 100644 test-server/ruby-v3-server/.duvet/.gitignore create mode 100644 test-server/ruby-v3-server/.duvet/config.toml diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml new file mode 100644 index 00000000..5727c38e --- /dev/null +++ b/.github/workflows/duvet.yml @@ -0,0 +1,45 @@ +name: Run Tests + +on: + workflow_call: + # Optional inputs that can be provided when calling this workflow + +jobs: + test: + runs-on: macos-latest + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Clone duvet repository + run: git clone https://github.com/awslabs/duvet.git /tmp/duvet + + - name: Build and install duvet + run: | + cd /tmp/duvet + cargo xtask build + cargo install --path ./duvet + + - name: Run duvet + if: always() + run: cd test-server && make duvet + + - name: Upload duvet reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: reports + include-hidden-files: true + path: test-server/*-server/.duvet/reports/report.html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e10b7d0d..691144d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,3 +23,8 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit + + run-duvet: + name: Run Duvet + uses: ./.github/workflows/duvet.yml + secrets: inherit diff --git a/.gitmodules b/.gitmodules index a2e3a730..68816e01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -23,3 +23,4 @@ [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git + branch = fire-egg-staging diff --git a/test-server/Makefile b/test-server/Makefile index 7fd66285..a5d83908 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -119,3 +119,10 @@ test-create-client: -H "User-Agent: smithy-java/0.0.3 ua/2.1 os/macos#15.5 lang/java#23.0.2" \ -d '{"config":{"enableLegacyUnauthenticatedModes":false,"enableDelayedAuthenticationMode":false,"enableLegacyWrappingAlgorithms":false,"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}}}' \ http://localhost:$(PORT)/client + +duvet: + @echo "Running duvet reports..." + @for dir in $(SERVER_DIRS); do \ + echo "Running make duvet in $$dir..."; \ + $(MAKE) -C $$dir duvet; \ + done \ No newline at end of file diff --git a/test-server/README.md b/test-server/README.md index 4f43f1bf..818e8ded 100644 --- a/test-server/README.md +++ b/test-server/README.md @@ -62,3 +62,29 @@ Performance optimizations have been implemented to speed up the test-server CI p - JVM optimizations For detailed information about the optimizations, see [OPTIMIZATION.md](./OPTIMIZATION.md). + +### Duvet + +To check duvet you need to install Rust. +Until the latest version of Duvet is release + +```bash + git clone https://github.com/awslabs/duvet.git /tmp/duvet + pushd /tmp/duvet + cargo xtask build + cargo install --path ./duvet + popd rm -rf /tmp/duvet +``` + +Inside each test server directory there is a `.duvet` directory that contains a `config.toml`. +This is the best way to configure `duvet`. + +You can adjust the source pattern or comment style as needed. +Examples: + +- `ruby-v2-server/.duvet/config.toml` +- `php-v2-server/.duvet/config.toml` + +There are Makefile targets, +but you can just run `make duvet` or `duvet report` inside a server directory to run the report. +To view the report `make view-report-mac` or `open .duvet/reports/report.html` diff --git a/test-server/cpp-v2-server/.duvet/.gitignore b/test-server/cpp-v2-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/cpp-v2-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/cpp-v2-server/.duvet/config.toml b/test-server/cpp-v2-server/.duvet/config.toml new file mode 100644 index 00000000..88bb7213 --- /dev/null +++ b/test-server/cpp-v2-server/.duvet/config.toml @@ -0,0 +1,24 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 9e0f04b1..cc562c1a 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -29,3 +29,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/go-v3-server/.duvet/.gitignore b/test-server/go-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/go-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/go-v3-server/.duvet/config.toml b/test-server/go-v3-server/.duvet/config.toml new file mode 100644 index 00000000..4729a668 --- /dev/null +++ b/test-server/go-v3-server/.duvet/config.toml @@ -0,0 +1,21 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "**/*.go" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/go-v3-server/Makefile b/test-server/go-v3-server/Makefile index 0ab142de..fb61e578 100644 --- a/test-server/go-v3-server/Makefile +++ b/test-server/go-v3-server/Makefile @@ -23,3 +23,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/java-v3-server/.duvet/.gitignore b/test-server/java-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/java-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/java-v3-server/.duvet/config.toml b/test-server/java-v3-server/.duvet/config.toml new file mode 100644 index 00000000..b38762ab --- /dev/null +++ b/test-server/java-v3-server/.duvet/config.toml @@ -0,0 +1,21 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "**/*.java" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/java-v3-server/.gitignore b/test-server/java-v3-server/.gitignore new file mode 100644 index 00000000..e660fd93 --- /dev/null +++ b/test-server/java-v3-server/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/test-server/java-v3-server/Makefile b/test-server/java-v3-server/Makefile index 1e0dc763..445be2ac 100644 --- a/test-server/java-v3-server/Makefile +++ b/test-server/java-v3-server/Makefile @@ -22,3 +22,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/net-v2-v3-server/.duvet/.gitignore b/test-server/net-v2-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/net-v2-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/net-v2-v3-server/.duvet/config.toml b/test-server/net-v2-v3-server/.duvet/config.toml new file mode 100644 index 00000000..04d2e812 --- /dev/null +++ b/test-server/net-v2-v3-server/.duvet/config.toml @@ -0,0 +1,21 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "**/*.cs" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile index f5b18688..a16ff57e 100644 --- a/test-server/net-v2-v3-server/Makefile +++ b/test-server/net-v2-v3-server/Makefile @@ -52,4 +52,10 @@ start-net-v3-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) \ - $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) \ No newline at end of file + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/php-v2-server/.duvet/.gitignore b/test-server/php-v2-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/php-v2-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/php-v2-server/.duvet/config.toml b/test-server/php-v2-server/.duvet/config.toml new file mode 100644 index 00000000..64b00927 --- /dev/null +++ b/test-server/php-v2-server/.duvet/config.toml @@ -0,0 +1,24 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-php-sdk/src/S3/**/*.php" + +[[source]] +pattern = "local-php-sdk/src/Crypto/**/*.php" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/php-v2-server/Makefile b/test-server/php-v2-server/Makefile index 6962ce5e..adb63258 100644 --- a/test-server/php-v2-server/Makefile +++ b/test-server/php-v2-server/Makefile @@ -22,3 +22,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/php-v3-server/.duvet/.gitignore b/test-server/php-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/php-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/php-v3-server/.duvet/config.toml b/test-server/php-v3-server/.duvet/config.toml new file mode 100644 index 00000000..64b00927 --- /dev/null +++ b/test-server/php-v3-server/.duvet/config.toml @@ -0,0 +1,24 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-php-sdk/src/S3/**/*.php" + +[[source]] +pattern = "local-php-sdk/src/Crypto/**/*.php" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/php-v3-server/Makefile b/test-server/php-v3-server/Makefile index d62be452..7b386f71 100644 --- a/test-server/php-v3-server/Makefile +++ b/test-server/php-v3-server/Makefile @@ -22,3 +22,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/python-v3-server/.duvet/.gitignore b/test-server/python-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/python-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/python-v3-server/.duvet/config.toml b/test-server/python-v3-server/.duvet/config.toml new file mode 100644 index 00000000..09dbe6d3 --- /dev/null +++ b/test-server/python-v3-server/.duvet/config.toml @@ -0,0 +1,22 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "**/*.py" +comment-style = { meta = "##=", content = "##%" } + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/python-v3-server/Makefile b/test-server/python-v3-server/Makefile index e6e9d509..0468dc87 100644 --- a/test-server/python-v3-server/Makefile +++ b/test-server/python-v3-server/Makefile @@ -26,3 +26,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/ruby-v2-server/.duvet/.gitignore b/test-server/ruby-v2-server/.duvet/.gitignore index 0745fbc6..93956e36 100644 --- a/test-server/ruby-v2-server/.duvet/.gitignore +++ b/test-server/ruby-v2-server/.duvet/.gitignore @@ -1,2 +1,3 @@ reports/ -requirements/ \ No newline at end of file +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/ruby-v2-server/.duvet/config.toml b/test-server/ruby-v2-server/.duvet/config.toml index 0bb7d893..7118cd70 100644 --- a/test-server/ruby-v2-server/.duvet/config.toml +++ b/test-server/ruby-v2-server/.duvet/config.toml @@ -9,10 +9,14 @@ comment-style = { meta = "##=", content = "##%" } source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" [report.html] enabled = true # Enable snapshots to prevent requirement coverage regressions [report.snapshot] -enabled = true +enabled = false diff --git a/test-server/ruby-v2-server/.duvet/snapshot.txt b/test-server/ruby-v2-server/.duvet/snapshot.txt deleted file mode 100644 index 9c23c073..00000000 --- a/test-server/ruby-v2-server/.duvet/snapshot.txt +++ /dev/null @@ -1,83 +0,0 @@ -SPECIFICATION: [Content Metadata](../specification/s3-encryption/data-format/content-metadata.md) - SECTION: [Content Metadata MapKeys](#content-metadata-mapkeys) - TEXT[!MUST]: The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. - TEXT[!MUST]: The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. - TEXT[!SHOULD]: - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V1 format objects. - TEXT[!MUST]: - The mapkey "x-amz-key" MUST be present for V1 format objects. - TEXT[!MUST]: - The mapkey "x-amz-matdesc" MUST be present for V1 format objects. - TEXT[!MUST]: - The mapkey "x-amz-iv" MUST be present for V1 format objects. - TEXT[!MUST]: - The mapkey "x-amz-key-v2" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-matdesc" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-iv" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-wrap-alg" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-tag-len" MUST be present for V2 format objects. - TEXT[!MUST]: - The mapkey "x-amz-c" MUST be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-c") SHOULD be represented by a constant named "CONTENT_CIPHER_V3" or similar in the implementation code. - TEXT[!MUST]: - The mapkey "x-amz-3" MUST be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-3") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_V3" or similar in the implementation code. - TEXT[!SHOULD]: - The mapkey "x-amz-m" SHOULD be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-m") SHOULD be represented by a constant named "MAT_DESC_V3" or similar in the implementation code. - TEXT[!SHOULD]: - The mapkey "x-amz-t" SHOULD be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-t") SHOULD be represented by a constant named "ENCRYPTION_CONTEXT_V3" or similar in the implementation code. - TEXT[!MUST]: - The mapkey "x-amz-w" MUST be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-w") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_ALGORITHM_V3" or similar in the implementation code. - TEXT[!MUST]: - The mapkey "x-amz-d" MUST be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-d") SHOULD be represented by a constant named "KEY_COMMITMENT_V3" or similar in the implementation code. - TEXT[!MUST]: - The mapkey "x-amz-i" MUST be present for V3 format objects. - TEXT[!SHOULD]: - This mapkey ("x-amz-i") SHOULD be represented by a constant named "MESSAGE_ID_V3" or similar in the implementation code. - TEXT[!MUST]: In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata. - - SECTION: [Determining S3EC Object Status](#determining-s3ec-object-status) - TEXT[!MUST]: - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. - TEXT[!MUST]: - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. - TEXT[!MUST]: - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. - TEXT[!MUST]: If the object matches none of the V1/V2/V3 formats, the S3EC MUST attempt to get the instruction file. - TEXT[!SHOULD]: If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. - TEXT[!SHOULD]: In general, if there is any deviation from the above format, with the exception of additional unrelated mapkeys, then the S3EC SHOULD throw an exception. - - SECTION: [V1/V2 Shared](#v1-v2-shared) - TEXT[!MAY]: This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. - - SECTION: [V3 Only](#v3-only) - TEXT[!MAY]: This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. - TEXT[!MUST]: The Material Description MUST only be read when there is no Encryption Context. - TEXT[!MUST]: The default Material Description value MUST be set to an empty map (`{}`). - TEXT[!MUST]: The Encryption Context value MUST take precedence over Material Description when decoding. - TEXT[!MUST]: - The wrapping algorithm value "01" MUST be translated to AESWrap upon retrieval, and vice versa on write. - TEXT[!MUST]: - The wrapping algorithm value "02" MUST be translated to AES/GCM upon retrieval, and vice versa on write. - TEXT[!MUST]: - The wrapping algorithm value "11" MUST be translated to kms upon retrieval, and vice versa on write. - TEXT[!MUST]: - The wrapping algorithm value "12" MUST be translated to kms+context upon retrieval, and vice versa on write. - TEXT[!MUST]: - The wrapping algorithm value "21" MUST be translated to RSA/ECB/OAEPWithSHA-256AndMGF1Padding upon retrieval, and vice versa on write. - TEXT[!MUST]: - The wrapping algorithm value "22" MUST be translated to RSA-OAEP-SHA1 upon retrieval, and vice versa on write. - -SPECIFICATION: [Content Metadata Strategy](../specification/s3-encryption/data-format/metadata-strategy.md) - SECTION: [Object Metadata](#object-metadata) - TEXT[!MUST]: By default, the S3EC MUST store content metadata in the S3 Object Metadata. - TEXT[!SHOULD]: The S3EC SHOULD support decoding the S3 Server's "double encoding". - TEXT[!MUST]: If the S3EC does not support decoding the S3 Server's "double encoding" then it MUST return the content metadata untouched. - - SECTION: [Instruction File](#instruction-file) - TEXT[!MUST]: The S3EC MUST support writing some or all (depending on format) content metadata to an Instruction File. - TEXT[!MUST]: The content metadata stored in the Instruction File MUST be serialized to a JSON string. - TEXT[!MUST]: The serialized JSON string MUST be the only contents of the Instruction File. - TEXT[!MUST]: Instruction File writes MUST NOT be enabled by default. - TEXT[!MUST]: Instruction File writes MUST be optionally configured during client creation or on each PutObject request. - TEXT[!MAY]: The S3EC MAY support re-encryption/key rotation via Instruction Files. - TEXT[!MUST]: The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. - TEXT[!SHOULD]: The S3EC SHOULD support providing a custom Instruction File suffix on GetObject requests, regardless of whether or not re-encryption is supported. - - SECTION: [V1/V2 Instruction Files](#v1-v2-instruction-files) - TEXT[!MUST]: In the V1/V2 message format, all of the content metadata MUST be stored in the Instruction File. - - SECTION: [V3 Instruction Files](#v3-instruction-files) - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-c" and its value in the Object Metadata when writing with an Instruction File. - TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-c" and its value in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-d" and its value in the Object Metadata when writing with an Instruction File. - TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-d" and its value in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-i" and its value in the Object Metadata when writing with an Instruction File. - TEXT[!MUST]: - The V3 message format MUST NOT store the mapkey "x-amz-i" and its value in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-3" and its value in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-w" and its value in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File. - TEXT[!MUST]: - The V3 message format MUST store the mapkey "x-amz-t" and its value (when present in the content metadata) in the Instruction File. diff --git a/test-server/ruby-v3-server/.duvet/.gitignore b/test-server/ruby-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/ruby-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/ruby-v3-server/.duvet/config.toml b/test-server/ruby-v3-server/.duvet/config.toml new file mode 100644 index 00000000..7118cd70 --- /dev/null +++ b/test-server/ruby-v3-server/.duvet/config.toml @@ -0,0 +1,22 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-ruby-sdk/gems/aws-sdk-s3/lib/**/*.rb" +comment-style = { meta = "##=", content = "##%" } + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/ruby-v3-server/Makefile b/test-server/ruby-v3-server/Makefile index e4492423..6e62e785 100644 --- a/test-server/ruby-v3-server/Makefile +++ b/test-server/ruby-v3-server/Makefile @@ -27,3 +27,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/specification b/test-server/specification index e82ef6b9..c534aee8 160000 --- a/test-server/specification +++ b/test-server/specification @@ -1 +1 @@ -Subproject commit e82ef6b9c29a550f89b76cd790381743b8c07ad5 +Subproject commit c534aee8c2d34c462dfac6ab21ae59467dcedd68 From 68e58530431fbd24a61ab9dc492c76e33c137b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:45:47 -0700 Subject: [PATCH 107/201] chore: add suffix to test-objects to avoid accidental collisions (#32) * chore: add suffix to test-objects to avoid accidental collisions --- .../amazon/encryption/s3/RoundTripTests.java | 14 +++++++------- .../software/amazon/encryption/s3/TestUtils.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 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 6a16ba7b..a94c561c 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 @@ -52,7 +52,7 @@ public static void setup() { @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTarget decLang) { S3ECTestServerClient encClient = testServerClientFor(encLang); - final String objectKey = "cross-lang-test-key-" + encLang; + final String objectKey = appendTestSuffix("cross-lang-test-key-" + encLang); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(KMS_KEY_ARN) @@ -92,7 +92,7 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag return; } S3ECTestServerClient encClient = testServerClientFor(encLang); - final String objectKey = "cross-lang-test-key-kms-ec-" + encLang; + final String objectKey = appendTestSuffix("cross-lang-test-key-kms-ec-" + encLang); final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); @@ -142,7 +142,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa return; } S3ECTestServerClient encClient = testServerClientFor(encLang); - final String objectKey = "cross-lang-test-key-kms-ec-subset-fails" + encLang; + final String objectKey = appendTestSuffix("cross-lang-test-key-kms-ec-subset-fails" + encLang); final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); @@ -193,7 +193,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en return; } S3ECTestServerClient encClient = testServerClientFor(encLang); - final String objectKey = "cross-lang-test-key-kms-ec-incorrect-fails" + encLang; + final String objectKey = appendTestSuffix("cross-lang-test-key-kms-ec-incorrect-fails" + encLang); final String input = "simple-test-input"; final Map encCtx = new HashMap<>(); encCtx.put("user-defined-enc-ctx-key", "user-defined-enc-ctx-value"); @@ -246,7 +246,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1Legacy(String language) { S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); - final String objectKey = "test-key-kms-v1-" + language; + final String objectKey = appendTestSuffix("test-key-kms-v1-" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(KMS_KEY_ARN) @@ -288,7 +288,7 @@ public void kmsV1Legacy(String language) { @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyWithEncCtx(String language) { S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); - final String objectKey = "test-key-kms-v1-with-enc-ctx-" + language; + final String objectKey = appendTestSuffix("test-key-kms-v1-with-enc-ctx-" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(KMS_KEY_ARN) @@ -337,7 +337,7 @@ public void kmsV1LegacyWithEncCtx(String language) { @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); - final String objectKey = "test-key-kms-v1-fails-disabled" + language; + final String objectKey = appendTestSuffix("test-key-kms-v1-fails-disabled" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(KMS_KEY_ARN) 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 3237ee72..62371b56 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,6 +17,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; import org.junit.jupiter.params.provider.Arguments; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; @@ -316,4 +318,17 @@ public static Stream crossLanguageClients() { Arguments.of(t1, t2) ))); } + + /** + * For a given string, append a suffix to distinguish it from + * simultaneous test runs. + * @param s The string to append the suffix to + * @return The string with the suffix appended + */ + public static String appendTestSuffix(final String s) { + StringBuilder stringBuilder = new StringBuilder(s); + stringBuilder.append(DateTimeFormat.forPattern("-yyMMdd-hhmmss-").print(new DateTime())); + stringBuilder.append((int) (Math.random() * 100000)); + return stringBuilder.toString(); + } } From b1b7f815629edbc9d5fd1884c71993465e924058 Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 2 Oct 2025 09:07:57 -0700 Subject: [PATCH 108/201] Add a filter to _everything_ (#30) --- test-server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/Makefile b/test-server/Makefile index a5d83908..207873c6 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,7 +8,7 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | sort) +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep $(FILTER),cat) | sort) # SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) From c41a87bc704f3006051e91826828544f68b125ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:56:26 -0700 Subject: [PATCH 109/201] chore: add php v2 transition server (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * josé can't spell --- .github/workflows/test.yml | 37 ++- .gitmodules | 6 +- .../amazon/encryption/s3/TestUtils.java | 4 +- .../.duvet/.gitignore | 3 + .../.duvet/config.toml | 24 ++ .../php-v2-transition-server/.gitignore | 4 + test-server/php-v2-transition-server/Makefile | 30 ++ .../php-v2-transition-server/composer.json | 36 +++ .../php-v2-transition-server/local-php-sdk | 1 + .../php-v2-transition-server/src/client.php | 68 ++++ .../php-v2-transition-server/src/errors.php | 42 +++ .../src/get_object.php | 85 +++++ .../php-v2-transition-server/src/index.php | 294 ++++++++++++++++++ .../src/put_object.php | 72 +++++ 14 files changed, 687 insertions(+), 19 deletions(-) create mode 100644 test-server/php-v2-transition-server/.duvet/.gitignore create mode 100644 test-server/php-v2-transition-server/.duvet/config.toml create mode 100644 test-server/php-v2-transition-server/.gitignore create mode 100644 test-server/php-v2-transition-server/Makefile create mode 100644 test-server/php-v2-transition-server/composer.json create mode 160000 test-server/php-v2-transition-server/local-php-sdk create mode 100644 test-server/php-v2-transition-server/src/client.php create mode 100644 test-server/php-v2-transition-server/src/errors.php create mode 100644 test-server/php-v2-transition-server/src/get_object.php create mode 100644 test-server/php-v2-transition-server/src/index.php create mode 100644 test-server/php-v2-transition-server/src/put_object.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a07c9a96..f99b5e2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,8 @@ on: # Optional inputs that can be provided when calling this workflow inputs: python-version: - description: 'Python version to use' - default: '3.11' + description: "Python version to use" + default: "3.11" required: false type: string @@ -16,7 +16,7 @@ jobs: permissions: id-token: write contents: read - + steps: - name: Checkout code uses: actions/checkout@v5 @@ -32,7 +32,7 @@ jobs: repository: awslabs/aws-sdk-cpp-staging ref: fire-egg-dev path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ - + - name: Set up Python uses: actions/setup-python@v5 with: @@ -41,23 +41,28 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.4' - + ruby-version: "3.4" + - name: Set up PHP with Composer uses: shivammathur/setup-php@verbose with: - php-version: '8.1' - + php-version: "8.1" + - name: Install PHP V2 dependencies working-directory: ./test-server/php-v2-server shell: bash run: composer install + - name: Install PHP V2 Transition dependencies + working-directory: ./test-server/php-v2-transition-server + shell: bash + run: composer install + - name: Install PHP V3 dependencies working-directory: ./test-server/php-v3-server shell: bash run: composer install - + - name: Install Go uses: actions/setup-go@v5 with: @@ -71,10 +76,10 @@ jobs: key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-uv- - + - name: Install Uv run: pip install uv - + # Cache Gradle dependencies and build outputs - name: Cache Gradle packages uses: actions/cache@v4 @@ -87,25 +92,25 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-v3-server/**/*.gradle*', 'test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*', 'test-server/java-v3-server/**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - + - name: Install dependencies run: make install - + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role aws-region: us-west-2 - + - name: Run unit tests run: make test-unit - + - name: Run integration tests run: make test-integration env: CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} - + - name: Run test-server tests run: cd test-server && make ci env: diff --git a/.gitmodules b/.gitmodules index 68816e01..dc1094d8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,7 @@ [submodule "test-server/php-v2-server/local-php-sdk"] path = test-server/php-v2-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git - branch = s3ec/transitional + branch = master [submodule "test-server/php-v3-server/local-php-sdk"] path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git @@ -24,3 +24,7 @@ path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git branch = fire-egg-staging +[submodule "test-server/php-v2-transition-server/local-php-sdk"] + path = test-server/php-v2-transition-server/local-php-sdk + url = git@github.com:aws/private-aws-sdk-php-staging.git + branch = s3ec/transitional 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 62371b56..4818b2a8 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 @@ -75,7 +75,7 @@ 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_V3, NET_V2_CURRENT, NET_V3); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3, NET_V2_CURRENT, NET_V3); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2_CURRENT, NET_V3); @@ -131,7 +131,7 @@ public class TestUtils { // servers.put(NET_V2_TRANSITION, new LanguageServerTarget(NET_V2_TRANSITION, "8096")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); // servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); - // servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); + servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); servers.put(JAVA_V4, new LanguageServerTarget(JAVA_V4, "8090")); serverMap = filterServers(servers); } diff --git a/test-server/php-v2-transition-server/.duvet/.gitignore b/test-server/php-v2-transition-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/php-v2-transition-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/php-v2-transition-server/.duvet/config.toml b/test-server/php-v2-transition-server/.duvet/config.toml new file mode 100644 index 00000000..64b00927 --- /dev/null +++ b/test-server/php-v2-transition-server/.duvet/config.toml @@ -0,0 +1,24 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-php-sdk/src/S3/**/*.php" + +[[source]] +pattern = "local-php-sdk/src/Crypto/**/*.php" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/php-v2-transition-server/.gitignore b/test-server/php-v2-transition-server/.gitignore new file mode 100644 index 00000000..07108589 --- /dev/null +++ b/test-server/php-v2-transition-server/.gitignore @@ -0,0 +1,4 @@ +vendor/* +cookies.txt +server.pid +composer.lock \ No newline at end of file diff --git a/test-server/php-v2-transition-server/Makefile b/test-server/php-v2-transition-server/Makefile new file mode 100644 index 00000000..536d5cdb --- /dev/null +++ b/test-server/php-v2-transition-server/Makefile @@ -0,0 +1,30 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8099 + +start-server: + @echo "Starting PHP V2 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" \ + composer run start & echo $$! > $(PID_FILE) + @echo "PHP V2 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/php-v2-transition-server/composer.json b/test-server/php-v2-transition-server/composer.json new file mode 100644 index 00000000..6a0f263b --- /dev/null +++ b/test-server/php-v2-transition-server/composer.json @@ -0,0 +1,36 @@ +{ + "name": "aws/s3ec-php-v2-transition-test-server", + "description": "PHP V2 Transition implementation of the S3EC Test Server framework", + "type": "project", + "license": "Apache-2.0", + "repositories": [ + { + "type": "path", + "url": "./local-php-sdk", + "options": { + "symlink": true + } + } + ], + "require": { + "php": ">=7.4", + "aws/aws-sdk-php": "@dev", + "ramsey/uuid": "^4.9" + }, + "autoload": { + "psr-4": { + "S3EC\\PhpV2Server\\": "src/" + } + }, + "scripts": { + "start": [ + "php -S 0.0.0.0:8099 src/index.php" + ] + }, + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.1" + } + } +} \ No newline at end of file diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk new file mode 160000 index 00000000..d78bd3b2 --- /dev/null +++ b/test-server/php-v2-transition-server/local-php-sdk @@ -0,0 +1 @@ +Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 diff --git a/test-server/php-v2-transition-server/src/client.php b/test-server/php-v2-transition-server/src/client.php new file mode 100644 index 00000000..44fe1b39 --- /dev/null +++ b/test-server/php-v2-transition-server/src/client.php @@ -0,0 +1,68 @@ +toString(); + $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + + if ($configData == []) { + return GenericServerError("Invalid config in request body", 400); + } + if (($keyMaterial || $kmsKeyId) === null) { + return GenericServerError("Invalid keyMaterial in config", 400); + } + + // Store client configuration instead of objects (AWS objects can't be serialized) + $_SESSION['s3ecCache'][$clientId] = [ + 's3Config' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsConfig' => [ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ], + 'kmsKeyId' => $kmsKeyId, + 'legacy' => $legacyAlgorithms, + 'created' => time() + ]; + + // Auto-update cookies.txt with current session ID so tests can access cached clients + writeSessionIdToCookiesFile(session_id()); + + header("Content-Type: application/json"); + return json_encode([ + 'clientId' => $clientId, + ]); +} diff --git a/test-server/php-v2-transition-server/src/errors.php b/test-server/php-v2-transition-server/src/errors.php new file mode 100644 index 00000000..67449c11 --- /dev/null +++ b/test-server/php-v2-transition-server/src/errors.php @@ -0,0 +1,42 @@ + 'GenericServerError', + 'message' => $message + ]; + + return json_encode($errorResponse); +} + +/** + * Used for modeled errors, e.g. errors thrown by the S3EC + * Tests SHOULD expect this error in negative tests. + * + * @param string $message The error message to include in the response + * @return string JSON-encoded error response + */ +function S3EncryptionClientError($message) +{ + http_response_code(500); + header('Content-Type: application/json'); + + $errorResponse = [ + "__type" => "software.amazon.encryption.s3#S3EncryptionClientError", + 'message' => $message + ]; + + return json_encode($errorResponse); +} diff --git a/test-server/php-v2-transition-server/src/get_object.php b/test-server/php-v2-transition-server/src/get_object.php new file mode 100644 index 00000000..41875f54 --- /dev/null +++ b/test-server/php-v2-transition-server/src/get_object.php @@ -0,0 +1,85 @@ +getObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + 'Bucket' => $bucket, + 'Key' => $key, + ]); + + // Capture and discard any unwanted output from AWS SDK + $unwantedOutput = ob_get_clean(); + if (!empty($unwantedOutput)) { + error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes"); + } + + $body = $result['Body']->getContents(); + $formattedMetadata = formatMetadataForResponse($result["Metadata"]); + + // Now set headers safely + header("Content-Metadata: " . $formattedMetadata); + header("Content-Type: application/octet-stream"); + header("Content-Length: " . strlen($body)); + return $body; + } catch (InvalidArgumentException $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + return GenericServerError("Invalid argument: " . $e->getMessage(), 400); + } catch (Exception $e) { + // Clean up output buffer if still active + if (ob_get_level()) { + ob_end_clean(); + } + if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { + return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); + } else { + return GenericServerError("Server error: " . $e->getMessage(), 500); + } + } +} diff --git a/test-server/php-v2-transition-server/src/index.php b/test-server/php-v2-transition-server/src/index.php new file mode 100644 index 00000000..cc5dee29 --- /dev/null +++ b/test-server/php-v2-transition-server/src/index.php @@ -0,0 +1,294 @@ += 7 && $parts[5] === 'PHPSESSID') { + error_log("Found session ID in cookies.txt: " . $parts[6]); + return $parts[6]; // Return the session ID value + } + } + + error_log("No PHPSESSID found in cookies.txt file"); + return null; +} + +// Function to write session ID to cookies.txt file +function writeSessionIdToCookiesFile($sessionId) +{ + $cookiesFile = __DIR__ . '/../cookies.txt'; + + // Create Netscape cookie format entry + $cookieLine = "localhost\tFALSE\t/\tFALSE\t0\tPHPSESSID\t$sessionId"; + + // Write header and cookie entry + $content = "# Netscape HTTP Cookie File\n"; + $content .= "# https://curl.se/docs/http-cookies.html\n"; + $content .= "# This file was generated by libcurl! Edit at your own risk.\n\n"; + $content .= $cookieLine . "\n"; + + $result = file_put_contents($cookiesFile, $content); + + if ($result === false) { + error_log("Failed to write session ID to cookies.txt file: $cookiesFile"); + return false; + } + + error_log("Successfully wrote session ID to cookies.txt: $sessionId"); + return true; +} + +set_time_limit(600); +// Start session to persist cache across requests +// First try to use session ID from cookies.txt if available +$sessionId = getSessionIdFromCookiesFile(); +if ($sessionId) { + session_id($sessionId); +} +session_start(); + +// Initialize session cache if it doesn't exist +if (!isset($_SESSION['s3ecCache'])) { + $_SESSION['s3ecCache'] = []; +} + +// Simple router class +class SimpleRouter +{ + private $routes = []; + + public function addRoute($method, $path, $handler) + { + $this->routes[] = [ + 'method' => strtoupper($method), + 'path' => $path, + 'handler' => $handler + ]; + } + + public function handleRequest() + { + $method = $_SERVER['REQUEST_METHOD']; + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + + foreach ($this->routes as $route) { + if ($route['method'] === $method) { + $params = $this->matchPathWithParams($route['path'], $path); + if ($params !== false) { + return call_user_func($route['handler'], $params); + } + } + } + + // Default 404 response + http_response_code(404); + return json_encode(['error' => 'Not Found']); + } + + private function matchPathWithParams($routePath, $requestPath) + { + // Handle exact matches first (for routes without parameters) + if ($routePath === $requestPath) { + return []; + } + + // Convert route path like '/object/{bucket}/{key}' to regex + $pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath); + $pattern = '/^' . str_replace('/', '\/', $pattern) . '$/'; + + if (preg_match($pattern, $requestPath, $matches)) { + array_shift($matches); // Remove full match + + // Extract parameter names + preg_match_all('/\{([^}]+)\}/', $routePath, $paramNames); + $params = []; + + foreach ($paramNames[1] as $index => $paramName) { + $params[$paramName] = $matches[$index] ?? null; + } + + return $params; + } + + return false; + } +} + +// Helper function to get cached client by ID +function getCachedClient($clientId) +{ + if (!isset($_SESSION['s3ecCache'][$clientId])) { + return null; + } + + $config = $_SESSION['s3ecCache'][$clientId]; + + // Recreate the AWS clients from stored configuration + $s3Client = new S3Client($config['s3Config']); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient($config['kmsConfig']); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider, + 'config' => $config + ]; +} + +function createDefaultClientTuple(): array +{ + $s3Client = new S3Client([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $encryptionClient = new S3EncryptionClientV2($s3Client); + + $kmsClient = new KmsClient([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'http' => [ + 'debug' => false, + 'verify' => true, + 'curl' => [ + CURLOPT_VERBOSE => false, + CURLOPT_NOPROGRESS => true + ] + ] + ]); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); + + return [ + 'encryptionClient' => $encryptionClient, + 'materialsProvider' => $materialsProvider + ]; +} + +function metadataStringToMap($metadata): array +{ + $md = []; + + if (empty($metadata)) { + return $md; + } + + $mdList = explode(',', $metadata); + + foreach ($mdList as $entry) { + $parts = explode(']:[', $entry); + + if (count($parts) === 2) { + $key = substr($parts[0], 1); + $value = substr($parts[1], 0, -1); + $md[$key] = $value; + } else { + throw new InvalidArgumentException("Malformed metadata list entry: " . $entry); + } + } + + return $md; +} +function formatMetadataForResponse($metadata) +{ + $metadataList = []; + // Handle different metadata input types + if (is_array($metadata)) { + // If it's an associative array (like Python dict) + foreach ($metadata as $key => $value) { + $metadataList[] = $key . '=' . $value; + } + } elseif (is_string($metadata) && !empty($metadata)) { + // If it's already a string, assume it's in the correct format + return $metadata; + } + + // Convert array to comma-separated string + return implode(',', $metadataList); +} + +// Initialize router +$router = new SimpleRouter(); + +// Add basic routes +$router->addRoute('GET', '/', function () { + return json_encode([ + 'service' => 'S3EC PHP v2 Test Server', + 'status' => 'running', + 'port' => 8087, + 'endpoints' => [ + 'GET /' => 'Server status', + 'POST /client' => 'Create an S3EncryptionClient and cache it.', + 'GET /object/{bucket}/{key}' => 'Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a GetObject request to S3.', + 'PUT /object/{bucket}/{key}' => 'Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a PutObject request to S3.', + ] + ]); +}); + +$router->addRoute('GET', '/cache', function () { + return json_encode([ + 'sessionId' => session_id(), + 'sessionStatus' => session_status(), + 'totalCachedClients' => count($_SESSION['s3ecCache'] ?? []), + 'allClientIds' => array_keys($_SESSION['s3ecCache'] ?? []), + 'cacheDetails' => $_SESSION['s3ecCache'] ?? [] + ]); +}); + +$router->addRoute('GET', '/object/{bucket}/{key}', function ($params) { + return handleGetObject($params); +}); + +$router->addRoute('PUT', '/object/{bucket}/{key}', function ($params) { + return handlePutObject($params); +}); + +$router->addRoute('POST', '/client', function () { + return handleCreateClient(); +}); + +// Handle the request and output response +$result = $router->handleRequest(); +if ($result !== false) { + echo $result; +} diff --git a/test-server/php-v2-transition-server/src/put_object.php b/test-server/php-v2-transition-server/src/put_object.php new file mode 100644 index 00000000..c7de4bb4 --- /dev/null +++ b/test-server/php-v2-transition-server/src/put_object.php @@ -0,0 +1,72 @@ + 'gcm', + 'KeySize' => 256, + ]; + $legacyConfig = $s3ecClientTuple["legacy"] ?? false; + $legacy = null; + if ($legacyConfig === false) { + $legacy = "V2"; + } else { + $legacy = "V2_AND_LEGACY"; + } + + try { + $result = $s3ec->putObject([ + '@SecurityProfile' => $legacy, + '@MaterialsProvider' => $materialProvider, + '@KmsEncryptionContext' => $encryptionContext, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucket, + 'Key' => $key, + 'Body' => $rawBody, + ]); + + header("Content-Type: application/json"); + return json_encode([ + "bucket" => $bucket, + "key" => $key, + // php for some reason blows java's heap if we pass the metadata + // "metadata" => $encryptionContext + ]); + + } catch (InvalidArgumentException $e) { + return S3EncryptionClientError("Invalid argument: " . $e->getMessage()); + } catch (Exception $e) { + return GenericServerError("Server error: " . $e->getMessage()); + } +} From cb01d31ebc8f86f46ae811de3dea3b8844f8cfdc Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 9 Oct 2025 09:08:42 -0700 Subject: [PATCH 110/201] feat(test-server): Add Go V4 test server (#12) --- .github/workflows/test.yml | 3 + .gitmodules | 3 + test-server/go-v4-server/Makefile | 25 ++ test-server/go-v4-server/README.md | 23 ++ test-server/go-v4-server/go.mod | 35 ++ test-server/go-v4-server/go.sum | 45 +++ test-server/go-v4-server/local-go-s3ec | 1 + test-server/go-v4-server/main.go | 350 ++++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 1 + 9 files changed, 486 insertions(+) create mode 100644 test-server/go-v4-server/Makefile create mode 100644 test-server/go-v4-server/README.md create mode 100644 test-server/go-v4-server/go.mod create mode 100644 test-server/go-v4-server/go.sum create mode 160000 test-server/go-v4-server/local-go-s3ec create mode 100644 test-server/go-v4-server/main.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f99b5e2a..bc8e2f45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,9 @@ jobs: uses: actions/checkout@v5 with: submodules: true + # This is Ryan Emery's (seebees) PAT. + # To grant this workflow access to a new private repo, + # ask Ryan to edit this PAT's permissions to add access to a new private repo. token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - name: Checkout CPP code diff --git a/.gitmodules b/.gitmodules index dc1094d8..659db28b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,6 +12,9 @@ path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git branch = s3ec/improved +[submodule "test-server/go-v4-server/local-go-s3ec"] + path = test-server/go-v4-server/local-go-s3ec + url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging [submodule "test-server/java-v3-transition-server/s3ec-staging"] path = test-server/java-v3-transition-server/s3ec-staging url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git diff --git a/test-server/go-v4-server/Makefile b/test-server/go-v4-server/Makefile new file mode 100644 index 00000000..a8a2553d --- /dev/null +++ b/test-server/go-v4-server/Makefile @@ -0,0 +1,25 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8089 + +start-server: + @echo "Starting Go V4 server..." + go mod tidy + 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" \ + go run . & echo $$! > $(PID_FILE) + @echo "Go V4 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/go-v4-server/README.md b/test-server/go-v4-server/README.md new file mode 100644 index 00000000..d97a37bf --- /dev/null +++ b/test-server/go-v4-server/README.md @@ -0,0 +1,23 @@ +# S3EC Go V4 Test Server + +This is the Go implementation of the S3ECTestServer framework for S3EC Go V4. It provides a server implementation for testing Go S3 Encryption Client V4 functionality. + +## Overview + +The S3EC Go test server implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +go run . +``` + +This will start the server running on port `8089`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/go-v4-server/go.mod b/test-server/go-v4-server/go.mod new file mode 100644 index 00000000..4ab1895c --- /dev/null +++ b/test-server/go-v4-server/go.mod @@ -0,0 +1,35 @@ +module github.com/aws/amazon-s3-encryption-client-python/test-server/go-server + +go 1.21 + +require ( + github.com/aws/amazon-s3-encryption-client-go/v4 v4.0.0 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 + github.com/google/uuid v1.5.0 + github.com/gorilla/mux v1.8.1 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) + +// S3EC Go V4 is not released to pkg.go.dev as of writing. +// It is included as a submodule and referenced locally. +replace github.com/aws/amazon-s3-encryption-client-go/v4 => ./local-go-s3ec/v4 diff --git a/test-server/go-v4-server/go.sum b/test-server/go-v4-server/go.sum new file mode 100644 index 00000000..1bb969a3 --- /dev/null +++ b/test-server/go-v4-server/go.sum @@ -0,0 +1,45 @@ + +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec new file mode 160000 index 00000000..cbb8bc60 --- /dev/null +++ b/test-server/go-v4-server/local-go-s3ec @@ -0,0 +1 @@ +Subproject commit cbb8bc608754ae52f8063987d0570a7c5a927fa0 diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go new file mode 100644 index 00000000..75871d5f --- /dev/null +++ b/test-server/go-v4-server/main.go @@ -0,0 +1,350 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/aws/amazon-s3-encryption-client-go/v4/client" + "github.com/aws/amazon-s3-encryption-client-go/v4/materials" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" + "github.com/gorilla/mux" +) + +// Server represents the Go test server +type Server struct { + clientCache map[string]*client.S3EncryptionClientV4 + kmsClient *kms.Client +} + +// CreateClientInput represents the input for creating a client +type CreateClientInput struct { + Config S3ECConfig `json:"config"` +} + +// CreateClientOutput represents the output for creating a client +type CreateClientOutput struct { + ClientID string `json:"clientId"` +} + +// S3ECConfig represents the S3 encryption client configuration +type S3ECConfig struct { + EnableLegacyUnauthenticatedModes bool `json:"enableLegacyUnauthenticatedModes"` + EnableDelayedAuthenticationMode bool `json:"enableDelayedAuthenticationMode"` + EnableLegacyWrappingAlgorithms bool `json:"enableLegacyWrappingAlgorithms"` + SetBufferSize int64 `json:"setBufferSize"` + KeyMaterial KeyMaterial `json:"keyMaterial"` +} + +// KeyMaterial represents the key material for encryption +type KeyMaterial struct { + RSAKey []byte `json:"rsaKey"` + AESKey []byte `json:"aesKey"` + KMSKeyID string `json:"kmsKeyId"` +} + +// PutObjectOutput represents the output for put object operation +type PutObjectOutput struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + Metadata []string `json:"metadata"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Type string `json:"__type"` + Message string `json:"message"` +} + +// NewServer creates a new server instance +func NewServer() (*Server, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + return &Server{ + clientCache: make(map[string]*client.S3EncryptionClientV4), + kmsClient: kms.NewFromConfig(cfg), + }, nil +} + +// createGenericServerError creates a generic server error response +func (s *Server) createGenericServerError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("[Go V4] GenericServerError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#GenericServerError", + Message: message, + }) +} + +// createS3EncryptionClientError creates an S3 encryption client error response +func (s *Server) createS3EncryptionClientError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("[Go V4] S3EncryptionClientError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#S3EncryptionClientError", + Message: message, + }) +} + +// metadataStringToMap converts metadata string to map +func metadataStringToMap(mdString string) (map[string]string, error) { + md := make(map[string]string) + if mdString == "" { + return md, nil + } + + mdList := strings.Split(mdString, ",") + for _, entry := range mdList { + // Split on "]:[" to separate key and value + parts := strings.Split(entry, "]:[") + if len(parts) == 2 { + // Remove remaining brackets from start and end + key := parts[0][1:] // Remove first character + value := parts[1][:len(parts[1])-1] // Remove last character + md[key] = value + } else { + return nil, fmt.Errorf("malformed metadata list entry: %s", entry) + } + } + return md, nil +} + +// createClient handles POST /client +func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { + // Read body + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var input CreateClientInput + if err := json.Unmarshal(body, &input); err != nil { + s.createGenericServerError(w, "Invalid JSON in request body", http.StatusBadRequest) + return + } + + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) + return + } + + // Create KMS keyring + kmsClient := kms.NewFromConfig(cfg) + keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { + options.EnableLegacyWrappingAlgorithms = input.Config.EnableLegacyWrappingAlgorithms + }) + cmm, err := materials.NewCryptographicMaterialsManager(keyring) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create CMM: %v", err), http.StatusInternalServerError) + return + } + + // Create S3 encryption client + var s3EncryptionClient *client.S3EncryptionClientV4 + s3PlaintextClient := s3.NewFromConfig(cfg) + s3EncryptionClient, err = client.New(s3PlaintextClient, cmm) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create S3EC: %v", err), http.StatusInternalServerError) + return + } + + // Generate client ID + clientID := uuid.New().String() + + // Store client in cache + s.clientCache[clientID] = s3EncryptionClient + + // Return response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CreateClientOutput{ + ClientID: clientID, + }) +} + +// putObject handles PUT /object/{bucket}/{key} +func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Read body + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + // Create context with encryption context + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + // Create put object input + putInput := &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + Body: strings.NewReader(string(body)), + } + + // Add metadata if present + if len(encCtx) > 0 { + putInput.Metadata = encCtx + } + + // Make the put object request using the encryption client + _, err = client.PutObject(encryptionContext, putInput) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to put object: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("[Go V4] PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + + // Return response + w.Header().Set("Content-Type", "application/json") + resp := PutObjectOutput{ + Bucket: bucket, + Key: key, + Metadata: []string{}, // TODO: pass metadata back in response + } + json.NewEncoder(w).Encode(resp) +} + +// getObject handles GET /object/{bucket}/{key} +func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + // Create get object input + getInput := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + + // Make the get object request using the encryption client + result, err := client.GetObject(encryptionContext, getInput) + if err != nil { + errMsg := err.Error() + // Shim the S3EC error message to the error message expected by the test server. + // We don't want to change the S3EC error message but the test server expects a specific error message; + // This is the appropriate place to rewrite the error message. + if strings.Contains(errMsg, "to decrypt x-amz-cek-alg value `kms` you must enable legacyWrappingAlgorithms on the keyring") { + s.createS3EncryptionClientError(w, "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms", http.StatusInternalServerError) + return + } + + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to get object: %v", err), http.StatusInternalServerError) + return + } + defer result.Body.Close() + + // Read the body + body, err := io.ReadAll(result.Body) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to read object body: %v", err), http.StatusInternalServerError) + return + } + + // Convert metadata to string format + var metadataList []string + if result.Metadata != nil { + for k, v := range result.Metadata { + metadataList = append(metadataList, fmt.Sprintf("%s=%s", k, v)) + } + } + + metadataStr := strings.Join(metadataList, ",") + + log.Printf("[Go V4] GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + + // Set response headers + w.Header().Set("Content-Metadata", metadataStr) + + // Return the body as response + w.Write(body) +} + +func main() { + server, err := NewServer() + if err != nil { + log.Fatalf("[Go V4] Failed to create Go V4 server: %v", err) + } + + r := mux.NewRouter() + + // Register routes + r.HandleFunc("/client", server.createClient).Methods("POST") + r.HandleFunc("/object/{bucket}/{key}", server.putObject).Methods("PUT") + r.HandleFunc("/object/{bucket}/{key}", server.getObject).Methods("GET") + + fmt.Println("[Go V4] Starting Go V4 server on :8089...") + log.Fatal(http.ListenAndServe(":8089", r)) +} 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 4818b2a8..2e5bb8d6 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 @@ -123,6 +123,7 @@ public class TestUtils { servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); + servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers From 27459f39370eaf3937db35c95520d31fc45f1f68 Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 10 Oct 2025 09:24:45 -0700 Subject: [PATCH 111/201] KC and current tests working for Improved Ruby (#34) --- .gitignore | 1 + test-server/Makefile | 2 +- test-server/java-tests/README.md | 4 +- .../s3/ExhaustiveRoundTripTests1_25.java | 221 ++++++++++++++++++ .../amazon/encryption/s3/RoundTripTests.java | 68 ++++-- .../amazon/encryption/s3/TestUtils.java | 102 ++++++-- test-server/model/client.smithy | 9 +- test-server/model/object.smithy | 33 +++ test-server/ruby-v2-server/Gemfile.lock | 4 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/.duvet/config.toml | 5 + test-server/ruby-v3-server/Gemfile.lock | 4 +- test-server/ruby-v3-server/app.rb | 4 +- .../ruby-v3-server/lib/client_manager.rb | 24 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- test-server/specification | 2 +- 16 files changed, 431 insertions(+), 56 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java diff --git a/.gitignore b/.gitignore index 0e29a9fb..9a2c0f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ # Distribution / packaging dist/ build/ +bin/ *.egg-info/ # Uv diff --git a/test-server/Makefile b/test-server/Makefile index 207873c6..d2b91a5d 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -8,7 +8,7 @@ all: start-servers run-tests # CI target for GitHub Actions ci: start-servers run-tests stop-servers -SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep $(FILTER),cat) | sort) +SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) # SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) diff --git a/test-server/java-tests/README.md b/test-server/java-tests/README.md index eee84863..2a9b80ee 100644 --- a/test-server/java-tests/README.md +++ b/test-server/java-tests/README.md @@ -1,8 +1,8 @@ -## Java Tests +# Java Tests This project contains Java client tests for the S3 Encryption Client. -### Running Tests +## Running Tests To run the integration tests for this project: 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 new file mode 100644 index 00000000..eb48d6bd --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java @@ -0,0 +1,221 @@ +/* + * 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.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +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 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.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 + * + * Tests 1-25 are included in this file. + */ +public class ExhaustiveRoundTripTests1_25 { + + @BeforeAll + public static void setup() { + TestUtils.validateServersRunning(); + } + + // Begin Exhaustive tests defined here: + // https://tiny.amazon.com/3xnzwczl/loopcloumicrpeyJ3 + + + // Exhaustive test 2 + // Outcome Version Operation Policy Content Encryption + // Pass Improved Decrypt ForbidEncryptAllowDecrypt CBC + + @ParameterizedTest(name = "{displayName} for Encrypt: Java-V1, Decrypt: {0}") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + public void GIVEN_CBCEncryptedData_AND_ImprovedClientDecryptingWithForbidEncryptAllowDecrypt_WHEN_Decrypt_THEN_Pass( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + final String objectKey = "test-key-kms-v1-" + language; + final String input = "simple-test-input"; + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + // Create the object using the old client + // V1 Client + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); + + CryptoConfiguration v1Config = + new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) + .withStorageMode(CryptoStorageMode.ObjectMetadata) + .withAwsKmsRegion(TestUtils.KMS_REGION); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() + .withCryptoConfiguration(v1Config) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(TestUtils.BUCKET, objectKey, input); + + S3ECTestServerClient decClient = TestUtils.testServerClientFor(language); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .enableLegacyWrappingAlgorithms(true) + .build() + ) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + // When: decrypt KC object with a current version client + GetObjectOutput output = decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Then: Pass + } + + // Exhaustive test 3 + // Outcome Version Operation Policy Content Encryption + // Pass Improved Decrypt ForbidEncryptAllowDecrypt GCM + + @ParameterizedTest(name = "{displayName} for Encrypt: Java-V1-GCM, Decrypt: {0}") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + public void GIVEN_GCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncryptAllowDecrypt_WHEN_Decrypt_THEN_Pass( + TestUtils.LanguageServerTarget language + ) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + final String objectKey = "test-key-kms-v1-gcm-" + language; + final String input = "simple-test-input"; + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + CreateClientOutput output1 = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .enableLegacyWrappingAlgorithms(true) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String s3ECId = output1.getClientId(); + + // Create the object using the old client with GCM encryption + // V1 Client with GCM + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(TestUtils.KMS_KEY_ARN); + + CryptoConfiguration v1Config = + new CryptoConfiguration(CryptoMode.StrictAuthenticatedEncryption) // StrictAuthenticatedEncryption uses GCM + .withStorageMode(CryptoStorageMode.ObjectMetadata) + .withAwsKmsRegion(TestUtils.KMS_REGION); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() + .withCryptoConfiguration(v1Config) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(TestUtils.BUCKET, objectKey, input); + + // When: decrypt GCM object with an improved version client + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(s3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Then: Pass + assertEquals(input, new String(output.getBody().array())); + } + + // Exhaustive test 4 + // Outcome Version Operation Policy Content Encryption + // Pass Improved Decrypt ForbidEncryptAllowDecrypt KC-GCM + + @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") + @MethodSource("software.amazon.encryption.s3.TestUtils#encryptImprovedDecryptImproved") + public void GIVEN_KCGCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncryptAllowDecrypt_WHEN_Decrypt_THEN_Pass( + TestUtils.LanguageServerTarget encLang, TestUtils.LanguageServerTarget decLang + ) { + + S3ECTestServerClient encClient = TestUtils.testServerClientFor(encLang); + final String objectKey = "encrypt-kc-gcm-decrypt-improved-test-key-" + encLang; + final String input = "simple-test-input"; + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .build()) + .build()); + String encS3ECId = encClientOutput.getClientId(); + + // Given: object encrypted with key commitment + encClient.putObject(PutObjectInput.builder() + .clientID(encS3ECId) + .key(objectKey) + .bucket(TestUtils.BUCKET) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); + + S3ECTestServerClient decClient = TestUtils.testServerClientFor(decLang); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + // When: decrypt KC-GCM object with an improved version client with ForbidEncryptAllowDecrypt policy + GetObjectOutput output = decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Then: Pass + assertEquals(input, StandardCharsets.UTF_8.decode(output.getBody()).toString()); + } + +} 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 a94c561c..f03a045c 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 @@ -23,6 +23,7 @@ import org.junit.jupiter.params.provider.Arguments; 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.GetObjectInput; @@ -58,8 +59,12 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar .kmsKeyId(KMS_KEY_ARN) .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() - .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .config(S3ECConfig + .builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String encS3ECId = encClientOutput.getClientId(); encClient.putObject(PutObjectInput.builder() @@ -71,7 +76,10 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() @@ -103,7 +111,10 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String encS3ECId = encClientOutput.getClientId(); @@ -117,7 +128,10 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String decS3ECId = decClientOutput.getClientId(); GetObjectOutput output = decClient.getObject(GetObjectInput.builder() @@ -153,7 +167,9 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) .build()); String encS3ECId = encClientOutput.getClientId(); @@ -167,7 +183,10 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String decS3ECId = decClientOutput.getClientId(); try { @@ -204,7 +223,10 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build(); CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String encS3ECId = encClientOutput.getClientId(); @@ -218,7 +240,10 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() - .keyMaterial(kmsKeyArn).build()) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build() + ) .build()); String decS3ECId = decClientOutput.getClientId(); @@ -244,8 +269,8 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") - public void kmsV1Legacy(String language) { - S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); + public void kmsV1Legacy(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = testServerClientFor(language); final String objectKey = appendTestSuffix("test-key-kms-v1-" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -255,6 +280,7 @@ public void kmsV1Legacy(String language) { .config(S3ECConfig.builder() .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .build()) .build()); String s3ECId = output1.getClientId(); @@ -286,8 +312,8 @@ public void kmsV1Legacy(String language) { @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") - public void kmsV1LegacyWithEncCtx(String language) { - S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); + public void kmsV1LegacyWithEncCtx(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = testServerClientFor(language); final String objectKey = appendTestSuffix("test-key-kms-v1-with-enc-ctx-" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -297,6 +323,7 @@ public void kmsV1LegacyWithEncCtx(String language) { .config(S3ECConfig.builder() .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .build()) .build()); String s3ECId = output1.getClientId(); @@ -335,8 +362,8 @@ public void kmsV1LegacyWithEncCtx(String language) { @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") - public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { - S3ECTestServerClient client = testServerClientFor(getServerMap().get(language)); + public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = testServerClientFor(language); final String objectKey = appendTestSuffix("test-key-kms-v1-fails-disabled" + language); final String input = "simple-test-input"; KeyMaterial kmsKeyArn = KeyMaterial.builder() @@ -346,6 +373,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .config(S3ECConfig.builder() .enableLegacyWrappingAlgorithms(false) .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .build()) .build()); String s3ECId = output1.getClientId(); @@ -374,13 +402,15 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(String language) { .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.equals(NET_V3) || language.equals(NET_V2_CURRENT) - || language.equals(CPP_V2_CURRENT) || language.equals(CPP_V2_TRANSITION) || language.equals(CPP_V3)) { + if (language.getLanguageName().equals(NET_V3) || language.getLanguageName().equals(NET_V2_CURRENT) + || 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" )); - } else if (language.equals(RUBY_V3) || language.equals(RUBY_V2_CURRENT)) { - 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.")); + } else if (language.getLanguageName().equals(RUBY_V3) || language.getLanguageName().equals(RUBY_V2_CURRENT)) { + 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 { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); } 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 2e5bb8d6..ddb41fd1 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 @@ -92,23 +92,23 @@ public class TestUtils { public static final Set TRANSITION_VERSIONS = Set.of( - JAVA_V3_TRANSITION, - GO_V3_TRANSITION, - NET_V2_TRANSITION, - CPP_V2_TRANSITION, - RUBY_V2_TRANSITION, - PHP_V2_TRANSITION + // JAVA_V3_TRANSITION, + // GO_V3_TRANSITION, + // NET_V2_TRANSITION, + // CPP_V2_TRANSITION, + // PHP_V2_TRANSITION, + RUBY_V2_TRANSITION ); public static final Set IMPROVED_VERSIONS = Set.of( - JAVA_V4, - PYTHON_V3, - GO_V4, - NET_V3, - CPP_V3, - RUBY_V3, - PHP_V3 + // JAVA_V4, + // PYTHON_V3, + // GO_V4, + // NET_V3, + // CPP_V3, + // PHP_V3, + RUBY_V3 ); private static final Map serverMap; @@ -280,7 +280,6 @@ public static void validateServersRunning() { */ public static Stream clientsForTest() { return serverMap.values().stream() - .map(LanguageServerTarget::getLanguageName) .map(Arguments::of); } @@ -288,24 +287,87 @@ public static Stream clientsForTest() { * Get stream of arguments for current version clients for testing. */ public static Stream currentClientsForTest() { - return clientsForTest() - .filter(arg -> CURRENT_VERSIONS.contains(arg.get()[0])); + return serverMap.values().stream() + .filter(target -> CURRENT_VERSIONS.contains(target.getLanguageName())) + .map(Arguments::of); } /** * Get stream of arguments for transition version clients for testing. */ public static Stream transitionClientsForTest() { - return clientsForTest() - .filter(arg -> TRANSITION_VERSIONS.contains(arg.get()[0])); + return serverMap.values().stream() + .filter(target -> TRANSITION_VERSIONS.contains(target.getLanguageName())) + .map(Arguments::of); } /** * Get stream of arguments for improved version clients for testing. */ public static Stream improvedClientsForTest() { - return clientsForTest() - .filter(arg -> IMPROVED_VERSIONS.contains(arg.get()[0])); + return serverMap.values().stream() + .filter(target -> IMPROVED_VERSIONS.contains(target.getLanguageName())) + .map(Arguments::of); + } + + /** + * These functions provide a stream of arguments for parameterized tests. + * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption + */ + public static Stream encryptImprovedDecryptImproved() { + return improvedClientsForTest() + .flatMap(encrypt -> improvedClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptImprovedDecryptTransition() { + return improvedClientsForTest() + .flatMap(encrypt -> transitionClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptTransitionDecryptImproved() { + return transitionClientsForTest() + .flatMap(encrypt -> improvedClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptImprovedDecryptCurrent() { + return improvedClientsForTest() + .flatMap(encrypt -> currentClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptCurrentDecryptImproved() { + return currentClientsForTest() + .flatMap(encrypt -> improvedClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptTransitionDecryptCurrent() { + return transitionClientsForTest() + .flatMap(encrypt -> currentClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); + } + + public static Stream encryptCurrentDecryptTransition() { + return currentClientsForTest() + .flatMap(encrypt -> transitionClientsForTest() + .flatMap(decrypt -> Stream.of( + Arguments.of(encrypt.get()[0], decrypt.get()[0]) + ))); } /** diff --git a/test-server/model/client.smithy b/test-server/model/client.smithy index 4de56b5b..3f30c2d1 100644 --- a/test-server/model/client.smithy +++ b/test-server/model/client.smithy @@ -28,10 +28,17 @@ structure KeyMaterial { kmsKeyId: String } +enum CommitmentPolicy { + REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + REQUIRE_ENCRYPT_ALLOW_DECRYPT, + FORBID_ENCRYPT_ALLOW_DECRYPT +} + structure S3ECConfig { enableLegacyUnauthenticatedModes: Boolean = false, enableDelayedAuthenticationMode: Boolean = false, enableLegacyWrappingAlgorithms: Boolean = false, setBufferSize: Long, - keyMaterial: KeyMaterial + keyMaterial: KeyMaterial, + commitmentPolicy: CommitmentPolicy, } diff --git a/test-server/model/object.smithy b/test-server/model/object.smithy index 623d8ed3..a4b12d5a 100644 --- a/test-server/model/object.smithy +++ b/test-server/model/object.smithy @@ -93,6 +93,39 @@ operation GetObject { } } +@readonly +@http(method: "GET", uri: "/object/{bucket}/{key}") +operation ReEncrypt { + input := for Object { + @httpLabel + @required + $bucket + + @httpLabel + @required + $key + + /// Should probably be renamed to be EC specific + @httpHeader("Content-Metadata") + $metadata + + @httpHeader("ClientID") + @required + @notProperty + clientID: String + } + + output := for Object { + @httpHeader("Content-Metadata") + @required + $metadata + + @required + @httpPayload + $body + } +} + /// Smithy does not know how to serialize a map list ObjectMetadata { member: String diff --git a/test-server/ruby-v2-server/Gemfile.lock b/test-server/ruby-v2-server/Gemfile.lock index 660aadd5..04815a40 100644 --- a/test-server/ruby-v2-server/Gemfile.lock +++ b/test-server/ruby-v2-server/Gemfile.lock @@ -1,14 +1,14 @@ PATH remote: local-ruby-sdk/gems/aws-sdk-kms specs: - aws-sdk-kms (1.112.0) + aws-sdk-kms (1.113.0) aws-sdk-core (~> 3, >= 3.231.0) aws-sigv4 (~> 1.5) PATH remote: local-ruby-sdk/gems/aws-sdk-s3 specs: - aws-sdk-s3 (1.199.0) + aws-sdk-s3 (1.199.1) aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index e129cf37..ba15842f 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit e129cf37c170254ebb631782cae145040cde6d0b +Subproject commit ba15842f5b5d9cf6855a1023753d32eb8606bbef diff --git a/test-server/ruby-v3-server/.duvet/config.toml b/test-server/ruby-v3-server/.duvet/config.toml index 7118cd70..eaea972c 100644 --- a/test-server/ruby-v3-server/.duvet/config.toml +++ b/test-server/ruby-v3-server/.duvet/config.toml @@ -12,7 +12,12 @@ source = "../specification/s3-encryption/data-format/metadata-strategy.md" [[specification]] source = "../specification/s3-encryption/encryption.md" [[specification]] +source = "../specification/s3-encryption/decryption.md" +[[specification]] source = "../specification/s3-encryption/key-derivation.md" +[[specification]] +source = "../specification/s3-encryption/key-commitment.md" + [report.html] enabled = true diff --git a/test-server/ruby-v3-server/Gemfile.lock b/test-server/ruby-v3-server/Gemfile.lock index ae04e5fd..9edf1f5d 100644 --- a/test-server/ruby-v3-server/Gemfile.lock +++ b/test-server/ruby-v3-server/Gemfile.lock @@ -1,14 +1,14 @@ PATH remote: local-ruby-sdk/gems/aws-sdk-kms specs: - aws-sdk-kms (1.112.0) + aws-sdk-kms (1.113.0) aws-sdk-core (~> 3, >= 3.231.0) aws-sigv4 (~> 1.5) PATH remote: local-ruby-sdk/gems/aws-sdk-s3 specs: - aws-sdk-s3 (1.199.0) + aws-sdk-s3 (1.199.1) aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) diff --git a/test-server/ruby-v3-server/app.rb b/test-server/ruby-v3-server/app.rb index abc25932..2633c5dc 100644 --- a/test-server/ruby-v3-server/app.rb +++ b/test-server/ruby-v3-server/app.rb @@ -129,7 +129,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 @@ -198,7 +198,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 diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index d3b12b23..d5d96801 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -25,14 +25,30 @@ def create_client(config) kms_key_id: kms_key_id, kms_client: @kms_client, key_wrap_schema: :kms_context, - content_encryption_schema: :aes_gcm_no_padding, + # content_encryption_schema: :aes_gcm_no_padding, # Set security profile based on legacy wrapping algorithms setting - security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 - } + # security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 + }.tap do |hash| + if !config['commitmentPolicy'].nil? + hash[:commitment_policy] = case config['commitmentPolicy'] + when 'FORBID_ENCRYPT_ALLOW_DECRYPT' + :forbid_encrypt_allow_decrypt + when 'REQUIRE_ENCRYPT_ALLOW_DECRYPT' + :require_encrypt_allow_decrypt + when 'REQUIRE_ENCRYPT_REQUIRE_DECRYPT' + :require_encrypt_require_decrypt + else + raise "Unsupported commitment_policy " + config['commitmentPolicy'] + end + end + if !config['enableLegacyWrappingAlgorithms'].nil? || !config['enableLegacyUnauthenticatedModes'].nil? + hash[:legacy_modes] = config['enableLegacyWrappingAlgorithms'] || config['enableLegacyUnauthenticatedModes'] + end + end # Create the S3 encryption client s3_client = Aws::S3::Client.new(region: 'us-west-2') - encryption_client = Aws::S3::EncryptionV2::Client.new( + encryption_client = Aws::S3::EncryptionV3::Client.new( client: s3_client, **encryption_config ) diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index e129cf37..902f15e0 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit e129cf37c170254ebb631782cae145040cde6d0b +Subproject commit 902f15e03d2816e11d1750f4279742ba35afaac5 diff --git a/test-server/specification b/test-server/specification index c534aee8..0ae23623 160000 --- a/test-server/specification +++ b/test-server/specification @@ -1 +1 @@ -Subproject commit c534aee8c2d34c462dfac6ab21ae59467dcedd68 +Subproject commit 0ae23623ff4b3330da0c58890fbc032a7d378d08 From 4b71aa281f889a08760dfaa6f05a541f21c1724d Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Tue, 14 Oct 2025 11:05:57 -0700 Subject: [PATCH 112/201] chore: add .net submodule and run test against it (#13) --- .github/workflows/test.yml | 18 ++++++++++++++++++ .gitmodules | 8 ++++++++ .../net-v2-v3-server/NetV2V3Server.csproj | 9 +++++++-- test-server/net-v2-v3-server/s3ec-net-v2 | 1 + test-server/net-v2-v3-server/s3ec-net-v3 | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) create mode 160000 test-server/net-v2-v3-server/s3ec-net-v2 create mode 160000 test-server/net-v2-v3-server/s3ec-net-v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc8e2f45..263ced29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,24 @@ jobs: ref: fire-egg-dev path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ + - name: Checkout .NET V2 code + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + # This is the branch for S3EC .NET V2 + ref: v3sdk-development + path: test-server/net-v2-v3-server/s3ec-net-v2/ + + - name: Checkout .NET V3 code + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + # This is the branch for S3EC .NET V3 + ref: s3ec-v3 + path: test-server/net-v2-v3-server/s3ec-net-v3 + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.gitmodules b/.gitmodules index 659db28b..0bf186eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,11 @@ path = test-server/php-v2-transition-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git branch = s3ec/transitional +[submodule "test-server/net-v2-v3-server/s3ec-net-v2"] + path = test-server/net-v2-v3-server/s3ec-net-v2 + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = v3sdk-development +[submodule "test-server/net-v2-v3-server/s3ec-net-v3"] + path = test-server/net-v2-v3-server/s3ec-net-v3 + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = s3ec-v3 diff --git a/test-server/net-v2-v3-server/NetV2V3Server.csproj b/test-server/net-v2-v3-server/NetV2V3Server.csproj index a803a838..8d664eff 100644 --- a/test-server/net-v2-v3-server/NetV2V3Server.csproj +++ b/test-server/net-v2-v3-server/NetV2V3Server.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + false @@ -19,11 +20,15 @@ - + - + + + + + diff --git a/test-server/net-v2-v3-server/s3ec-net-v2 b/test-server/net-v2-v3-server/s3ec-net-v2 new file mode 160000 index 00000000..ba85c07e --- /dev/null +++ b/test-server/net-v2-v3-server/s3ec-net-v2 @@ -0,0 +1 @@ +Subproject commit ba85c07e0706bae8df242fb7bbfa7e53a264bafa diff --git a/test-server/net-v2-v3-server/s3ec-net-v3 b/test-server/net-v2-v3-server/s3ec-net-v3 new file mode 160000 index 00000000..cc942cb5 --- /dev/null +++ b/test-server/net-v2-v3-server/s3ec-net-v3 @@ -0,0 +1 @@ +Subproject commit cc942cb541a733a4340f46bd3e4a1d29a9cbb9a3 From 9112ec5907f11f3d06ac917b2937a5a82aff5107 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:27:34 -0400 Subject: [PATCH 113/201] let v2 transition read v3 format (#40) --- .../.duvet/config.toml | 24 +++++++++++++++++++ test-server/cpp-v2-transition-server/Makefile | 6 +++++ .../amazon/encryption/s3/TestUtils.java | 3 ++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 test-server/cpp-v2-transition-server/.duvet/config.toml diff --git a/test-server/cpp-v2-transition-server/.duvet/config.toml b/test-server/cpp-v2-transition-server/.duvet/config.toml new file mode 100644 index 00000000..88bb7213 --- /dev/null +++ b/test-server/cpp-v2-transition-server/.duvet/config.toml @@ -0,0 +1,24 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 05803c78..0a63b2ed 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -27,3 +27,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html 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 ddb41fd1..4f5454ab 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 @@ -95,7 +95,7 @@ public class TestUtils { // JAVA_V3_TRANSITION, // GO_V3_TRANSITION, // NET_V2_TRANSITION, - // CPP_V2_TRANSITION, + CPP_V2_TRANSITION, // PHP_V2_TRANSITION, RUBY_V2_TRANSITION ); @@ -131,6 +131,7 @@ public class TestUtils { // servers.put(GO_V3_TRANSITION, new LanguageServerTarget(GO_V3_TRANSITION, "8095")); // servers.put(NET_V2_TRANSITION, new LanguageServerTarget(NET_V2_TRANSITION, "8096")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + // servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); // 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, "8090")); From 4ad0e87b27a7c24114ce9236f8d5916e8dea61eb Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 23 Oct 2025 16:46:40 -0700 Subject: [PATCH 114/201] Seebees/more complete tests (#36) More complete CBC, GCM, and KC-GCM tests. fix Ruby, and rollback strangeness in CPP --- .github/workflows/test.yml | 2 +- test-server/Makefile | 4 +- test-server/cpp-v2-server/Makefile | 24 +- test-server/cpp-v2-server/main.cpp | 8 +- test-server/cpp-v2-transition-server/Makefile | 22 +- test-server/cpp-v2-transition-server/main.cpp | 8 +- .../amazon/encryption/s3/CBCDecryptTests.java | 178 ++++++++++++++ .../amazon/encryption/s3/GCMTests.java | 201 ++++++++++++++++ .../amazon/encryption/s3/KC_GCMTests.java | 218 ++++++++++++++++++ .../amazon/encryption/s3/RoundTripTests.java | 6 +- .../amazon/encryption/s3/TestUtils.java | 126 +++++++++- test-server/model/client.smithy | 11 +- test-server/ruby-v2-server/Makefile | 13 +- test-server/ruby-v2-server/app.rb | 2 +- .../ruby-v2-server/lib/client_manager.rb | 11 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/Makefile | 3 +- test-server/ruby-v3-server/app.rb | 2 +- .../ruby-v3-server/lib/client_manager.rb | 3 - test-server/ruby-v3-server/local-ruby-sdk | 2 +- 20 files changed, 780 insertions(+), 66 deletions(-) create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java create 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_GCMTests.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 263ced29..8c8a9616 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ on: jobs: test: - runs-on: macos-14 + runs-on: macos-14-large permissions: id-token: write contents: read diff --git a/test-server/Makefile b/test-server/Makefile index d2b91a5d..cd667505 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -3,10 +3,10 @@ .PHONY: all start-servers run-tests stop-servers clean ci check-env help # Default target -all: start-servers run-tests +all: start-all-servers run-tests # CI target for GitHub Actions -ci: start-servers run-tests stop-servers +ci: start-all-servers run-tests stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) # SERVER_DIRS := cpp-v3-server diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index cc562c1a..9399b631 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -6,20 +6,20 @@ PID_FILE := server.pid PORT := 8085 build/s3ec-server: - brew install libmicrohttpd nlohmann-json ossp-uuid - git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git - cd aws-sdk-cpp - mkdir -p build && cd build && cmake .. +# brew install libmicrohttpd nlohmann-json ossp-uuid +# git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git +# cd aws-sdk-cpp +# mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server - @echo "Starting Cpp V2 server..." - cd build && make && \ - 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" \ - ./s3ec-server & echo $$! > $(PID_FILE) - @echo "Cpp V2 server starting..." +# @echo "Starting Cpp V2 server..." +# cd build && make && \ +# 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" \ +# ./s3ec-server & echo $$! > $(PID_FILE) +# @echo "Cpp V2 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index 9be401f8..c89ea8c3 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -55,16 +55,14 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, try { json request = json::parse(body); std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; - bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; + bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; + bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - if (legacy) { + if (legacy1 || legacy2) { config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - } else { - config.SetSecurityProfile(SecurityProfile::V2); - } auto encryption_client = std::make_shared(config); diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 0a63b2ed..b879358d 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -6,18 +6,18 @@ PID_FILE := server.pid PORT := 8097 build/s3ec-server: - brew install libmicrohttpd nlohmann-json ossp-uuid - mkdir -p build && cd build && cmake .. +# brew install libmicrohttpd nlohmann-json ossp-uuid +# mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server - @echo "Starting Cpp V2 server..." - cd build && make && \ - 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" \ - ./s3ec-server & echo $$! > $(PID_FILE) - @echo "Cpp V2 server starting..." +# @echo "Starting Cpp V2 server..." +# cd build && make && \ +# 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" \ +# ./s3ec-server & echo $$! > $(PID_FILE) +# @echo "Cpp V2 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ @@ -26,7 +26,7 @@ stop-server: fi wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT) +# $(MAKE) -C .. wait-for-port PORT=$(PORT) duvet: duvet report diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 32735d60..4dd505bf 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -55,16 +55,14 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, try { json request = json::parse(body); std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; - bool legacy = request["config"]["enableLegacyWrappingAlgorithms"]; + bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; + bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - if (legacy) { + if (legacy1 || legacy2) { config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - } else { - config.SetSecurityProfile(SecurityProfile::V2); - } auto encryption_client = std::make_shared(config); diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java new file mode 100644 index 00000000..3aba7506 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java @@ -0,0 +1,178 @@ +/* +* 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.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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.Nested; +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.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 software.amazon.encryption.s3.model.EncryptionAlgorithm; + +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 +* +* These tests deal with decrypting CBC messages +*/ + +class CBCDecryptTests { + private static String sharedObjectKey = appendTestSuffix("test-cbc-kms-v1-"); + private static KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(TestUtils.KMS_KEY_ARN) + .build(); + + @BeforeAll + static void encryptCBCObject() { + // Create the object using the old client + // V1 Client + 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(); + + v1Client.putObject(TestUtils.BUCKET, sharedObjectKey, sharedObjectKey); + } + + @ParameterizedTest(name = "{0}: Transition configured with the default should decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_the_default_should_decrypt_cbc(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Transition configured with ForbidEncryptAllowDecrypt should decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#transitionClientsForTest") + void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc(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) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with ForbidEncryptAllowDecrypt should decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient decClient = TestUtils.testServerClientFor(language); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + TestUtils.Decrypt(decClient, decS3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptAllowDecrypt should decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_cbc(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient decClient = TestUtils.testServerClientFor(language); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + TestUtils.Decrypt(decClient, decS3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with RequireEncryptRequireDecrypt should fail to decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_require_encrypt_require_decrypt_should_fail_to_decrypt_cbc(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient decClient = TestUtils.testServerClientFor(language); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + .enableLegacyUnauthenticatedModes(true) + .build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + TestUtils.Decrypt_fails(decClient, decS3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF); + } + + @ParameterizedTest(name = "{0}: Improved configured with the default should fail to decrypt CBC") + @MethodSource("software.amazon.encryption.s3.TestUtils#improvedClientsForTest") + void improved_configured_with_the_default_should_fail_to_decrypt_cbc(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient decClient = TestUtils.testServerClientFor(language); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + + TestUtils.Decrypt_fails(decClient, decS3ECId, Arrays.asList(sharedObjectKey), EncryptionAlgorithm.ALG_AES_256_CBC_IV16_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 new file mode 100644 index 00000000..deb1571d --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTests.java @@ -0,0 +1,201 @@ +/* +* 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) + .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) + .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_GCMTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java new file mode 100644 index 00000000..838df97e --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTests.java @@ -0,0 +1,218 @@ +/* +* 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 KC_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 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(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, 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(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, 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(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, 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, crossLanguageObjects, 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, crossLanguageObjects, 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) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, 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, crossLanguageObjects, 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, crossLanguageObjects, 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, crossLanguageObjects, 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 f03a045c..e8dc4bae 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 @@ -197,7 +197,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { + 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")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -259,7 +259,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT)) { + 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")); } else { assertTrue(e.getMessage().contains("Provided encryption context does not match information retrieved from S3")); @@ -407,7 +407,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la assertTrue(e.getMessage().contains( "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); - } else if (language.getLanguageName().equals(RUBY_V3) || language.getLanguageName().equals(RUBY_V2_CURRENT)) { + } 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()); 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 4f5454ab..a79d5bc2 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 @@ -5,8 +5,13 @@ package software.amazon.encryption.s3; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.net.Socket; import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -27,10 +32,22 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; import software.amazon.encryption.s3.client.S3ECTestServerClient; +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.PutObjectInput; +import software.amazon.encryption.s3.model.PutObjectOutput; +import software.amazon.encryption.s3.model.S3ECConfig; import software.amazon.encryption.s3.model.S3ECTestServerApiService; +import software.amazon.encryption.s3.model.S3EncryptionClientError; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.GetObjectMetadataRequest; + public class TestUtils { // Version name constants @@ -85,7 +102,7 @@ public class TestUtils { JAVA_V3_CURRENT, GO_V3_CURRENT, NET_V2_CURRENT, - CPP_V2_CURRENT, + // CPP_V2_CURRENT, RUBY_V2_CURRENT, PHP_V2_CURRENT ); @@ -95,7 +112,7 @@ public class TestUtils { // JAVA_V3_TRANSITION, // GO_V3_TRANSITION, // NET_V2_TRANSITION, - CPP_V2_TRANSITION, + // CPP_V2_TRANSITION, // PHP_V2_TRANSITION, RUBY_V2_TRANSITION ); @@ -120,8 +137,8 @@ public class TestUtils { servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); - servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); + // servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); + // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); @@ -130,9 +147,8 @@ public class TestUtils { 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); - // servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); - // servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + // servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + 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, "8090")); serverMap = filterServers(servers); @@ -395,4 +411,100 @@ public static String appendTestSuffix(final String s) { stringBuilder.append((int) (Math.random() * 100000)); return stringBuilder.toString(); } + + private static AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient(); + public static EncryptionAlgorithm GetEncryptionAlgorithm(String objectKey) + { + 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; + } + } + + throw new RuntimeException("Need to support instruction files!"); + } + + public static void Encrypt( + S3ECTestServerClient client, + String S3ECId, + String objectKey, + List crossLanguageObjects, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + PutObjectOutput foo = client.putObject(PutObjectInput.builder() + .clientID(S3ECId) + .key(objectKey) + .bucket(TestUtils.BUCKET) + .body(ByteBuffer.wrap(objectKey.getBytes(StandardCharsets.UTF_8))) + .build()); + + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "When encrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm + ); + + crossLanguageObjects.add(objectKey); + } + + public static void Decrypt( + S3ECTestServerClient client, + String S3ECId, List crossLanguageObjects, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + for (String objectKey : crossLanguageObjects) { + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + + // Then: Pass + assertEquals(objectKey, new String(output.getBody().array())); + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "When decrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm + ); + } + } + + public static void Decrypt_fails( + S3ECTestServerClient client, + String S3ECId, List crossLanguageObjects, + EncryptionAlgorithm expectedEncryptionAlgorithm + ) { + List successfulDecrypt = new ArrayList<>(); + for (String objectKey : crossLanguageObjects) { + try { + + assertEquals( + expectedEncryptionAlgorithm, + GetEncryptionAlgorithm(objectKey), + "Before decrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm + ); + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .build()); + // It should fail to decrypt + successfulDecrypt.add(objectKey); + } catch (S3EncryptionClientError e) { + // This is a success + // TODO, add the failure message + } + } + + assertEquals(successfulDecrypt.size(), 0, "Decryption should have failed:" + String.join(",", successfulDecrypt)); + } } diff --git a/test-server/model/client.smithy b/test-server/model/client.smithy index 3f30c2d1..5514672f 100644 --- a/test-server/model/client.smithy +++ b/test-server/model/client.smithy @@ -29,11 +29,17 @@ structure KeyMaterial { } enum CommitmentPolicy { - REQUIRE_ENCRYPT_REQUIRE_DECRYPT, - REQUIRE_ENCRYPT_ALLOW_DECRYPT, + REQUIRE_ENCRYPT_REQUIRE_DECRYPT + REQUIRE_ENCRYPT_ALLOW_DECRYPT FORBID_ENCRYPT_ALLOW_DECRYPT } +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 +} + structure S3ECConfig { enableLegacyUnauthenticatedModes: Boolean = false, enableDelayedAuthenticationMode: Boolean = false, @@ -41,4 +47,5 @@ structure S3ECConfig { setBufferSize: Long, keyMaterial: KeyMaterial, commitmentPolicy: CommitmentPolicy, + encryptionAlgorithm: EncryptionAlgorithm, } diff --git a/test-server/ruby-v2-server/Makefile b/test-server/ruby-v2-server/Makefile index 15751f6a..2b2b59e1 100644 --- a/test-server/ruby-v2-server/Makefile +++ b/test-server/ruby-v2-server/Makefile @@ -3,7 +3,7 @@ .PHONY: start-server stop-server wait-for-server PID_FILE := server.pid -PORT := 8086 +PORT := 8098 start-server: @if [ -f $(PID_FILE) ]; then \ @@ -11,22 +11,23 @@ start-server: exit 1; \ fi; @echo "Starting Ruby V2 server..." + gem install openssl bundle install 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" \ - bundle exec ruby app.rb & echo $$! > server.pid + PORT=$(PORT) bundle exec ruby app.rb & echo $$! > $(PID_FILE) @echo "Ruby V2 server starting..." stop-server: - @if [ -f server.pid ]; then \ - kill $$(cat server.pid) 2>/dev/null || true; \ - rm server.pid; \ + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ fi wait-for-server: - $(MAKE) -C .. wait-for-port PORT=8086 + $(MAKE) -C .. wait-for-port PORT=$(PORT) duvet: duvet report diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb index 6096a5ea..adc7ade4 100644 --- a/test-server/ruby-v2-server/app.rb +++ b/test-server/ruby-v2-server/app.rb @@ -7,7 +7,7 @@ class S3ECRubyServer < Sinatra::Base configure do - set :port, 8086 + set :port, ENV['PORT'] || 8098 set :bind, '0.0.0.0' set :show_exceptions, false set :raise_errors, false diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb index d3b12b23..c6a25610 100644 --- a/test-server/ruby-v2-server/lib/client_manager.rb +++ b/test-server/ruby-v2-server/lib/client_manager.rb @@ -16,7 +16,6 @@ def initialize def create_client(config) # Extract configuration kms_key_id = config.dig('keyMaterial', 'kmsKeyId') - enable_legacy_wrapping = config['enableLegacyWrappingAlgorithms'] || false raise 'KMS Key ID is required' if kms_key_id.nil? || kms_key_id.empty? @@ -26,9 +25,13 @@ def create_client(config) kms_client: @kms_client, key_wrap_schema: :kms_context, content_encryption_schema: :aes_gcm_no_padding, - # Set security profile based on legacy wrapping algorithms setting - security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 - } + }.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 + hash[:security_profile] = legacy_modes ? :v2_and_legacy : :v2 + end + end # Create the S3 encryption client s3_client = Aws::S3::Client.new(region: 'us-west-2') diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index ba15842f..5d67d8f3 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit ba15842f5b5d9cf6855a1023753d32eb8606bbef +Subproject commit 5d67d8f3d3c80b34cda643b83f189abe322ac4b4 diff --git a/test-server/ruby-v3-server/Makefile b/test-server/ruby-v3-server/Makefile index 6e62e785..7e256788 100644 --- a/test-server/ruby-v3-server/Makefile +++ b/test-server/ruby-v3-server/Makefile @@ -11,12 +11,13 @@ start-server: exit 1; \ fi; @echo "Starting Ruby V3 server..." + gem install openssl bundle install 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" \ - bundle exec ruby app.rb & echo $$! > $(PID_FILE) + PORT=$(PORT) bundle exec ruby app.rb & echo $$! > $(PID_FILE) @echo "Ruby V3 server starting..." stop-server: diff --git a/test-server/ruby-v3-server/app.rb b/test-server/ruby-v3-server/app.rb index 2633c5dc..f6e28817 100644 --- a/test-server/ruby-v3-server/app.rb +++ b/test-server/ruby-v3-server/app.rb @@ -7,7 +7,7 @@ class S3ECRubyServer < Sinatra::Base configure do - set :port, 8092 + set :port, ENV['PORT'] || 8092 set :bind, '0.0.0.0' set :show_exceptions, false set :raise_errors, false diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index d5d96801..7c125458 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -16,7 +16,6 @@ def initialize def create_client(config) # Extract configuration kms_key_id = config.dig('keyMaterial', 'kmsKeyId') - enable_legacy_wrapping = config['enableLegacyWrappingAlgorithms'] || false raise 'KMS Key ID is required' if kms_key_id.nil? || kms_key_id.empty? @@ -26,8 +25,6 @@ def create_client(config) kms_client: @kms_client, key_wrap_schema: :kms_context, # content_encryption_schema: :aes_gcm_no_padding, - # Set security profile based on legacy wrapping algorithms setting - # security_profile: enable_legacy_wrapping ? :v2_and_legacy : :v2 }.tap do |hash| if !config['commitmentPolicy'].nil? hash[:commitment_policy] = case config['commitmentPolicy'] diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 902f15e0..5d67d8f3 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 902f15e03d2816e11d1750f4279742ba35afaac5 +Subproject commit 5d67d8f3d3c80b34cda643b83f189abe322ac4b4 From 2ab3b8143955e80221d30b813f44ac2e66116d8e Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 24 Oct 2025 11:54:28 -0700 Subject: [PATCH 115/201] chore: Dashboard for spec compliance in each language (#38) --- .github/workflows/duvet.yml | 51 + .../spec-compliance-dashboard/.gitignore | 1 + .../generate_compliance_dashboard.py | 1049 +++++++++++++++++ .../templates/homepage_styles.css | 335 ++++++ .../templates/homepage_template.html | 21 + .../templates/report_template.html | 276 +++++ .../templates/styles.css | 387 ++++++ .../templates/summary_stats_template.html | 63 + 8 files changed, 2183 insertions(+) create mode 100644 test-server/spec-compliance-dashboard/.gitignore create mode 100644 test-server/spec-compliance-dashboard/generate_compliance_dashboard.py create mode 100644 test-server/spec-compliance-dashboard/templates/homepage_styles.css create mode 100644 test-server/spec-compliance-dashboard/templates/homepage_template.html create mode 100644 test-server/spec-compliance-dashboard/templates/report_template.html create mode 100644 test-server/spec-compliance-dashboard/templates/styles.css create mode 100644 test-server/spec-compliance-dashboard/templates/summary_stats_template.html diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index 5727c38e..70e335c7 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -10,6 +10,7 @@ jobs: permissions: id-token: write contents: read + pages: write steps: - name: Checkout code @@ -43,3 +44,53 @@ jobs: name: reports include-hidden-files: true path: test-server/*-server/.duvet/reports/report.html + + - name: Generate compliance dashboard + if: always() + run: | + cd test-server/spec-compliance-dashboard + python generate_compliance_dashboard.py + + - name: Create dashboard redirect index.html + if: always() + run: | + cat > test-server/index.html << 'EOF' + + + + + + Redirecting to Compliance Dashboard... + + +

Redirecting to Compliance Dashboard...

+ + + EOF + + - name: Upload compliance dashboard + if: always() + uses: actions/upload-artifact@v4 + with: + name: compliance-dashboard + include-hidden-files: true + path: | + test-server/spec-compliance-dashboard/compliance_homepage.html + test-server/*/compliance_summary_report.html + test-server/*/.duvet/reports/report.html + test-server/spec-compliance-dashboard/templates/* + test-server/index.html + + - name: Setup Pages + if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + uses: actions/upload-pages-artifact@v3 + with: + path: test-server/ + + - name: Deploy to GitHub Pages + if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + uses: actions/deploy-pages@v4 diff --git a/test-server/spec-compliance-dashboard/.gitignore b/test-server/spec-compliance-dashboard/.gitignore new file mode 100644 index 00000000..c9e1b5bb --- /dev/null +++ b/test-server/spec-compliance-dashboard/.gitignore @@ -0,0 +1 @@ +compliance_homepage.html \ No newline at end of file diff --git a/test-server/spec-compliance-dashboard/generate_compliance_dashboard.py b/test-server/spec-compliance-dashboard/generate_compliance_dashboard.py new file mode 100644 index 00000000..d19f6c6e --- /dev/null +++ b/test-server/spec-compliance-dashboard/generate_compliance_dashboard.py @@ -0,0 +1,1049 @@ +#!/usr/bin/env python3 +""" +Self-contained script to generate compliance dashboard and all server reports. +Automatically discovers servers with .duvet/reports/report.html files and generates +individual reports using the enhanced report-based format with deep links, source traceability, +copy buttons, and comprehensive statistics. +""" + +import json +import re +import os +from pathlib import Path +from datetime import datetime + + +def parse_report_html(report_file_path): + """Parse the report.html file and extract specification data.""" + with open(report_file_path, "r", encoding="utf-8") as f: + content = f.read() + + # Extract JSON from script tag with id="result" + start_marker = '" + + start_idx = content.find(start_marker) + if start_idx == -1: + raise ValueError("No result script tag found in HTML") + + start_idx += len(start_marker) + end_idx = content.find(end_marker, start_idx) + if end_idx == -1: + raise ValueError("No closing script tag found") + + json_content = content[start_idx:end_idx] + data = json.loads(json_content) + + # Convert report.html JSON structure to match snapshot structure + return convert_report_to_specifications(data) + + +def convert_report_to_specifications(data): + """Convert duvet report.html JSON structure to match snapshot structure.""" + specifications = {} + + for spec_path, spec in (data.get("specifications", {})).items(): + spec_data = { + "title": spec.get("title", "Unknown"), + "spec_path": spec_path, # Store the original spec path + "sections": {}, + } + + # Process sections - sections is a list, not a dict + for section in spec.get("sections", []): + section_data = { + "title": section.get("title", "Unknown"), + "section_id": section.get("id", "unknown"), # Store the section ID + "requirements": [], + } + + # Process requirements for this section + for req_id in section.get("requirements", []): + # Get annotation data + annotation = None + if "annotations" in data and isinstance(data["annotations"], list): + # annotations is a list indexed by req_id + if req_id < len(data["annotations"]): + annotation = data["annotations"][req_id] + + # Get status data + status = None + if "statuses" in data and isinstance(data["statuses"], dict): + status = data["statuses"].get(str(req_id)) + + if annotation and status: + # Parse status indicators (matching snapshot logic) + has_implementation = bool( + status.get("citation") + ) # Only citation counts as implementation + has_test = bool(status.get("test")) + has_exception = bool(status.get("exception")) + has_implication = bool(status.get("implication")) + has_partial_coverage = bool(status.get("incomplete")) + + # Determine completion status (matching snapshot rules exactly) + is_complete = ( + (has_implementation and has_test) or has_exception or has_implication + ) and not has_partial_coverage # Partial coverage means not complete + + # Collect related annotations for detailed status + related_sources = [] + if "related" in status: + for related_id in status["related"]: + if related_id < len(data["annotations"]): + related_annotation = data["annotations"][related_id] + source = related_annotation.get("source", "") + line = related_annotation.get("line", "") + annotation_type = related_annotation.get("type", "CITATION") + if source: + source_info = { + "source": source, + "line": line, + "type": annotation_type, + } + related_sources.append(source_info) + + requirement = { + "text": annotation.get("comment", "No comment available"), + "has_implementation": has_implementation, + "has_test": has_test, + "has_exception": has_exception, + "has_implication": has_implication, + "has_partial_coverage": has_partial_coverage, + "is_complete": is_complete, + "related_sources": related_sources, + } + + section_data["requirements"].append(requirement) + elif req_id < len(data.get("annotations", [])): + # Fallback: create requirement with basic info + annotation = data["annotations"][req_id] + requirement = { + "text": annotation.get("comment", f"Requirement {req_id}"), + "has_implementation": False, + "has_test": False, + "has_exception": False, + "has_implication": False, + "is_complete": False, + "related_sources": [], + } + section_data["requirements"].append(requirement) + + spec_data["sections"][section.get("title", "Unknown")] = section_data + + specifications[spec.get("title", "Unknown")] = spec_data + + return specifications + + +def get_spec_status(spec_data): + """Determine the overall status of a specification based on all its sections.""" + sections = spec_data.get("sections", {}) + + if not sections: + return "✅" # No sections means complete + + # Get status of each section + section_statuses = [] + for section_data in sections.values(): + requirements = section_data.get("requirements", []) + if not requirements: + section_statuses.append("✅") # Empty section is complete + else: + complete_reqs = sum(1 for req in requirements if req["is_complete"]) + total_reqs = len(requirements) + + if complete_reqs == total_reqs: + section_statuses.append("✅") # All requirements complete + elif complete_reqs > 0: + section_statuses.append("🟡") # Some requirements complete + else: + section_statuses.append("❌") # No requirements complete + + # Apply the corrected logic based on section statuses: + if all(status == "✅" for status in section_statuses): + return "✅" # Green check if all sections are green + elif any(status in ["✅", "🟡"] for status in section_statuses): + return "🟡" # Yellow if any section is green or yellow + else: + return "❌" # Red X if all sections are red X + + +def get_requirement_status(requirement): + """Get the status emoji for a single requirement.""" + if requirement["is_complete"]: + return "✅" + elif requirement.get("has_partial_coverage", False): + return "🟡" # Partial coverage - incomplete + elif requirement["has_implementation"] and requirement["related_sources"]: + return "🟡" # Has implementation but no test + else: + return "❌" # No implementation + + +def format_requirement_text(text): + """Format requirement text to style status metadata lines.""" + lines = text.split("\n") + formatted_lines = [] + + for line in lines: + # Check if line contains status metadata + if line.strip().startswith("Status:"): + formatted_lines.append(f'') + else: + formatted_lines.append(line) + + return "\n".join(formatted_lines) + + +def calculate_summary_statistics(specifications): + """Calculate summary statistics for all specifications.""" + total_sections = 0 + complete_sections = 0 + total_requirements = 0 + complete_requirements = 0 + + # Count requirements by implementation type + no_implementation = 0 + implementation_only = 0 + test_only = 0 + implementation_and_test = 0 + exception_count = 0 + implication_count = 0 + partial_coverage_count = 0 + + for spec_data in specifications.values(): + sections = spec_data.get("sections", {}) + total_sections += len(sections) + + for section_data in sections.values(): + requirements = section_data.get("requirements", []) + total_requirements += len(requirements) + + # Count complete requirements + section_complete_reqs = sum(1 for req in requirements if req["is_complete"]) + complete_requirements += section_complete_reqs + + # A section is complete if all its requirements are complete + if requirements and section_complete_reqs == len(requirements): + complete_sections += 1 + elif not requirements: # Empty section is considered complete + complete_sections += 1 + + # Count requirements by implementation type + for req in requirements: + if req["has_exception"]: + exception_count += 1 + elif req["has_implication"]: + implication_count += 1 + elif ( + req["has_implementation"] + and req["has_test"] + and not req.get("has_partial_coverage", False) + ): + implementation_and_test += 1 + elif req["has_implementation"] and not req.get("has_partial_coverage", False): + implementation_only += 1 + elif req["has_test"] and not req.get("has_partial_coverage", False): + test_only += 1 + else: + # Partial coverage gets counted as no implementation + no_implementation += 1 + + return { + "total_sections": total_sections, + "complete_sections": complete_sections, + "total_requirements": total_requirements, + "complete_requirements": complete_requirements, + "no_implementation": no_implementation, + "implementation_only": implementation_only, + "test_only": test_only, + "implementation_and_test": implementation_and_test, + "exception_count": exception_count, + "implication_count": implication_count, + "partial_coverage_count": partial_coverage_count, + } + + +def url_encode_spec_path(spec_path): + """URL encode the spec path for use in duvet report URLs.""" + import urllib.parse + + return urllib.parse.quote(spec_path, safe="") + + +def generate_spec_url(duvet_report_path, spec_path): + """Generate URL to a specific specification in the duvet report.""" + encoded_path = url_encode_spec_path(spec_path) + return f"{duvet_report_path}#/spec/{encoded_path}" + + +def generate_section_url(duvet_report_path, spec_path, section_id): + """Generate URL to a specific section in the duvet report.""" + encoded_path = url_encode_spec_path(spec_path) + return f"{duvet_report_path}#/spec/{encoded_path}/{section_id}" + + +def generate_github_url(source_path, line_number=None, github_base_url=None): + """Generate GitHub URL for a source file.""" + if not github_base_url: + return None + + # Convert local path to GitHub path + # Remove local-go-s3ec/ prefix if present + if source_path.startswith("local-go-s3ec/"): + github_path = source_path[len("local-go-s3ec/") :] + else: + github_path = source_path + + url = f"{github_base_url}/{github_path}" + if line_number: + url += f"#L{line_number}" + + return url + + +def load_template(template_path): + """Load a template file.""" + with open(template_path, "r", encoding="utf-8") as f: + return f.read() + + +def generate_enhanced_html_report(report_file_path, output_file_path, server_name): + """Generate an enhanced interactive HTML report using templates.""" + specifications = parse_report_html(report_file_path) + + # Load the report template + template_dir = Path(__file__).parent / "templates" + template = load_template(template_dir / "report_template.html") + + # Create relative path to the duvet report.html + duvet_report_path = ".duvet/reports/report.html" + + # GitHub base URL - can be configured for when deployed to GitHub Pages + github_base_url = None + + # Calculate summary statistics + stats = calculate_summary_statistics(specifications) + + # Calculate percentages for each implementation type + total_reqs = stats["total_requirements"] + if total_reqs > 0: + # Calculate raw percentages + impl_test_pct = (stats["implementation_and_test"] / total_reqs) * 100 + impl_only_pct = (stats["implementation_only"] / total_reqs) * 100 + test_only_pct = (stats["test_only"] / total_reqs) * 100 + exception_pct = (stats["exception_count"] / total_reqs) * 100 + implication_pct = (stats["implication_count"] / total_reqs) * 100 + no_impl_pct = (stats["no_implementation"] / total_reqs) * 100 + + # Ensure percentages add up to exactly 100% by using precise calculation + # and assigning any remainder to the largest segment + if total_reqs > 0: + # Calculate exact percentages using integer arithmetic to avoid floating point errors + percentages_data = [ + (stats["implementation_and_test"], "impl_test"), + (stats["implication_count"], "implication"), + (stats["exception_count"], "exception"), + (stats["implementation_only"], "impl_only"), + (stats["no_implementation"], "no_impl"), + ] + + # Calculate percentages with high precision, then distribute remainder + total_allocated = 0.0 + calculated_percentages = {} + + # Calculate all but the last percentage + for i, (count, name) in enumerate(percentages_data[:-1]): + pct = round((count / total_reqs) * 100, 1) + calculated_percentages[name] = pct + total_allocated += pct + + # Last segment gets the remainder to ensure exactly 100% + last_count, last_name = percentages_data[-1] + calculated_percentages[last_name] = round(100.0 - total_allocated, 1) + + # Assign back to variables + impl_test_pct = calculated_percentages["impl_test"] + implication_pct = calculated_percentages["implication"] + exception_pct = calculated_percentages["exception"] + impl_only_pct = calculated_percentages["impl_only"] + no_impl_pct = calculated_percentages["no_impl"] + else: + impl_test_pct = impl_only_pct = test_only_pct = exception_pct = implication_pct = ( + no_impl_pct + ) = 0 + + # Generate summary statistics HTML with color-coded progress bars + content_html = f""" +
+
+
+
+ Requirements by Implementation Type + {stats['complete_requirements']}/{stats['total_requirements']} completed +
+
+
+
+
+
+
+
+
+
+ +
+
+
{stats['implementation_and_test']}
+
Implementation + Test
+
+
+
{stats['implication_count']}
+
Implication
+
+
+
{stats['exception_count']}
+
Exception
+
+
+
{stats['implementation_only']}
+
Implementation Only
+
+
+
{stats['no_implementation']}
+
No Implementation
+
+
+
{stats['total_requirements']}
+
Total
+
+
+
+ """ + + # Generate content for each specification + spec_counter = 0 + + for spec_title, spec_data in specifications.items(): + status_icon = get_spec_status(spec_data) + sections = spec_data.get("sections", {}) + + # Calculate requirement-level progress for this spec + spec_total_requirements = 0 + spec_complete_requirements = 0 + + for section_data in sections.values(): + section_requirements = section_data.get("requirements", []) + spec_total_requirements += len(section_requirements) + spec_complete_requirements += sum( + 1 for req in section_requirements if req["is_complete"] + ) + + # Determine alternating background class + row_class = "even" if spec_counter % 2 == 0 else "odd" + spec_counter += 1 + + # Generate spec-specific URL + spec_url = generate_spec_url(duvet_report_path, spec_data["spec_path"]) + + # Calculate spec-level statistics + spec_impl_test = 0 + spec_implication = 0 + spec_exception = 0 + spec_impl_only = 0 + spec_no_impl = 0 + + for section_data in sections.values(): + section_requirements = section_data.get("requirements", []) + for req in section_requirements: + if req["has_implementation"] and req["has_test"]: + spec_impl_test += 1 + elif req["has_implication"]: + spec_implication += 1 + elif req["has_exception"]: + spec_exception += 1 + elif req["has_implementation"]: + spec_impl_only += 1 + else: + spec_no_impl += 1 + + # Calculate percentages for spec progress bar + if spec_total_requirements > 0: + spec_impl_test_pct = (spec_impl_test / spec_total_requirements) * 100 + spec_implication_pct = (spec_implication / spec_total_requirements) * 100 + spec_exception_pct = (spec_exception / spec_total_requirements) * 100 + spec_impl_only_pct = (spec_impl_only / spec_total_requirements) * 100 + spec_no_impl_pct = (spec_no_impl / spec_total_requirements) * 100 + else: + spec_impl_test_pct = spec_implication_pct = spec_exception_pct = spec_impl_only_pct = ( + spec_no_impl_pct + ) = 0 + + content_html += f""" +
+
+
+ {status_icon} + {spec_title} + ({spec_complete_requirements}/{spec_total_requirements} completed) + 🔗 +
+ +
+ +
+""" + + # Add sections within each specification + for section_title, section_data in sections.items(): + section_requirements = section_data.get("requirements", []) + section_complete = sum(1 for req in section_requirements if req["is_complete"]) + section_total = len(section_requirements) + + # Skip sections with no requirements at all + if section_total == 0: + continue + + # Determine section status using the corrected logic + # Get individual requirement statuses + req_statuses = [get_requirement_status(req) for req in section_requirements] + + if all(status == "✅" for status in req_statuses): + section_status = "✅" # All requirements are green + elif any(status in ["✅", "🟡"] for status in req_statuses): + section_status = "🟡" # Any requirement is green or yellow + else: + section_status = "❌" # All requirements are red X + + section_id = f"{spec_title.replace(' ', '_')}_{section_title.replace(' ', '_').replace('#', '').replace('-', '_')}" + + # Generate section-specific URL + section_url = generate_section_url( + duvet_report_path, spec_data["spec_path"], section_data["section_id"] + ) + + # Generate local file path for this section + local_file_path = f"{spec_data['spec_path']}#{section_data['section_id']}" + + # Calculate section-level statistics + section_impl_test = sum( + 1 for req in section_requirements if req["has_implementation"] and req["has_test"] + ) + section_implication = sum(1 for req in section_requirements if req["has_implication"]) + section_exception = sum(1 for req in section_requirements if req["has_exception"]) + section_impl_only = sum( + 1 + for req in section_requirements + if req["has_implementation"] + and not req["has_test"] + and not req["has_exception"] + and not req["has_implication"] + ) + section_no_impl = sum( + 1 + for req in section_requirements + if not req["has_implementation"] + and not req["has_test"] + and not req["has_exception"] + and not req["has_implication"] + ) + + # Calculate percentages for section progress bar + if section_total > 0: + section_impl_test_pct = (section_impl_test / section_total) * 100 + section_implication_pct = (section_implication / section_total) * 100 + section_exception_pct = (section_exception / section_total) * 100 + section_impl_only_pct = (section_impl_only / section_total) * 100 + section_no_impl_pct = (section_no_impl / section_total) * 100 + else: + section_impl_test_pct = section_implication_pct = section_exception_pct = ( + section_impl_only_pct + ) = section_no_impl_pct = 0 + + content_html += f""" +
+
+
+ {section_status} + {section_title} + ({section_complete}/{section_total} completed) + 🔗 +
+ +
+
+ +
+ {local_file_path} + +
+""" + + # Add requirements within each section + req_counter = 1 + for requirement in section_requirements: + req_status = get_requirement_status(requirement) + req_text = format_requirement_text(requirement["text"]) + + # Build detailed source information with GitHub links - one bullet per source + sources_html = "" + if requirement["related_sources"]: + source_bullets = [] + for source_info in requirement["related_sources"]: + source_type = source_info["type"] + source_path = source_info["source"] + line_num = source_info["line"] + + # Generate GitHub URL if possible + github_url = generate_github_url(source_path, line_num, github_base_url) + + if github_url and source_path.endswith(".go"): + # Create clickable link for Go source files + source_display = f'{source_path}' + if line_num: + source_display += f":{line_num}" + source_display += "" + else: + # Plain text for non-Go files or when no GitHub URL + source_display = source_path + if line_num: + source_display += f":{line_num}" + + type_display = source_type.lower() + # Add partial indicator if this requirement has partial coverage + if requirement.get("has_partial_coverage", False): + type_display = f"partial {type_display}" + source_bullets.append(f"• {type_display}: {source_display}") + + sources_html = ( + '
' + + "
".join(source_bullets) + + "
" + ) + else: + sources_html = '
• no implementation found
' + + # Determine requirement type for filtering + if requirement["has_exception"]: + req_type = "exception" + elif requirement["has_implication"]: + req_type = "implication" + elif ( + requirement["has_implementation"] + and requirement["has_test"] + and not requirement.get("has_partial_coverage", False) + ): + req_type = "impl-test" + elif requirement["has_implementation"] and not requirement.get( + "has_partial_coverage", False + ): + req_type = "impl-only" + else: + # Partial coverage and no implementation both get "none" type + req_type = "none" + + # Prepare requirement text for copying (clean version without HTML) + clean_req_text = requirement["text"].replace("\n", " ").strip() + # Escape single quotes for JavaScript + clean_req_text = clean_req_text.replace("'", "\\'") + copy_text = f"//# {clean_req_text}" + + content_html += f""" +
+
+ Requirement {req_counter}: + {req_status} + +
+
{req_text}
+ {sources_html} +
+""" + req_counter += 1 + + content_html += """ +
+
+""" + + content_html += """ +
+
+""" + + # Replace placeholders in template + html_content = template.format(server_name=server_name, content=content_html) + + # Write the HTML file + with open(output_file_path, "w", encoding="utf-8") as f: + f.write(html_content) + + +def generate_server_report(server_path, server_name): + """Generate individual server report using the enhanced report-based format.""" + report_file = server_path / ".duvet" / "reports" / "report.html" + + if not report_file.exists(): + return None + + try: + # Parse the report directly + specifications = parse_report_html(report_file) + + # Generate the enhanced HTML report + html_output_file = server_path / "compliance_summary_report.html" + generate_enhanced_html_report(report_file, html_output_file, server_name) + + # Calculate detailed statistics + stats = calculate_summary_statistics(specifications) + + # Calculate overall status based on actual implementation progress + total_reqs = stats.get("total_requirements", 0) + complete_reqs = stats.get("complete_requirements", 0) + + if total_reqs == 0: + overall_status = "❌" # No requirements means not compliant + elif complete_reqs == total_reqs: + overall_status = "✅" # All requirements complete + elif complete_reqs > 0: + overall_status = "🟡" # Some requirements complete + else: + overall_status = "❌" # No requirements complete + + # Calculate spec-level status + spec_statuses = {} + for spec_title, spec_data in specifications.items(): + spec_statuses[spec_title] = get_spec_status(spec_data) + + total_specs = len(specifications) + complete_specs = sum(1 for status in spec_statuses.values() if status == "✅") + + return { + "name": server_name, + "status": overall_status, + "total_specs": total_specs, + "complete_specs": complete_specs, + "total_sections": stats["total_sections"], + "complete_sections": stats["complete_sections"], + "total_requirements": stats["total_requirements"], + "complete_requirements": stats["complete_requirements"], + "report_file": f"../{server_name}/compliance_summary_report.html", + "specifications": spec_statuses, + "stats": stats, # Include full stats for homepage display + } + + except Exception as e: + print(f"Error processing {server_name}: {e}") + return None + + +def generate_expected_output(report_file_path, output_file_path): + """Generate the expected output format from report.html.""" + specifications = parse_report_html(report_file_path) + + output_lines = [] + for spec_title, spec_data in specifications.items(): + status_icon = get_spec_status(spec_data) + output_lines.append(f"{spec_title}: {status_icon}") + + # Write the output file + with open(output_file_path, "w", encoding="utf-8") as f: + f.write("\n".join(output_lines)) + + +def generate_stats_output(report_file_path, output_file_path): + """Generate detailed statistics output for dashboard use.""" + specifications = parse_report_html(report_file_path) + stats = calculate_summary_statistics(specifications) + + # Write stats as JSON for easy parsing + import json + + with open(output_file_path, "w", encoding="utf-8") as f: + json.dump(stats, f, indent=2) + + +def generate_homepage(servers_info, output_file): + """Generate the main homepage with links to all server reports using templates.""" + + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Load the homepage template + template_dir = Path(__file__).parent / "templates" + template = load_template(template_dir / "homepage_template.html") + + content_html = "" + + if servers_info: + # Calculate overall statistics + total_servers = len(servers_info) + compliant_servers = sum(1 for server in servers_info if server["status"] == "✅") + partial_servers = sum(1 for server in servers_info if server["status"] == "🟡") + non_compliant_servers = sum(1 for server in servers_info if server["status"] == "❌") + + # Add compact dark mode summary header + content_html += f""" +
+
+
+ {total_servers} +
Total
+
+
+ {compliant_servers} +
Compliant
+
+
+ {partial_servers} +
Partial
+
+
+ {non_compliant_servers} +
Missing
+
+
+
+ +
+""" + + # Generate server cards with detailed statistics + for server in sorted(servers_info, key=lambda x: x["name"]): + # Get detailed stats for this server + server_stats = server.get("stats", {}) + + # Calculate percentages for each implementation type + total_reqs = server_stats.get("total_requirements", 0) + if total_reqs > 0: + # Calculate raw percentages + impl_test_pct = (server_stats.get("implementation_and_test", 0) / total_reqs) * 100 + impl_only_pct = (server_stats.get("implementation_only", 0) / total_reqs) * 100 + test_only_pct = (server_stats.get("test_only", 0) / total_reqs) * 100 + exception_pct = (server_stats.get("exception_count", 0) / total_reqs) * 100 + implication_pct = (server_stats.get("implication_count", 0) / total_reqs) * 100 + no_impl_pct = (server_stats.get("no_implementation", 0) / total_reqs) * 100 + + # Ensure percentages add up to exactly 100% by using precise calculation + if total_reqs > 0: + # Calculate exact percentages and distribute remainder to largest segment + percentages_data = [ + (server_stats.get("implementation_and_test", 0), "impl_test"), + (server_stats.get("implication_count", 0), "implication"), + (server_stats.get("exception_count", 0), "exception"), + (server_stats.get("implementation_only", 0), "impl_only"), + (server_stats.get("no_implementation", 0), "no_impl"), + ] + + # Calculate percentages with high precision, then distribute remainder + total_allocated = 0.0 + calculated_percentages = {} + + # Calculate all but the last percentage + for i, (count, name) in enumerate(percentages_data[:-1]): + pct = round((count / total_reqs) * 100, 1) + calculated_percentages[name] = pct + total_allocated += pct + + # Last segment gets the remainder to ensure exactly 100% + last_count, last_name = percentages_data[-1] + calculated_percentages[last_name] = round(100.0 - total_allocated, 1) + + # Assign back to variables + impl_test_pct = calculated_percentages["impl_test"] + implication_pct = calculated_percentages["implication"] + exception_pct = calculated_percentages["exception"] + impl_only_pct = calculated_percentages["impl_only"] + no_impl_pct = calculated_percentages["no_impl"] + else: + impl_test_pct = impl_only_pct = test_only_pct = exception_pct = implication_pct = ( + no_impl_pct + ) = 0 + + content_html += f""" +
+
+
{server['name']}
+
{server['status']}
+
+
+
+
+ Requirements Progress + {server_stats.get('complete_requirements', 0)}/{server_stats.get('total_requirements', 0)} completed +
+
+
+
+
+
+
+
+
+ +
+
+
{server_stats.get('implementation_and_test', 0)}
+
Impl+Test
+
+
+
{server_stats.get('implication_count', 0)}
+
Implication
+
+
+
{server_stats.get('exception_count', 0)}
+
Exception
+
+
+
{server_stats.get('implementation_only', 0)}
+
Impl Only
+
+
+
{server_stats.get('no_implementation', 0)}
+
None
+
+
+
{server_stats.get('total_requirements', 0)}
+
Total
+
+
+
+ +
+""" + + content_html += """ +
+""" + else: + content_html += """ +
+

No servers with compliance reports found.

+

Make sure servers have .duvet/reports/report.html files.

+
+""" + + # Replace placeholders in template + html_content = template.format(timestamp=current_time, content=content_html) + + # Write the HTML file + with open(output_file, "w", encoding="utf-8") as f: + f.write(html_content) + + +def discover_servers(): + """Discover all servers with .duvet/reports/report.html files.""" + servers_info = [] + # Get the test-server directory (parent of spec-compliance-dashboard) + test_server_dir = Path(__file__).parent.parent + + # Look for directories with .duvet/reports/report.html + for item in test_server_dir.iterdir(): + if ( + item.is_dir() + and not item.name.startswith(".") + and item.name != "spec-compliance-dashboard" + ): + duvet_report = item / ".duvet" / "reports" / "report.html" + if duvet_report.exists(): + server_info = generate_server_report(item, item.name) + if server_info: + servers_info.append(server_info) + print(f"Processed server: {item.name}") + + return servers_info + + +def main(): + """Main function to generate both individual server reports and dashboard.""" + import sys + + # Check if server directory is provided as argument (for single server mode) + if len(sys.argv) > 1: + server_dir = Path(sys.argv[1]) + server_name = sys.argv[2] if len(sys.argv) > 2 else server_dir.name + + report_file = server_dir / ".duvet" / "reports" / "report.html" + html_output_file = server_dir / "compliance_summary_report.html" + expected_output_file = server_dir / "expected_output_report.txt" + + if not report_file.exists(): + print(f"Error: Report file not found at {report_file}") + return 1 + + try: + # Generate HTML report + generate_enhanced_html_report(report_file, html_output_file, server_name) + print(f"Interactive HTML report generated: {html_output_file}") + + # Generate expected output + generate_expected_output(report_file, expected_output_file) + print(f"Expected output generated: {expected_output_file}") + + # Generate stats output for dashboard + stats_output_file = server_dir / "compliance_stats.json" + generate_stats_output(report_file, stats_output_file) + print(f"Stats output generated: {stats_output_file}") + + return 0 + except Exception as e: + print(f"Error generating reports: {e}") + return 1 + else: + # Dashboard mode - discover all servers and generate dashboard + try: + print("Discovering servers with compliance reports...") + servers_info = discover_servers() + + if servers_info: + print(f"Found {len(servers_info)} servers with reports") + + # Generate the main dashboard homepage + homepage_file = Path(__file__).parent / "compliance_homepage.html" + generate_homepage(servers_info, homepage_file) + print(f"Dashboard homepage generated: {homepage_file}") + + return 0 + else: + print("No servers with .duvet/reports/report.html found") + return 1 + + except Exception as e: + print(f"Error generating dashboard: {e}") + return 1 + + +if __name__ == "__main__": + exit(main()) diff --git a/test-server/spec-compliance-dashboard/templates/homepage_styles.css b/test-server/spec-compliance-dashboard/templates/homepage_styles.css new file mode 100644 index 00000000..466f1393 --- /dev/null +++ b/test-server/spec-compliance-dashboard/templates/homepage_styles.css @@ -0,0 +1,335 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + margin: 0; + padding: 20px; + background-color: #0d1117; + color: #c9d1d9; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: #161b22; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border: 1px solid #30363d; + overflow: hidden; +} + +.header { + background: #21262d; + color: #c9d1d9; + padding: 15px 20px; + text-align: center; + border-bottom: 1px solid #30363d; +} + +.header h1 { + margin: 0; + font-size: 1.8em; + font-weight: 400; +} + +.header p { + margin: 5px 0 0 0; + opacity: 0.9; + font-size: 0.9em; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + padding: 30px; + background: #0d1117; +} + +.stat-card { + background: #161b22; + padding: 20px; + border-radius: 6px; + text-align: center; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border: 1px solid #30363d; +} + +.stat-number { + font-size: 2em; + font-weight: bold; + color: #c9d1d9; +} + +.stat-label { + color: #8b949e; + font-size: 0.9em; + margin-top: 5px; +} + +.servers-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 20px; + padding: 30px; + max-width: 1200px; + margin: 0 auto; +} + +.server-card { + background: #161b22; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border: 1px solid #30363d; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; +} + +.server-card:hover { + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0,0,0,0.4); +} + +.server-header { + padding: 12px 16px; + background: #21262d; + color: #c9d1d9; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #30363d; +} + +.server-name { + font-size: 1.2em; + font-weight: 600; +} + +.server-status { + font-size: 1.5em; +} + +.server-body { + padding: 20px; +} + +.progress-bar { + background: #0d1117; + border-radius: 6px; + height: 8px; + margin: 15px 0; + overflow: hidden; + border: 1px solid #30363d; +} + +.progress-fill { + height: 100%; + background: #238636; + border-radius: 6px; + transition: width 0.3s ease; +} + +.progress-bar.color-coded { + display: flex; + height: 12px; +} + +.progress-segment { + height: 100%; + transition: width 0.3s ease; + position: relative; +} + +.progress-segment:first-child { + border-radius: 6px 0 0 6px; +} + +.progress-segment:last-child { + border-radius: 0 6px 6px 0; +} + +.progress-segment:only-child { + border-radius: 6px; +} + +.progress-segment.impl-test { + background: #28a745; +} + +.progress-segment.impl-only { + background: #ffc107; +} + +.progress-segment.test-only { + background: #6c757d; +} + +.progress-segment.exception { + background: #87ceeb; +} + +.progress-segment.implication { + background: #dda0dd; +} + +.progress-segment.no-impl { + background: #dc3545; +} + +.progress-item { + margin-bottom: 15px; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.progress-label { + color: #c9d1d9; + font-weight: 500; + font-size: 0.9em; +} + +.progress-count { + color: #8b949e; + font-size: 0.8em; +} + +.breakdown-grid-compact { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; + margin-top: 10px; +} + +.breakdown-item-compact { + background: #0d1117; + padding: 8px 4px; + border-radius: 4px; + text-align: center; + border: 1px solid #30363d; +} + +.breakdown-number-compact { + font-size: 1.1em; + font-weight: bold; + color: #c9d1d9; + margin-bottom: 2px; +} + +.breakdown-label-compact { + color: #8b949e; + font-size: 0.7em; + line-height: 1.2; +} + +/* Regular breakdown grid (used by the generated HTML) */ +.breakdown-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 8px; + margin-top: 10px; +} + +.breakdown-item { + background: transparent; + padding: 8px 4px; + border-radius: 4px; + text-align: center; + border: none; +} + +.breakdown-number { + font-size: 1.1em; + font-weight: bold; + color: #c9d1d9; + margin-bottom: 2px; +} + +.breakdown-label { + color: #8b949e; + font-size: 0.7em; + line-height: 1.2; +} + +.server-summary { + margin-top: 15px; +} + +.summary-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; +} + +.summary-row:last-child { + margin-bottom: 0; +} + +.summary-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.summary-number { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 2px; +} + +.summary-label { + color: #8b949e; + font-size: 0.75em; + text-align: center; +} + +.server-stats { + display: flex; + justify-content: space-between; + margin-top: 15px; + font-size: 0.9em; + color: #8b949e; +} + +.server-footer { + padding: 15px 20px; + background: #0d1117; + border-top: 1px solid #30363d; + text-align: center; +} + +.view-report-btn { + display: inline-block; + padding: 10px 20px; + background: #238636; + color: white; + text-decoration: none; + border-radius: 6px; + transition: background-color 0.2s; + font-size: 0.9em; +} + +.view-report-btn:hover { + background: #2ea043; +} + +.no-data { + text-align: center; + padding: 40px; + color: #8b949e; +} + +.footer { + padding: 20px; + text-align: center; + background: #21262d; + color: #8b949e; + font-size: 0.9em; + border-top: 1px solid #30363d; +} diff --git a/test-server/spec-compliance-dashboard/templates/homepage_template.html b/test-server/spec-compliance-dashboard/templates/homepage_template.html new file mode 100644 index 00000000..eddc8a4d --- /dev/null +++ b/test-server/spec-compliance-dashboard/templates/homepage_template.html @@ -0,0 +1,21 @@ + + + + + + Spec Compliance Dashboard + + + +
+
+

Spec Compliance Dashboard

+

Last updated: {timestamp}

+
+ {content} + +
+ + diff --git a/test-server/spec-compliance-dashboard/templates/report_template.html b/test-server/spec-compliance-dashboard/templates/report_template.html new file mode 100644 index 00000000..94d06ff8 --- /dev/null +++ b/test-server/spec-compliance-dashboard/templates/report_template.html @@ -0,0 +1,276 @@ + + + + + + {server_name} - Duvet Compliance Report + + + +
+ + {content} +
+ + + + diff --git a/test-server/spec-compliance-dashboard/templates/styles.css b/test-server/spec-compliance-dashboard/templates/styles.css new file mode 100644 index 00000000..161175d6 --- /dev/null +++ b/test-server/spec-compliance-dashboard/templates/styles.css @@ -0,0 +1,387 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + margin: 0; + padding: 20px; + background-color: #0d1117; + color: #c9d1d9; +} + +.container { + max-width: 1000px; + margin: 0 auto; + background: #161b22; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border: 1px solid #30363d; + overflow: hidden; +} + +.header { + background: #21262d; + color: #c9d1d9; + padding: 8px 15px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #30363d; +} + +.header h1 { + margin: 0; + font-size: 1.2em; + font-weight: 500; +} + +.nav-link { + color: white; + text-decoration: none; + font-size: 0.9em; + opacity: 0.9; +} + +.nav-link:hover { + opacity: 1; + text-decoration: underline; +} + +.spec-section { + border-bottom: 1px solid #30363d; +} + +.spec-section.even { + background: #161b22; +} + +.spec-section.odd { + background: #0d1117; +} + +.spec-header { + padding: 15px 20px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + background: transparent; + transition: background-color 0.2s; + color: #c9d1d9; +} + +.spec-header:hover { + background: #21262d; +} + +.spec-title { + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.completion-count { + color: #8b949e; + font-size: 0.8em; + font-weight: 400; +} + +.status-emoji { + font-size: 20px; +} + +.expand-icon { + font-size: 14px; + transition: transform 0.2s; +} + +.spec-content { + display: none; + padding: 20px; + background: transparent; +} + +.spec-content.expanded { + display: block; +} + +.requirement-item { + margin-bottom: 15px; + padding: 15px; + background: #161b22; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border: 1px solid #30363d; + color: #c9d1d9; +} + +.requirement-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + color: #c9d1d9; +} + +.requirement-id { + font-weight: bold; + color: #c9d1d9; +} + +.requirement-status { + font-size: 16px; +} + +.requirement-text { + color: #c9d1d9; + white-space: pre-wrap; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.4; +} + +.section-item { + margin-bottom: 10px; + border-radius: 6px; + background: #21262d; + border: 1px solid #30363d; +} + +.section-header { + padding: 12px 15px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + background: transparent; + transition: background-color 0.2s; + color: #c9d1d9; +} + +.section-header:hover { + background: #30363d; +} + +.section-title { + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.section-content { + display: none; + padding: 15px; + background: transparent; +} + +.section-content.expanded { + display: block; +} + +.requirement-metadata { + color: #8b949e; + font-size: 12px; + margin-top: 8px; + font-style: italic; +} + +.status-metadata { + color: #6e7681; + font-size: 12px; + font-style: italic; +} + +.summary-stats { + padding: 20px; + background: #0d1117; + border-bottom: 1px solid #30363d; +} + +.summary-stats h2 { + margin: 0 0 15px 0; + color: #c9d1d9; + font-size: 1.4em; + font-weight: 600; +} + +.summary-stats h3 { + margin: 20px 0 10px 0; + color: #c9d1d9; + font-size: 1.1em; + font-weight: 500; +} + +.progress-section { + margin-bottom: 20px; +} + +.progress-item { + margin-bottom: 15px; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.progress-label { + color: #c9d1d9; + font-weight: 500; +} + +.progress-count { + color: #8b949e; + font-size: 0.9em; +} + +.progress-bar { + background: #21262d; + border-radius: 6px; + height: 8px; + overflow: hidden; + border: 1px solid #30363d; +} + +.progress-fill { + height: 100%; + background: #238636; + border-radius: 6px; + transition: width 0.3s ease; +} + +.progress-bar.color-coded { + display: flex; + height: 12px; +} + +.progress-segment { + height: 100%; + transition: width 0.3s ease; + position: relative; +} + +.progress-segment:first-child { + border-radius: 6px 0 0 6px; +} + +.progress-segment:last-child { + border-radius: 0 6px 6px 0; +} + +.progress-segment:only-child { + border-radius: 6px; +} + +.progress-segment.impl-test { + background: #28a745; +} + +.progress-segment.impl-only { + background: #ffc107; +} + +.progress-segment.test-only { + background: #6c757d; +} + +.progress-segment.exception { + background: #87ceeb; +} + +.progress-segment.implication { + background: #dda0dd; +} + +.progress-segment.no-impl { + background: #dc3545; +} + +.breakdown-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 10px; +} + +.breakdown-grid.single-row { + grid-template-columns: repeat(6, 1fr); + grid-template-rows: 1fr; +} + +.breakdown-item { + background: transparent; + padding: 12px; + border-radius: 6px; + text-align: center; + border: none; + transition: all 0.2s ease; +} + +.breakdown-item.clickable-filter { + cursor: pointer; + border: 1px solid transparent; +} + +.breakdown-item.clickable-filter:hover { + background: #21262d; + border: 1px solid #30363d; + transform: translateY(-1px); +} + +.breakdown-item.active-filter { + background: #21262d; + border: 2px solid #58a6ff; + box-shadow: 0 0 8px rgba(88, 166, 255, 0.3); +} + +.breakdown-number { + font-size: 1.4em; + font-weight: bold; + color: #c9d1d9; + margin-bottom: 3px; +} + +.breakdown-label { + color: #8b949e; + font-size: 0.8em; +} + +.breakdown-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + padding: 10px 0; + border-bottom: 1px solid #30363d; + margin-bottom: 15px; +} + +.breakdown-header:hover { + background: #21262d; + border-radius: 6px; + padding: 10px 15px; + margin: 0 -15px 15px -15px; +} + +.breakdown-header h3 { + margin: 0; +} + +.pie-chart-container { + background: #161b22; + padding: 20px; + border-radius: 6px; + border: 1px solid #30363d; + margin-top: 15px; + justify-content: center; + align-items: center; +} + +.pie-chart-container canvas { + max-width: 100%; + height: auto; +} diff --git a/test-server/spec-compliance-dashboard/templates/summary_stats_template.html b/test-server/spec-compliance-dashboard/templates/summary_stats_template.html new file mode 100644 index 00000000..0415d138 --- /dev/null +++ b/test-server/spec-compliance-dashboard/templates/summary_stats_template.html @@ -0,0 +1,63 @@ +
+

Summary Statistics

+
+
+
+ Sections Implemented + {complete_sections}/{total_sections} +
+
+
+
+
+
+
+ Requirements Implemented + {complete_requirements}/{total_requirements} +
+
+
+
+
+
+ +
+

Implementation Breakdown

+ +
+
+
+
{implementation_and_test}
+
Implementation + Test
+
+
+
{implementation_only}
+
Implementation Only
+
+
+
{test_only}
+
Test Only
+
+
+
{exception_count}
+
Exception
+
+
+
{implication_count}
+
Implication
+
+
+
{no_implementation}
+
No Implementation
+
+
+ + + + +
From f961822a7c9ca47116c9d1f58edf52d28d9f4e33 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 24 Oct 2025 14:17:50 -0700 Subject: [PATCH 116/201] chore: run CI on pushes to fireegg-test-servers --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 691144d8..ac14bcf0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: Main Workflow on: push: - branches: [ main ] + branches: [ main, fireegg-test-servers ] pull_request: workflow_dispatch: inputs: From ce432a7176adc51dc0a2579d30c3f077b810c1db Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 27 Oct 2025 11:22:57 -0700 Subject: [PATCH 117/201] chore: Bump Go V4 server to spec-complete commit (#43) --- test-server/go-v4-server/.duvet/.gitignore | 3 +++ test-server/go-v4-server/.duvet/config.toml | 27 +++++++++++++++++++ test-server/go-v4-server/Makefile | 6 +++++ test-server/go-v4-server/go.mod | 2 +- test-server/go-v4-server/go.sum | 1 - test-server/go-v4-server/local-go-s3ec | 2 +- test-server/go-v4-server/main.go | 19 ++++++++++++- .../amazon/encryption/s3/CBCDecryptTests.java | 4 +++ .../amazon/encryption/s3/TestUtils.java | 2 +- test-server/specification | 2 +- 10 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 test-server/go-v4-server/.duvet/.gitignore create mode 100644 test-server/go-v4-server/.duvet/config.toml diff --git a/test-server/go-v4-server/.duvet/.gitignore b/test-server/go-v4-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/go-v4-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/go-v4-server/.duvet/config.toml b/test-server/go-v4-server/.duvet/config.toml new file mode 100644 index 00000000..713e72d3 --- /dev/null +++ b/test-server/go-v4-server/.duvet/config.toml @@ -0,0 +1,27 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "local-go-s3ec/v4/**/*.go" + +# 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/go-v4-server/Makefile b/test-server/go-v4-server/Makefile index a8a2553d..cfaf32fe 100644 --- a/test-server/go-v4-server/Makefile +++ b/test-server/go-v4-server/Makefile @@ -23,3 +23,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/go-v4-server/go.mod b/test-server/go-v4-server/go.mod index 4ab1895c..33b1cc9f 100644 --- a/test-server/go-v4-server/go.mod +++ b/test-server/go-v4-server/go.mod @@ -1,6 +1,6 @@ module github.com/aws/amazon-s3-encryption-client-python/test-server/go-server -go 1.21 +go 1.24 require ( github.com/aws/amazon-s3-encryption-client-go/v4 v4.0.0 diff --git a/test-server/go-v4-server/go.sum b/test-server/go-v4-server/go.sum index 1bb969a3..f4e3646a 100644 --- a/test-server/go-v4-server/go.sum +++ b/test-server/go-v4-server/go.sum @@ -1,4 +1,3 @@ - github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index cbb8bc60..6a2a7fe0 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit cbb8bc608754ae52f8063987d0570a7c5a927fa0 +Subproject commit 6a2a7fe0418ceebc1b555c0f79b6328896e81939 diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go index 75871d5f..672236ac 100644 --- a/test-server/go-v4-server/main.go +++ b/test-server/go-v4-server/main.go @@ -11,6 +11,7 @@ import ( "github.com/aws/amazon-s3-encryption-client-go/v4/client" "github.com/aws/amazon-s3-encryption-client-go/v4/materials" + "github.com/aws/amazon-s3-encryption-client-go/v4/commitment" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/kms" @@ -42,6 +43,7 @@ type S3ECConfig struct { EnableLegacyWrappingAlgorithms bool `json:"enableLegacyWrappingAlgorithms"` SetBufferSize int64 `json:"setBufferSize"` KeyMaterial KeyMaterial `json:"keyMaterial"` + CommitmentPolicy string `json:"commitmentPolicy"` } // KeyMaterial represents the key material for encryption @@ -147,6 +149,16 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { return } + var commitmentPolicy commitment.CommitmentPolicy + switch input.Config.CommitmentPolicy { + case "REQUIRE_ENCRYPT_REQUIRE_DECRYPT": + commitmentPolicy = commitment.REQUIRE_ENCRYPT_REQUIRE_DECRYPT + case "REQUIRE_ENCRYPT_ALLOW_DECRYPT": + commitmentPolicy = commitment.REQUIRE_ENCRYPT_ALLOW_DECRYPT + case "FORBID_ENCRYPT_ALLOW_DECRYPT": + commitmentPolicy = commitment.FORBID_ENCRYPT_ALLOW_DECRYPT + } + // Create KMS keyring kmsClient := kms.NewFromConfig(cfg) keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { @@ -162,7 +174,12 @@ func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { // Create S3 encryption client var s3EncryptionClient *client.S3EncryptionClientV4 s3PlaintextClient := s3.NewFromConfig(cfg) - s3EncryptionClient, err = client.New(s3PlaintextClient, cmm) + s3EncryptionClient, err = client.New(s3PlaintextClient, cmm, func(clientOptions *client.EncryptionClientOptions) { + if input.Config.CommitmentPolicy != "" { + clientOptions.CommitmentPolicy = commitmentPolicy + } + clientOptions.EnableLegacyUnauthenticatedModes = input.Config.EnableLegacyUnauthenticatedModes + }) if err != nil { s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create S3EC: %v", err), http.StatusInternalServerError) diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java index 3aba7506..4de6aef4 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java @@ -90,6 +90,7 @@ void transition_configured_with_the_default_should_decrypt_cbc(TestUtils.Languag .keyMaterial(kmsKeyArn) // .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) .build()) .build()); String S3ECId = clientOutput.getClientId(); @@ -107,6 +108,7 @@ void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc( .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) .build()) .build()); String S3ECId = clientOutput.getClientId(); @@ -123,6 +125,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc(Te .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) .build()) .build()); String decS3ECId = decClientOutput.getClientId(); @@ -139,6 +142,7 @@ void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_cbc(T .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) .build()) .build()); String decS3ECId = decClientOutput.getClientId(); 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 a79d5bc2..78cb6eb2 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 @@ -121,7 +121,7 @@ public class TestUtils { Set.of( // JAVA_V4, // PYTHON_V3, - // GO_V4, + GO_V4, // NET_V3, // CPP_V3, // PHP_V3, diff --git a/test-server/specification b/test-server/specification index 0ae23623..129b9c5e 160000 --- a/test-server/specification +++ b/test-server/specification @@ -1 +1 @@ -Subproject commit 0ae23623ff4b3330da0c58890fbc032a7d378d08 +Subproject commit 129b9c5e53a8c4f6be10a52c9d3dcdf765000d78 From cda651f8c01f823946138661d0a3d76d6d27e583 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 27 Oct 2025 12:32:17 -0700 Subject: [PATCH 118/201] updates to spec and tests (#49) 1. Update spec reference 2. Update ruby with latest annotations 3. Update tests to have a better base name --- .../src/it/java/software/amazon/encryption/s3/KC_GCMTests.java | 2 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 838df97e..9e77e20e 100644 --- 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 @@ -59,7 +59,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class KC_GCMTests { - private static String sharedObjectKeyBase = "test-gcm-kms"; + private static String sharedObjectKeyBase = "test-kc-gcm-kms"; private static KeyMaterial kmsKeyArn = KeyMaterial.builder() .kmsKeyId(TestUtils.KMS_KEY_ARN) .build(); diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index 5d67d8f3..8f550866 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 5d67d8f3d3c80b34cda643b83f189abe322ac4b4 +Subproject commit 8f5508662bbb5cdc04be76083244b814b0f2c828 diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 5d67d8f3..8f550866 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 5d67d8f3d3c80b34cda643b83f189abe322ac4b4 +Subproject commit 8f5508662bbb5cdc04be76083244b814b0f2c828 From db23adf649445c63cc6ce5e1f41ab2dc27e23643 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 27 Oct 2025 13:13:28 -0700 Subject: [PATCH 119/201] fix: update spec compliance GH page on push even when `make duvet` fails (#50) --- .github/workflows/duvet.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index 70e335c7..e5be7039 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -82,15 +82,15 @@ jobs: test-server/index.html - name: Setup Pages - if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' uses: actions/configure-pages@v5 - name: Upload Pages artifact - if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' uses: actions/upload-pages-artifact@v3 with: path: test-server/ - name: Deploy to GitHub Pages - if: github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' uses: actions/deploy-pages@v4 From 8c6db9eb44b082a28ac143ae9824fe272a3e2166 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:18:27 -0400 Subject: [PATCH 120/201] chore: reenable c++ (#52) --- test-server/Makefile | 3 +-- test-server/cpp-v2-server/Makefile | 24 +++++++++---------- test-server/cpp-v2-server/main.cpp | 2 +- test-server/cpp-v2-transition-server/Makefile | 22 ++++++++--------- test-server/cpp-v2-transition-server/main.cpp | 2 +- .../amazon/encryption/s3/TestUtils.java | 8 +++---- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/test-server/Makefile b/test-server/Makefile index cd667505..a166030a 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -9,7 +9,6 @@ all: start-all-servers run-tests ci: start-all-servers run-tests stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) -# SERVER_DIRS := cpp-v3-server START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) @@ -125,4 +124,4 @@ duvet: @for dir in $(SERVER_DIRS); do \ echo "Running make duvet in $$dir..."; \ $(MAKE) -C $$dir duvet; \ - done \ No newline at end of file + done diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 9399b631..cc562c1a 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -6,20 +6,20 @@ PID_FILE := server.pid PORT := 8085 build/s3ec-server: -# brew install libmicrohttpd nlohmann-json ossp-uuid -# git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git -# cd aws-sdk-cpp -# mkdir -p build && cd build && cmake .. + brew install libmicrohttpd nlohmann-json ossp-uuid + git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git + cd aws-sdk-cpp + mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server -# @echo "Starting Cpp V2 server..." -# cd build && make && \ -# 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" \ -# ./s3ec-server & echo $$! > $(PID_FILE) -# @echo "Cpp V2 server starting..." + @echo "Starting Cpp V2 server..." + cd build && make && \ + 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" \ + ./s3ec-server & echo $$! > $(PID_FILE) + @echo "Cpp V2 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index c89ea8c3..c4f2c240 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -61,7 +61,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - if (legacy1 || legacy2) { + if (legacy1 || legacy2) config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); auto encryption_client = std::make_shared(config); diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index b879358d..0a63b2ed 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -6,18 +6,18 @@ PID_FILE := server.pid PORT := 8097 build/s3ec-server: -# brew install libmicrohttpd nlohmann-json ossp-uuid -# mkdir -p build && cd build && cmake .. + brew install libmicrohttpd nlohmann-json ossp-uuid + mkdir -p build && cd build && cmake .. start-server: | build/s3ec-server -# @echo "Starting Cpp V2 server..." -# cd build && make && \ -# 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" \ -# ./s3ec-server & echo $$! > $(PID_FILE) -# @echo "Cpp V2 server starting..." + @echo "Starting Cpp V2 server..." + cd build && make && \ + 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" \ + ./s3ec-server & echo $$! > $(PID_FILE) + @echo "Cpp V2 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ @@ -26,7 +26,7 @@ stop-server: fi wait-for-server: -# $(MAKE) -C .. wait-for-port PORT=$(PORT) + $(MAKE) -C .. wait-for-port PORT=$(PORT) duvet: duvet report diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 4dd505bf..e8ce8e5c 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -61,7 +61,7 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, auto materials = std::make_shared(kms_key_id); CryptoConfigurationV2 config(materials); - if (legacy1 || legacy2) { + if (legacy1 || legacy2) config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); auto encryption_client = std::make_shared(config); 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 78cb6eb2..a6af0872 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 @@ -102,7 +102,7 @@ public class TestUtils { JAVA_V3_CURRENT, GO_V3_CURRENT, NET_V2_CURRENT, - // CPP_V2_CURRENT, + CPP_V2_CURRENT, RUBY_V2_CURRENT, PHP_V2_CURRENT ); @@ -112,7 +112,7 @@ public class TestUtils { // JAVA_V3_TRANSITION, // GO_V3_TRANSITION, // NET_V2_TRANSITION, - // CPP_V2_TRANSITION, + CPP_V2_TRANSITION, // PHP_V2_TRANSITION, RUBY_V2_TRANSITION ); @@ -137,7 +137,7 @@ public class TestUtils { servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); - // servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); + servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); @@ -147,7 +147,7 @@ public class TestUtils { 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); 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, "8090")); From b131869e5897d633458f4b7aff664605dc9c1ade Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:25:34 -0400 Subject: [PATCH 121/201] cpp-v3 test server (#55) --- .github/workflows/test.yml | 13 +- test-server/Makefile | 4 +- test-server/cpp-v2-server/.duvet/config.toml | 5 + .../.duvet/.gitignore | 3 + .../.duvet/config.toml | 5 + test-server/cpp-v2-transition-server/main.cpp | 25 ++ test-server/cpp-v3-server/.duvet/.gitignore | 3 + test-server/cpp-v3-server/.duvet/config.toml | 29 ++ test-server/cpp-v3-server/CMakeLists.txt | 39 +++ test-server/cpp-v3-server/Makefile | 35 ++ test-server/cpp-v3-server/README.md | 37 ++ test-server/cpp-v3-server/main.cpp | 322 ++++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 5 +- test-server/net-v2-v3-server/Makefile | 2 +- 14 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 test-server/cpp-v2-transition-server/.duvet/.gitignore create mode 100644 test-server/cpp-v3-server/.duvet/.gitignore create mode 100644 test-server/cpp-v3-server/.duvet/config.toml create mode 100644 test-server/cpp-v3-server/CMakeLists.txt create mode 100644 test-server/cpp-v3-server/Makefile create mode 100644 test-server/cpp-v3-server/README.md create mode 100644 test-server/cpp-v3-server/main.cpp diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c8a9616..72045f16 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: # ask Ryan to edit this PAT's permissions to add access to a new private repo. token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - - name: Checkout CPP code + - name: Checkout CPP code for cpp-v2-transition uses: actions/checkout@v5 with: submodules: recursive @@ -36,6 +36,15 @@ jobs: ref: fire-egg-dev path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ + - name: Checkout CPP code cpp-v3 + uses: actions/checkout@v5 + with: + submodules: recursive + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v3-server/aws-sdk-cpp/ + - name: Checkout .NET V2 code uses: actions/checkout@v5 with: @@ -94,7 +103,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/uv - key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }} + key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-uv- diff --git a/test-server/Makefile b/test-server/Makefile index a166030a..be8df10b 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -3,10 +3,10 @@ .PHONY: all start-servers run-tests stop-servers clean ci check-env help # Default target -all: start-all-servers run-tests +all: start-all-servers wait-all-servers run-tests # CI target for GitHub Actions -ci: start-all-servers run-tests stop-servers +ci: start-all-servers wait-all-servers run-tests stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) diff --git a/test-server/cpp-v2-server/.duvet/config.toml b/test-server/cpp-v2-server/.duvet/config.toml index 88bb7213..d137df36 100644 --- a/test-server/cpp-v2-server/.duvet/config.toml +++ b/test-server/cpp-v2-server/.duvet/config.toml @@ -14,7 +14,12 @@ source = "../specification/s3-encryption/data-format/metadata-strategy.md" [[specification]] source = "../specification/s3-encryption/encryption.md" [[specification]] +source = "../specification/s3-encryption/decryption.md" +[[specification]] source = "../specification/s3-encryption/key-derivation.md" +[[specification]] +source = "../specification/s3-encryption/key-commitment.md" + [report.html] enabled = true diff --git a/test-server/cpp-v2-transition-server/.duvet/.gitignore b/test-server/cpp-v2-transition-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/cpp-v2-transition-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/cpp-v2-transition-server/.duvet/config.toml b/test-server/cpp-v2-transition-server/.duvet/config.toml index 88bb7213..d137df36 100644 --- a/test-server/cpp-v2-transition-server/.duvet/config.toml +++ b/test-server/cpp-v2-transition-server/.duvet/config.toml @@ -14,7 +14,12 @@ source = "../specification/s3-encryption/data-format/metadata-strategy.md" [[specification]] source = "../specification/s3-encryption/encryption.md" [[specification]] +source = "../specification/s3-encryption/decryption.md" +[[specification]] source = "../specification/s3-encryption/key-derivation.md" +[[specification]] +source = "../specification/s3-encryption/key-commitment.md" + [report.html] enabled = true diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index e8ce8e5c..58d807ef 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -50,10 +50,35 @@ std::string make_error(const std::string &message, int status_code) { message + "\"}"; } +bool unsupported(std::string& commitmentPolicy, std::string& encryptionAlgorithm) +{ + if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") return true; + if (commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") return true; + if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") return true; + return false; +} + +std::string get_config(json & request, const char * x) +{ + if (!request.contains("config")) return ""; + auto config = request["config"]; + if (config.contains(x)) + return config[x]; + return ""; +} + MHD_Result handle_create_client(struct MHD_Connection *connection, const std::string &body) { try { json request = json::parse(body); + std::string commitmentPolicy = get_config(request, "commitmentPolicy"); + std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm"); + + if (unsupported(commitmentPolicy, encryptionAlgorithm)) { + send_response(connection, 404, "{\"error\":\"Unsupported Option.\"}"); + return MHD_YES; + } + std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; diff --git a/test-server/cpp-v3-server/.duvet/.gitignore b/test-server/cpp-v3-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/cpp-v3-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/cpp-v3-server/.duvet/config.toml b/test-server/cpp-v3-server/.duvet/config.toml new file mode 100644 index 00000000..d137df36 --- /dev/null +++ b/test-server/cpp-v3-server/.duvet/config.toml @@ -0,0 +1,29 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" + +# Include required specifications here +[[specification]] +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" +[[specification]] +source = "../specification/s3-encryption/encryption.md" +[[specification]] +source = "../specification/s3-encryption/decryption.md" +[[specification]] +source = "../specification/s3-encryption/key-derivation.md" +[[specification]] +source = "../specification/s3-encryption/key-commitment.md" + + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/test-server/cpp-v3-server/CMakeLists.txt b/test-server/cpp-v3-server/CMakeLists.txt new file mode 100644 index 00000000..b282dbc4 --- /dev/null +++ b/test-server/cpp-v3-server/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.16) +project(s3ec-cpp-v2-server) + +set(CMAKE_CXX_STANDARD 17) + +# Configure AWS SDK build options +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") + +# Add AWS SDK as subdirectory +add_subdirectory(aws-sdk-cpp) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) + +find_package(nlohmann_json REQUIRED) + +add_executable(s3ec-server main.cpp) + +target_include_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_INCLUDE_DIRS} + /opt/homebrew/include +) + +target_link_directories(s3ec-server PRIVATE + ${LIBMICROHTTPD_LIBRARY_DIRS} + /opt/homebrew/lib +) + +target_link_libraries(s3ec-server + ${LIBMICROHTTPD_LIBRARIES} + aws-cpp-sdk-core + aws-cpp-sdk-kms + aws-cpp-sdk-s3 + 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 new file mode 100644 index 00000000..86fc285e --- /dev/null +++ b/test-server/cpp-v3-server/Makefile @@ -0,0 +1,35 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8091 + +build/s3ec-server: + brew install libmicrohttpd nlohmann-json ossp-uuid + mkdir -p build && cd build && cmake .. + +start-server: | build/s3ec-server + @echo "Starting Cpp V2 server..." + cd build && make && \ + 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" \ + ./s3ec-server & echo $$! > $(PID_FILE) + @echo "Cpp V2 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/cpp-v3-server/README.md b/test-server/cpp-v3-server/README.md new file mode 100644 index 00000000..8e77feda --- /dev/null +++ b/test-server/cpp-v3-server/README.md @@ -0,0 +1,37 @@ +# C++ S3 Encryption Test Server + +Minimal C++ implementation of the S3 Encryption test server. + +## Dependencies + +- libmicrohttpd +- AWS SDK for C++ +- nlohmann/json +- uuid + +On MacOS you can +```bash +brew install libmicrohttpd nlohmann-json ossp-uuid +``` + +## Build + +```bash +mkdir build && cd build +cmake .. +make +``` + +## Run + +```bash +./s3ec-server +``` + +Server runs on localhost:8085 + +## API Endpoints + +- `POST /client` - Create S3 encryption client +- `GET /object/{bucket}/{key}` - Get encrypted object +- `PUT /object/{bucket}/{key}` - Put encrypted object \ No newline at end of file diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp new file mode 100644 index 00000000..59167078 --- /dev/null +++ b/test-server/cpp-v3-server/main.cpp @@ -0,0 +1,322 @@ +#include +#include +#include +#include +#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; + +std::string generate_uuid() { + uuid_t uuid; + uuid_generate(uuid); + char uuid_str[37]; + uuid_unparse(uuid, uuid_str); + return std::string(uuid_str); +} + +std::string get_header_value(struct MHD_Connection *connection, + const char *key) { + const char *value = + MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); + return value ? std::string(value) : ""; +} + +MHD_Result send_response(struct MHD_Connection *connection, int status_code, + const std::string &content) { + struct MHD_Response *response = MHD_create_response_from_buffer( + content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); + MHD_Result ret = MHD_queue_response(connection, status_code, response); + MHD_destroy_response(response); + return ret; +} + +std::string make_error(const std::string &message, int status_code) { + return "{\"__type\": " + "\"software.amazon.encryption.s3#S3EncryptionClientError\", " + "\"message\": \"" + + message + "\"}"; +} + +MHD_Result unsupported(struct MHD_Connection *connection, std::string & commitmentPolicy, std::string & encryptionAlgorithm) { + fprintf(stderr, "Unsupported %s %s\n",commitmentPolicy.c_str(), encryptionAlgorithm.c_str() ); + send_response(connection, 404, "{\"error\":\"Unsupported Option.\"}"); + return MHD_YES; +} + +std::string get_config(json & request, const char * x) +{ + if (!request.contains("config")) return ""; + auto config = request["config"]; + if (config.contains(x)) + return config[x]; + return ""; +} + +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"]; + bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; + bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; + + auto materials = + std::make_shared(kms_key_id); + CryptoConfigurationV3 config(materials); + if (legacy1 || legacy2) + config.AllowLegacy(); + + 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(); + client_cache[client_id] = encryption_client; + + json response = {{"clientId", client_id}}; + return send_response(connection, 200, response.dump()); + } catch (const std::exception &e) { + fprintf(stderr, "handle_create_client exception %s\n", e.what()); + return send_response(connection, 500, + "{\"error\":\"An exception was thrown.\"}"); + } catch (...) { + return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); + } +} + +void fill_context(Aws::Map &map, + const std::string &metadata) { + if (metadata.empty()) { + return; + } + + // Parse metadata format: [key1]:[value1],[key2]:[value2],... + // or single pair: [key]:[value] + std::string current = metadata; + size_t pos = 0; + + while (pos < current.length()) { + // Find opening bracket for key + size_t key_start = current.find('[', pos); + if (key_start == std::string::npos) + break; + + // Find closing bracket for key + size_t key_end = current.find(']', key_start); + if (key_end == std::string::npos) + break; + + // Find colon separator + size_t colon = current.find(':', key_end); + if (colon == std::string::npos) + break; + + // Find opening bracket for value + size_t value_start = current.find('[', colon); + if (value_start == std::string::npos) + break; + + // Find closing bracket for value + size_t value_end = current.find(']', value_start); + if (value_end == std::string::npos) + break; + + // Extract key and value + std::string key = current.substr(key_start + 1, key_end - key_start - 1); + std::string value = + current.substr(value_start + 1, value_end - value_start - 1); + + // Add to map + map.emplace(key, value); + + // Move to next pair (look for comma or next opening bracket) + pos = value_end + 1; + size_t comma = current.find(',', pos); + if (comma != std::string::npos) { + pos = comma + 1; + } + } +} + +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()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + + try { + Aws::S3::Model::GetObjectRequest request; + 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); + + if (outcome.IsSuccess()) { + auto &stream = outcome.GetResult().GetBody(); + std::string content((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + 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()); + 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); + 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) { + auto it = client_cache.find(client_id); + if (it == client_cache.end()) { + return send_response(connection, 404, "{\"error\":\"Client not found\"}"); + } + + try { + Aws::Map kmsContextMap; + fill_context(kmsContextMap, metadata); + + Aws::S3::Model::PutObjectRequest request; + request.SetBucket(bucket); + request.SetKey(key); + + auto stream = std::make_shared(body); + request.SetBody(stream); + + auto outcome = it->second->PutObject(request, kmsContextMap); + if (outcome.IsSuccess()) { + 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()); + return send_response(connection, 500, msg); + } + } catch (const std::exception &e) { + fprintf(stderr, "handle_put_object exception %s\n", e.what()); + auto msg = make_error(e.what(), 500); + return send_response(connection, 500, msg); + } +} + +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; + } + return MHD_YES; + } + if (is_push && *upload_data_size) { + 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); + + if (is_push && url_str == "/client") { + std::unique_ptr body(static_cast(*con_cls)); + return handle_create_client(connection, *body); + } + + if (url_str.find("/object/") == 0) { + 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"); + 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; + return handle_put_object(connection, bucket, key, client_id, *body, metadata); + } + } + } + + return send_response(connection, 404, + "{\"error\":\"Not idea what is happening\"}"); +} + +int main() { + Aws::SDKOptions options; + Aws::InitAPI(options); + int port = 8091; + + struct MHD_Daemon *daemon = + MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, + &request_handler, NULL, MHD_OPTION_END); + + if (!daemon) { + fprintf(stderr, "Failed to start server on port %d\n", port); + Aws::ShutdownAPI(options); + return 1; + } + + fprintf(stderr, "Server running on port %d\n", port); + sleep(10000); + + MHD_stop_daemon(daemon); + Aws::ShutdownAPI(options); + fprintf(stderr, "Ending server\n"); + return 0; +} 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 a6af0872..4b0c8e74 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 @@ -123,7 +123,7 @@ public class TestUtils { // PYTHON_V3, GO_V4, // NET_V3, - // CPP_V3, + CPP_V3, // PHP_V3, RUBY_V3 ); @@ -138,6 +138,8 @@ public class TestUtils { servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); + servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); + servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); @@ -147,7 +149,6 @@ public class TestUtils { 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(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); 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, "8090")); diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile index a16ff57e..e752b925 100644 --- a/test-server/net-v2-v3-server/Makefile +++ b/test-server/net-v2-v3-server/Makefile @@ -51,7 +51,7 @@ start-net-v3-server: @echo ".NET V3 server starting..." wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) \ + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) duvet: From 4849825fa757acfcfc3a9fbc2e5aad8dfdc4b50d Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 3 Nov 2025 11:13:15 -0800 Subject: [PATCH 122/201] Update to latest Ruby unit tests (#53) * Update to latest Ruby unit tests * update to add security policy and tests * Latest ruby with more tests * All the specs * Latest tests --- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/.duvet/config.toml | 14 ++++++++++---- test-server/ruby-v3-server/lib/client_manager.rb | 7 ++++++- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index 8f550866..f04deb72 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 8f5508662bbb5cdc04be76083244b814b0f2c828 +Subproject commit f04deb7227dca1ad1a193d11f7c57803843a197e diff --git a/test-server/ruby-v3-server/.duvet/config.toml b/test-server/ruby-v3-server/.duvet/config.toml index eaea972c..7a34c0ff 100644 --- a/test-server/ruby-v3-server/.duvet/config.toml +++ b/test-server/ruby-v3-server/.duvet/config.toml @@ -4,19 +4,25 @@ 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/decryption.md" +source = "../specification/s3-encryption/key-commitment.md" [[specification]] source = "../specification/s3-encryption/key-derivation.md" [[specification]] -source = "../specification/s3-encryption/key-commitment.md" +source = "../specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "../specification/s3-encryption/data-format/metadata-strategy.md" [report.html] diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index 7c125458..158b4462 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -37,9 +37,14 @@ def create_client(config) else raise "Unsupported commitment_policy " + config['commitmentPolicy'] end + if config['commitmentPolicy'] == 'FORBID_ENCRYPT_ALLOW_DECRYPT' && config['encryptionAlgorithm'].nil? + hash[:content_encryption_schema] = :aes_gcm_no_padding + end end if !config['enableLegacyWrappingAlgorithms'].nil? || !config['enableLegacyUnauthenticatedModes'].nil? - hash[:legacy_modes] = config['enableLegacyWrappingAlgorithms'] || config['enableLegacyUnauthenticatedModes'] + legacy_modes = config['enableLegacyWrappingAlgorithms'] || config['enableLegacyUnauthenticatedModes'] + # Set security profile based on legacy wrapping algorithms setting + hash[:security_profile] = legacy_modes ? :v3_and_legacy : :v3 end end diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 8f550866..f04deb72 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 8f5508662bbb5cdc04be76083244b814b0f2c828 +Subproject commit f04deb7227dca1ad1a193d11f7c57803843a197e From 9afa66e66ae8dcf18de9d8914e7a9aeb82f4fc11 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:30:18 -0500 Subject: [PATCH 123/201] cpp duvet coverage (#58) --- .github/workflows/main.yml | 7 ++++++ .../.duvet/config.toml | 12 ++++++++- .../cpp-v2-transition-server/compliance.txt | 25 +++++++++++++++++++ test-server/cpp-v3-server/.duvet/config.toml | 12 ++++++++- test-server/cpp-v3-server/compliance.txt | 25 +++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test-server/cpp-v2-transition-server/compliance.txt create mode 100644 test-server/cpp-v3-server/compliance.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac14bcf0..52c3e465 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,6 +18,9 @@ jobs: uses: ./.github/workflows/lint.yml run-tests: + permissions: + id-token: write + contents: read name: Run Tests uses: ./.github/workflows/test.yml with: @@ -25,6 +28,10 @@ jobs: secrets: inherit run-duvet: + permissions: + id-token: write + contents: read + pages: write name: Run Duvet uses: ./.github/workflows/duvet.yml secrets: inherit diff --git a/test-server/cpp-v2-transition-server/.duvet/config.toml b/test-server/cpp-v2-transition-server/.duvet/config.toml index d137df36..cf036140 100644 --- a/test-server/cpp-v2-transition-server/.duvet/config.toml +++ b/test-server/cpp-v2-transition-server/.duvet/config.toml @@ -6,7 +6,17 @@ pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" [[source]] pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" -# Include required specifications here +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.h" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.cpp" + +[[source]] +pattern = "compliance.txt" + +[[specification]] +source = "../specification/s3-encryption/client.md" [[specification]] source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] diff --git a/test-server/cpp-v2-transition-server/compliance.txt b/test-server/cpp-v2-transition-server/compliance.txt new file mode 100644 index 00000000..b6051c5e --- /dev/null +++ b/test-server/cpp-v2-transition-server/compliance.txt @@ -0,0 +1,25 @@ +We're not doing double encoding yet + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata +//= type=exception +//# The S3EC SHOULD support decoding the S3 Server's "double encoding". + + + +Yes, this is how we do prefixes. + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. + + + +We do not support a custom Instruction File suffix under any circumstances. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. diff --git a/test-server/cpp-v3-server/.duvet/config.toml b/test-server/cpp-v3-server/.duvet/config.toml index d137df36..cf036140 100644 --- a/test-server/cpp-v3-server/.duvet/config.toml +++ b/test-server/cpp-v3-server/.duvet/config.toml @@ -6,7 +6,17 @@ pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" [[source]] pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" -# Include required specifications here +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.h" + +[[source]] +pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.cpp" + +[[source]] +pattern = "compliance.txt" + +[[specification]] +source = "../specification/s3-encryption/client.md" [[specification]] source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] diff --git a/test-server/cpp-v3-server/compliance.txt b/test-server/cpp-v3-server/compliance.txt new file mode 100644 index 00000000..b6051c5e --- /dev/null +++ b/test-server/cpp-v3-server/compliance.txt @@ -0,0 +1,25 @@ +We're not doing double encoding yet + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata +//= type=exception +//# The S3EC SHOULD support decoding the S3 Server's "double encoding". + + + +Yes, this is how we do prefixes. + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. + + + +We do not support a custom Instruction File suffix under any circumstances. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. From 9cc58e5511b9da1555297b3eb67bca43da5ebd98 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Wed, 5 Nov 2025 18:48:03 -0500 Subject: [PATCH 124/201] duvet (#59) --- .github/workflows/duvet.yml | 20 +++++++++++++- test-server/cpp-v2-server/.duvet/config.toml | 29 -------------------- test-server/cpp-v2-server/Makefile | 3 -- 3 files changed, 19 insertions(+), 33 deletions(-) delete mode 100644 test-server/cpp-v2-server/.duvet/config.toml diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index e5be7039..1531d9e3 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -14,11 +14,29 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + - name: Checkout CPP code for cpp-v2-transition + uses: actions/checkout@v5 + with: + submodules: recursive + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ + + - name: Checkout CPP code cpp-v3 + uses: actions/checkout@v5 + with: + submodules: recursive + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v3-server/aws-sdk-cpp/ + - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/test-server/cpp-v2-server/.duvet/config.toml b/test-server/cpp-v2-server/.duvet/config.toml deleted file mode 100644 index d137df36..00000000 --- a/test-server/cpp-v2-server/.duvet/config.toml +++ /dev/null @@ -1,29 +0,0 @@ -'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" - -# Include required specifications here -[[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" -[[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" -[[specification]] -source = "../specification/s3-encryption/encryption.md" -[[specification]] -source = "../specification/s3-encryption/decryption.md" -[[specification]] -source = "../specification/s3-encryption/key-derivation.md" -[[specification]] -source = "../specification/s3-encryption/key-commitment.md" - - -[report.html] -enabled = true - -# Enable snapshots to prevent requirement coverage regressions -[report.snapshot] -enabled = false diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index cc562c1a..0f4e4782 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -30,8 +30,5 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) -duvet: - duvet report - view-report-mac: open .duvet/reports/report.html From 56c4107e25dd2a73d68067dad084ed037c62d65b Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:48:20 -0500 Subject: [PATCH 125/201] duvet (#61) --- .github/workflows/duvet.yml | 9 -- test-server/cpp-v2-server/Makefile | 3 - .../.duvet/config.toml | 39 -------- test-server/cpp-v2-transition-server/Makefile | 6 -- .../cpp-v2-transition-server/compliance.txt | 25 ----- test-server/cpp-v3-server/.duvet/config.toml | 6 ++ test-server/cpp-v3-server/compliance.txt | 99 +++++++++++++++++-- 7 files changed, 96 insertions(+), 91 deletions(-) delete mode 100644 test-server/cpp-v2-transition-server/.duvet/config.toml delete mode 100644 test-server/cpp-v2-transition-server/compliance.txt diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index 1531d9e3..529be19d 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -19,15 +19,6 @@ jobs: submodules: true token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - - name: Checkout CPP code for cpp-v2-transition - uses: actions/checkout@v5 - with: - submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev - path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ - - name: Checkout CPP code cpp-v3 uses: actions/checkout@v5 with: diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 0f4e4782..9e0f04b1 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -29,6 +29,3 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/cpp-v2-transition-server/.duvet/config.toml b/test-server/cpp-v2-transition-server/.duvet/config.toml deleted file mode 100644 index cf036140..00000000 --- a/test-server/cpp-v2-transition-server/.duvet/config.toml +++ /dev/null @@ -1,39 +0,0 @@ -'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.cpp" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-s3-encryption/**/*.h" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.h" - -[[source]] -pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.cpp" - -[[source]] -pattern = "compliance.txt" - -[[specification]] -source = "../specification/s3-encryption/client.md" -[[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" -[[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" -[[specification]] -source = "../specification/s3-encryption/encryption.md" -[[specification]] -source = "../specification/s3-encryption/decryption.md" -[[specification]] -source = "../specification/s3-encryption/key-derivation.md" -[[specification]] -source = "../specification/s3-encryption/key-commitment.md" - - -[report.html] -enabled = true - -# Enable snapshots to prevent requirement coverage regressions -[report.snapshot] -enabled = false diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 0a63b2ed..05803c78 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -27,9 +27,3 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) - -duvet: - duvet report - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/cpp-v2-transition-server/compliance.txt b/test-server/cpp-v2-transition-server/compliance.txt deleted file mode 100644 index b6051c5e..00000000 --- a/test-server/cpp-v2-transition-server/compliance.txt +++ /dev/null @@ -1,25 +0,0 @@ -We're not doing double encoding yet - -//= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata -//= type=exception -//# The S3EC SHOULD support decoding the S3 Server's "double encoding". - - - -Yes, this is how we do prefixes. - -//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys -//= type=exception -//# The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. - -//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys -//= type=exception -//# The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. - - - -We do not support a custom Instruction File suffix under any circumstances. - -//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file -//= type=exception -//# The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. diff --git a/test-server/cpp-v3-server/.duvet/config.toml b/test-server/cpp-v3-server/.duvet/config.toml index cf036140..3a49ac85 100644 --- a/test-server/cpp-v3-server/.duvet/config.toml +++ b/test-server/cpp-v3-server/.duvet/config.toml @@ -12,6 +12,12 @@ pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.h" [[source]] pattern = "aws-sdk-cpp/src/aws-cpp-sdk-core/include/aws/core/utils/crypto/*.cpp" +[[source]] +pattern = "aws-sdk-cpp/tests/aws-cpp-sdk-s3-encryption-tests/*.cpp" + +[[source]] +pattern = "aws-sdk-cpp/tests/aws-cpp-sdk-s3-encryption-integration-tests/*.cpp" + [[source]] pattern = "compliance.txt" diff --git a/test-server/cpp-v3-server/compliance.txt b/test-server/cpp-v3-server/compliance.txt index b6051c5e..253f164f 100644 --- a/test-server/cpp-v3-server/compliance.txt +++ b/test-server/cpp-v3-server/compliance.txt @@ -1,25 +1,106 @@ -We're not doing double encoding yet - +** We're not doing double encoding yet //= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata //= type=exception //# The S3EC SHOULD support decoding the S3 Server's "double encoding". +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# This material description string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# This encryption context string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. -Yes, this is how we do prefixes. -//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +** The C++ S3EC does not support key rings nor cmms +//= ../specification/s3-encryption/client.md#cryptographic-materials //= type=exception -//# The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code. +//# The S3EC MUST accept either one CMM or one Keyring instance upon initialization. +//# If both a CMM and a Keyring are provided, the S3EC MUST throw an exception. +//# When a Keyring is provided, the S3EC MUST create an instance of the DefaultCMM using the provided Keyring. + + +** The C++ S3EC does not support Delayed Authentication buffer size configuration +//= ../specification/s3-encryption/client.md#set-buffer-size +//= type=exception +//# The S3EC SHOULD accept a configurable buffer size which refers to the maximum ciphertext length in bytes to store in memory when Delayed Authentication mode is disabled. +//# If Delayed Authentication mode is enabled, and the buffer size has been set to a value other than its default, the S3EC MUST throw an exception. +//# If Delayed Authentication mode is disabled, and no buffer size is provided, the S3EC MUST set the buffer size to a reasonable default. + +** In the C++ S3EC, there is no connection between the S3 client and any potential KMS clients +//= ../specification/s3-encryption/client.md#inherited-sdk-configuration +//= type=exception +//# If the S3EC accepts SDK client configuration, the configuration MUST be applied to all wrapped SDK clients including the KMS client. + + +** In the C++ S3EC, the encryption algorithm is uniquely determined by the client version and the CommitmentPolicy + +//= ../specification/s3-encryption/client.md#encryption-algorithm +//= type=exception +//# The S3EC MUST support configuration of the encryption algorithm (or algorithm suite) during its initialization. +//# The S3EC MUST validate that the configured encryption algorithm is not legacy. +//# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception. + +//= ../specification/s3-encryption/client.md#key-commitment +//= type=exception +//# The S3EC MUST validate the configured Encryption Algorithm against the provided key commitment policy. +//# If the configured Encryption Algorithm is incompatible with the key commitment policy, then it MUST throw an exception. + + +** The C++ S3EC does not accept a source of randomness during client initialization +//= ../specification/s3-encryption/client.md#randomness +//= type=exception +//# The S3EC MAY accept a source of randomness during client initialization. + + +** This is silly, and I don't want to do it +//= ../specification/s3-encryption/encryption.md#cipher-initialization +//= type=exception +//# The client SHOULD validate that the generated IV or Message ID is not zeros. + +** The C++ S3EC does not support custom materials. +** The built in Raw Keyring always has an empty Materials Description +** Therefore "x-amz-m" will never be written. //= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //= type=exception -//# The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys. +//# - The mapkey "x-amz-m" SHOULD be present for V3 format objects that use Raw Keyring Material Description. +** The C++ S3EC only implements GetObject and PutObject ** -We do not support a custom Instruction File suffix under any circumstances. +//= ../specification/s3-encryption/client.md#aws-sdk-compatibility +//= type=exception +//# The S3EC MUST adhere to the same interface for API operations as the conventional AWS SDK S3 client. + +//= ../specification/s3-encryption/client.md#aws-sdk-compatibility +//= type=exception +//# The S3EC SHOULD support invoking operations unrelated to client-side encryption e.g. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObject MUST be implemented by the S3EC. +//# - DeleteObject MUST delete the given object key. +//# - DeleteObject MUST delete the associated instruction file using the default instruction file suffix. +//# - DeleteObjects MUST be implemented by the S3EC. +//# - DeleteObjects MUST delete each of the given objects. +//# - DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - CreateMultipartUpload MAY be implemented by the S3EC. +//# - If implemented, CreateMultipartUpload MUST initiate a multipart upload. +//# - UploadPart MAY be implemented by the S3EC. +//# - UploadPart MUST encrypt each part. +//# - Each part MUST be encrypted in sequence. +//# - Each part MUST be encrypted using the same cipher instance for each part. +//# - CompleteMultipartUpload MAY be implemented by the S3EC. +//# - CompleteMultipartUpload MUST complete the multipart upload. +//# - AbortMultipartUpload MAY be implemented by the S3EC. +//# - AbortMultipartUpload MUST abort the multipart upload. -//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= ../specification/s3-encryption/client.md#optional-api-operations //= type=exception -//# The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. +//# - ReEncryptInstructionFile MAY be implemented by the S3EC. +//# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. +//# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. From 176e745ef1d79ea15735bae62d880348fdbc06ac Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 6 Nov 2025 12:54:31 -0800 Subject: [PATCH 126/201] chore: Kill Ruby (#62) --- .../software/amazon/encryption/s3/TestUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 4b0c8e74..8675061a 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 @@ -112,9 +112,9 @@ public class TestUtils { // JAVA_V3_TRANSITION, // GO_V3_TRANSITION, // NET_V2_TRANSITION, - CPP_V2_TRANSITION, + CPP_V2_TRANSITION // PHP_V2_TRANSITION, - RUBY_V2_TRANSITION + // RUBY_V2_TRANSITION ); public static final Set IMPROVED_VERSIONS = @@ -123,9 +123,9 @@ public class TestUtils { // PYTHON_V3, GO_V4, // NET_V3, - CPP_V3, + CPP_V3 // PHP_V3, - RUBY_V3 + // RUBY_V3 ); private static final Map serverMap; @@ -143,13 +143,13 @@ public class TestUtils { // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); - servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + // servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers 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(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + // 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, "8090")); serverMap = filterServers(servers); From be6dd9a54316933308c4b25150295765ddeac81f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:12:21 -0800 Subject: [PATCH 127/201] chore: add instruction file support to TestServer (#56) --- .github/workflows/lint.yml | 2 +- test-server/cpp-v2-server/main.cpp | 7 + test-server/cpp-v2-transition-server/main.cpp | 7 + test-server/cpp-v3-server/main.cpp | 7 + test-server/java-tests/build.gradle.kts | 1 + .../amazon/encryption/s3/RoundTripTests.java | 134 +++++++++++++++++- .../amazon/encryption/s3/TestUtils.java | 50 ++++--- test-server/java-v3-server/build.gradle.kts | 3 +- .../s3/CreateClientOperationImpl.java | 12 ++ .../s3/CreateClientOperationImpl.java | 11 ++ .../s3/CreateClientOperationImpl.java | 11 ++ test-server/model/client.smithy | 12 ++ test-server/model/object.smithy | 5 + test-server/php-v2-server/src/client.php | 6 + .../php-v2-transition-server/src/client.php | 5 + test-server/php-v3-server/src/client.php | 7 + .../ruby-v2-server/lib/client_manager.rb | 2 + .../ruby-v3-server/lib/client_manager.rb | 16 ++- 18 files changed, 274 insertions(+), 24 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 637003f3..bb1655bb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: jobs: lint: - runs-on: macos-13 + runs-on: macos-15 steps: - name: Checkout code diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp index c4f2c240..a2b05810 100644 --- a/test-server/cpp-v2-server/main.cpp +++ b/test-server/cpp-v2-server/main.cpp @@ -57,12 +57,19 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; + bool inst_put = false; + if (request["config"].contains("instructionFileConfig") && + request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) { + inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; + } 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); auto encryption_client = std::make_shared(config); diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 58d807ef..1fcedc3c 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -82,12 +82,19 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; + bool inst_put = false; + if (request["config"].contains("instructionFileConfig") && + request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) { + inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; + } 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); auto encryption_client = std::make_shared(config); diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 59167078..1f74974c 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -72,12 +72,19 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, std::string kms_key_id = request["config"]["keyMaterial"]["kmsKeyId"]; bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; + bool inst_put = false; + if (request["config"].contains("instructionFileConfig") && + request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) { + inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; + } auto materials = std::make_shared(kms_key_id); CryptoConfigurationV3 config(materials); 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"); diff --git a/test-server/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index bc37514f..106f82ef 100644 --- a/test-server/java-tests/build.gradle.kts +++ b/test-server/java-tests/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.13.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("com.amazonaws:aws-java-sdk:1.12.788") + testImplementation("software.amazon.awssdk:s3:2.37.1") testImplementation("org.bouncycastle:bcprov-jdk15on:1.70") } 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 e8dc4bae..a63e1518 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 @@ -6,6 +6,7 @@ package software.amazon.encryption.s3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static software.amazon.encryption.s3.TestUtils.*; @@ -15,19 +16,26 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Stream; +import com.amazonaws.services.s3.AmazonS3EncryptionClientV2; +import com.amazonaws.services.s3.AmazonS3EncryptionV2; +import com.amazonaws.services.s3.model.CryptoConfigurationV2; 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.opentest4j.TestAbortedException; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; 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.InstructionFileConfig; import software.amazon.encryption.s3.model.KeyMaterial; import software.amazon.encryption.s3.model.PutObjectInput; import software.amazon.encryption.s3.model.S3ECConfig; @@ -38,7 +46,6 @@ 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; @@ -416,4 +423,125 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la } } } + + @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") + @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") + public void instructionFileReadV2Format(TestUtils.LanguageServerTarget language) { + if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(language.getLanguageName())) { + throw new TestAbortedException(String.format("%s does not support KMS instruction files", language.getLanguageName())); + } + if (INSTRUCTION_FILE_GET_UNSUPPORTED.contains(language.getLanguageName())) { + throw new TestAbortedException(String.format("%s does not support instruction file Gets", language.getLanguageName())); + } + S3ECTestServerClient client = testServerClientFor(language); + final String objectKey = appendTestSuffix("read-instruction-file-v2-" + language); + final String input = "simple-test-input"; + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(KMS_KEY_ARN) + .build(); + CreateClientOutput output1 = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .enableLegacyWrappingAlgorithms(true) + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String s3ECId = output1.getClientId(); + + // Write with instruction file using V2 client + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ARN); + CryptoConfigurationV2 cryptoConfigurationV2 = new CryptoConfigurationV2(); + cryptoConfigurationV2.setStorageMode(CryptoStorageMode.InstructionFile); + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .withCryptoConfiguration(cryptoConfigurationV2) + .build(); + v2Client.putObject(BUCKET, objectKey, input); + + // Read should be enabled by default + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(s3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + + assertEquals(input, new String(output.getBody().array())); + } + + @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") + public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageServerTarget decLang) { + if (INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(encLang.getLanguageName())) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } + if (INSTRUCTION_FILE_GET_UNSUPPORTED.contains(decLang.getLanguageName())) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } + if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(encLang.getLanguageName())) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } + if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(decLang.getLanguageName())) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } + if (INSTRUCTION_FILE_ROUNDTRIP_TEMP_UNSUPPORTED.contains(encLang.getLanguageName())) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } + S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); + final String objectKey = appendTestSuffix(String.format("write-%s-read-%s-instruction-file", encLang.getLanguageName(), decLang.getLanguageName())); + final String input = "simple-test-input"; + KeyMaterial kmsKeyArn = KeyMaterial.builder() + .kmsKeyId(KMS_KEY_ARN) + .build(); + CreateClientOutput encOutput = encClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); + String encS3ECId = encOutput.getClientId(); + CreateClientOutput decOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .build()) + .build()); + String decS3ECId = decOutput.getClientId(); + + // Write with instruction file + encClient.putObject(PutObjectInput.builder() + .clientID(encS3ECId) + .bucket(BUCKET) + .key(objectKey) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); + + // Assert using Java plaintext client that an instruction file exists + ResponseBytes ptInstFile; + try (S3Client ptS3Client = S3Client.create()) { + ptInstFile = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey + ".instruction") + .build()); + } + // Check for inst file key + if (!encLang.getLanguageName().contains("Ruby")) { + // Ruby doesn't include it :( + 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() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + + assertEquals(input, new String(output.getBody().array())); + } } 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 8675061a..1cb6c3ce 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 @@ -22,32 +22,29 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.ObjectMetadata; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.junit.jupiter.params.provider.Arguments; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; -import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol; -import software.amazon.smithy.java.client.core.ClientConfig; -import software.amazon.smithy.java.client.core.ClientProtocol; -import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; -import software.amazon.encryption.s3.client.S3ECTestServerClient; 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.PutObjectInput; import software.amazon.encryption.s3.model.PutObjectOutput; -import software.amazon.encryption.s3.model.S3ECConfig; -import software.amazon.encryption.s3.model.S3ECTestServerApiService; import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol; +import software.amazon.smithy.java.client.core.ClientConfig; +import software.amazon.smithy.java.client.core.ClientProtocol; +import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; +import software.amazon.encryption.s3.client.S3ECTestServerClient; +import software.amazon.encryption.s3.model.S3ECTestServerApiService; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.GetObjectMetadataRequest; - public class TestUtils { // Version name constants @@ -97,6 +94,25 @@ public class TestUtils { public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2_CURRENT, NET_V3); + // .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); + + // Go does not write with instruction files + public static final Set INSTRUCTION_FILE_PUT_UNSUPPORTED = + Set.of(GO_V3_CURRENT, GO_V3_TRANSITION, GO_V4, PYTHON_V3 + // Apparently C++ V2 Current does not work, even though it should + , CPP_V2_CURRENT); + + // Not implemented yet in Python. + public static final Set INSTRUCTION_FILE_GET_UNSUPPORTED = + Set.of(PYTHON_V3); + + // PHP doesn't work but it should, temporarily disable + public static final Set INSTRUCTION_FILE_ROUNDTRIP_TEMP_UNSUPPORTED = + Set.of(PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3); + public static final Set CURRENT_VERSIONS = Set.of( JAVA_V3_CURRENT, @@ -286,7 +302,7 @@ public static List metadataMapToList(Map md) { public static void validateServersRunning() { for (LanguageServerTarget server : serverMap.values()) { if (!serverListening(server.getServerURI())) { - throw new RuntimeException(String.format("Test Server for %s is not running at endpoint: %s", + throw new RuntimeException(String.format("Test Server for %s is not running at endpoint: %s", server.getLanguageName(), server.getServerURI())); } } @@ -430,7 +446,7 @@ public static EncryptionAlgorithm GetEncryptionAlgorithm(String objectKey) return EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; } } - + throw new RuntimeException("Need to support instruction files!"); } @@ -456,7 +472,7 @@ public static void Encrypt( crossLanguageObjects.add(objectKey); } - + public static void Decrypt( S3ECTestServerClient client, String S3ECId, List crossLanguageObjects, @@ -468,7 +484,7 @@ public static void Decrypt( .bucket(TestUtils.BUCKET) .key(objectKey) .build()); - + // Then: Pass assertEquals(objectKey, new String(output.getBody().array())); assertEquals( @@ -478,7 +494,7 @@ public static void Decrypt( ); } } - + public static void Decrypt_fails( S3ECTestServerClient client, String S3ECId, List crossLanguageObjects, diff --git a/test-server/java-v3-server/build.gradle.kts b/test-server/java-v3-server/build.gradle.kts index ca793e56..baae4947 100644 --- a/test-server/java-v3-server/build.gradle.kts +++ b/test-server/java-v3-server/build.gradle.kts @@ -14,7 +14,8 @@ dependencies { implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") compileOnly("software.amazon.awssdk:aws-sdk-java:2.31.66") - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.3.5") + // This MUST stay at 3.5.0 + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.5.0") } // Use that application plugin to start the service via the `run` task. 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 d992c435..1415fd01 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 @@ -3,6 +3,7 @@ import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.materials.AesKeyring; import software.amazon.encryption.s3.materials.Keyring; import software.amazon.encryption.s3.materials.KmsKeyring; @@ -54,6 +55,7 @@ private boolean onlyOneNonNull(Object... values) { @Override public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { try { + // Key Material / Keyring Creation KeyMaterial key = input.getConfig().getKeyMaterial(); if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { throw new RuntimeException("KeyMaterial must be only one, non-null input!"); @@ -88,7 +90,17 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c } else { throw new RuntimeException("No KeyMaterial found!"); } + + // Client Creation + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + } S3Client s3Client = S3EncryptionClient.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()) .keyring(keyring) .build(); UUID uuid = UUID.randomUUID(); 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 d992c435..8622f1ac 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 @@ -2,6 +2,7 @@ import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.S3EncryptionClient; import software.amazon.encryption.s3.materials.AesKeyring; import software.amazon.encryption.s3.materials.Keyring; @@ -88,7 +89,17 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c } else { throw new RuntimeException("No KeyMaterial found!"); } + + // Client Creation + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + } S3Client s3Client = S3EncryptionClient.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()) .keyring(keyring) .build(); UUID uuid = UUID.randomUUID(); 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 d992c435..8622f1ac 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 @@ -2,6 +2,7 @@ import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.S3EncryptionClient; import software.amazon.encryption.s3.materials.AesKeyring; import software.amazon.encryption.s3.materials.Keyring; @@ -88,7 +89,17 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c } else { throw new RuntimeException("No KeyMaterial found!"); } + + // Client Creation + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + } S3Client s3Client = S3EncryptionClient.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()) .keyring(keyring) .build(); UUID uuid = UUID.randomUUID(); diff --git a/test-server/model/client.smithy b/test-server/model/client.smithy index 5514672f..bba19b62 100644 --- a/test-server/model/client.smithy +++ b/test-server/model/client.smithy @@ -40,6 +40,17 @@ enum EncryptionAlgorithm { ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY } +structure InstructionFileConfig { + /// This allows specifying a (non-encrypted) client for languages which + /// support this for instruction files. + /// In general, languages should not require specifying it, + /// so it is best to leave it null until there's a good reason not to. + /// This also requires a way to create non-encrypted clients which we don't have yet. + clientId: String, + enableInstructionFilePutObject: Boolean = false, + disableInstructionFile: Boolean = false +} + structure S3ECConfig { enableLegacyUnauthenticatedModes: Boolean = false, enableDelayedAuthenticationMode: Boolean = false, @@ -48,4 +59,5 @@ structure S3ECConfig { keyMaterial: KeyMaterial, commitmentPolicy: CommitmentPolicy, encryptionAlgorithm: EncryptionAlgorithm, + instructionFileConfig: InstructionFileConfig, } diff --git a/test-server/model/object.smithy b/test-server/model/object.smithy index a4b12d5a..6d793353 100644 --- a/test-server/model/object.smithy +++ b/test-server/model/object.smithy @@ -113,6 +113,11 @@ operation ReEncrypt { @required @notProperty clientID: String + + /// Custom instruction file suffix + @httpHeader("InstructionFileSuffix") + @notProperty + instructionFileSuffix: String } output := for Object { diff --git a/test-server/php-v2-server/src/client.php b/test-server/php-v2-server/src/client.php index 44fe1b39..9c39d540 100644 --- a/test-server/php-v2-server/src/client.php +++ b/test-server/php-v2-server/src/client.php @@ -19,6 +19,11 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $instFileConfig = $configData['instructionFileConfig'] ?? null; + $instFilePut = false; + if ($instFileConfig != null) { + $instFilePut = $instFileConfig['enableInstructionFilePutObject'] ?? false; + } if ($configData == []) { return GenericServerError("Invalid config in request body", 400); @@ -55,6 +60,7 @@ function handleCreateClient() ], 'kmsKeyId' => $kmsKeyId, 'legacy' => $legacyAlgorithms, + 'instFilePut' => $instFilePut, 'created' => time() ]; diff --git a/test-server/php-v2-transition-server/src/client.php b/test-server/php-v2-transition-server/src/client.php index 44fe1b39..beda557a 100644 --- a/test-server/php-v2-transition-server/src/client.php +++ b/test-server/php-v2-transition-server/src/client.php @@ -19,6 +19,10 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $instFilePut = false; + if ($instFileConfig != null) { + $instFilePut = $instFileConfig['enableInstructionFilePutObject'] ?? false; + } if ($configData == []) { return GenericServerError("Invalid config in request body", 400); @@ -55,6 +59,7 @@ function handleCreateClient() ], 'kmsKeyId' => $kmsKeyId, 'legacy' => $legacyAlgorithms, + 'instFilePut' => $instFilePut, 'created' => time() ]; diff --git a/test-server/php-v3-server/src/client.php b/test-server/php-v3-server/src/client.php index 6c40f590..2c6204bb 100644 --- a/test-server/php-v3-server/src/client.php +++ b/test-server/php-v3-server/src/client.php @@ -19,6 +19,12 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $instFileConfig = $configData['instructionFileConfig'] ?? null; + $instFilePut = false; + if ($instFileConfig != null) { + $instFilePut = $instFileConfig['enableInstructionFilePutObject'] ?? false; + } + if (empty($configData)) { return GenericServerError("Invalid config in request body", 400); @@ -55,6 +61,7 @@ function handleCreateClient() ], 'kmsKeyId' => $kmsKeyId, 'legacy' => $legacyAlgorithms, + 'instFilePut' => $instFilePut, 'created' => time() ]; diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb index c6a25610..717003bf 100644 --- a/test-server/ruby-v2-server/lib/client_manager.rb +++ b/test-server/ruby-v2-server/lib/client_manager.rb @@ -16,6 +16,7 @@ def initialize def create_client(config) # Extract configuration kms_key_id = config.dig('keyMaterial', 'kmsKeyId') + inst_file_put = config.dig('instructionFileConfig', 'enableInstructionFilePutObject') raise 'KMS Key ID is required' if kms_key_id.nil? || kms_key_id.empty? @@ -25,6 +26,7 @@ def create_client(config) 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| if !config['enableLegacyWrappingAlgorithms'].nil? || !config['enableLegacyUnauthenticatedModes'].nil? legacy_modes = config['enableLegacyWrappingAlgorithms'] || config['enableLegacyUnauthenticatedModes'] diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index 158b4462..a6fb551f 100644 --- a/test-server/ruby-v3-server/lib/client_manager.rb +++ b/test-server/ruby-v3-server/lib/client_manager.rb @@ -16,7 +16,18 @@ def initialize def create_client(config) # Extract configuration kms_key_id = config.dig('keyMaterial', 'kmsKeyId') - + inst_file_put = config.dig('instructionFileConfig', 'enableInstructionFilePutObject') + content_alg = config.dig('encryptionAlgorithm') + + # 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 + elsif content_alg == 'ALG_AES_256_GCM_IV12_TAG16_NO_KDF' + content_alg = :aes_gcm_no_padding + else + 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 @@ -24,7 +35,8 @@ def create_client(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, + content_encryption_schema: content_alg }.tap do |hash| if !config['commitmentPolicy'].nil? hash[:commitment_policy] = case config['commitmentPolicy'] From b3e8f931f6ea05e996356c9bcc100a947356b1d3 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 7 Nov 2025 11:37:19 -0800 Subject: [PATCH 128/201] chore: add .net transition servers (#48) --- .github/workflows/test.yml | 9 ++ .gitmodules | 4 + test-server/Makefile | 2 +- .../amazon/encryption/s3/RoundTripTests.java | 2 +- .../amazon/encryption/s3/TestUtils.java | 17 ++- .../.duvet/.gitignore | 3 + .../.duvet/config.toml | 27 +++++ .../net-v3-transition-server/.gitignore | 44 ++++++++ .../Controllers/ClientController.cs | 101 +++++++++++++++++ .../Controllers/ObjectController.cs | 105 ++++++++++++++++++ test-server/net-v3-transition-server/Makefile | 34 ++++++ .../Models/ClientRequest.cs | 48 ++++++++ .../Models/ClientResponse.cs | 8 ++ .../Models/ErrorModels.cs | 17 +++ .../NetV3TransitionServer.csproj | 27 +++++ .../net-v3-transition-server/Program.cs | 17 +++ .../net-v3-transition-server/README.md | 66 +++++++++++ .../Services/ClientCacheService.cs | 28 +++++ .../s3ec-v3-transition-branch | 1 + 19 files changed, 552 insertions(+), 8 deletions(-) create mode 100644 test-server/net-v3-transition-server/.duvet/.gitignore create mode 100644 test-server/net-v3-transition-server/.duvet/config.toml create mode 100644 test-server/net-v3-transition-server/.gitignore create mode 100644 test-server/net-v3-transition-server/Controllers/ClientController.cs create mode 100644 test-server/net-v3-transition-server/Controllers/ObjectController.cs create mode 100644 test-server/net-v3-transition-server/Makefile create mode 100644 test-server/net-v3-transition-server/Models/ClientRequest.cs create mode 100644 test-server/net-v3-transition-server/Models/ClientResponse.cs create mode 100644 test-server/net-v3-transition-server/Models/ErrorModels.cs create mode 100644 test-server/net-v3-transition-server/NetV3TransitionServer.csproj create mode 100644 test-server/net-v3-transition-server/Program.cs create mode 100644 test-server/net-v3-transition-server/README.md create mode 100644 test-server/net-v3-transition-server/Services/ClientCacheService.cs create mode 160000 test-server/net-v3-transition-server/s3ec-v3-transition-branch diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72045f16..c29cfb5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,15 @@ jobs: ref: s3ec-v3 path: test-server/net-v2-v3-server/s3ec-net-v3 + - name: Checkout .NET V3 code (Transition) + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + # This is the branch for S3EC .NET V3 transition + ref: rishav/key-commitment + path: test-server/net-v3-transition-server/s3ec-v3-transition-branch + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.gitmodules b/.gitmodules index 0bf186eb..952fb220 100644 --- a/.gitmodules +++ b/.gitmodules @@ -39,3 +39,7 @@ path = test-server/net-v2-v3-server/s3ec-net-v3 url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git branch = s3ec-v3 +[submodule "test-server/net-v3-transition-server/s3ec-v3-transition-branch"] + path = test-server/net-v3-transition-server/s3ec-v3-transition-branch + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = rishav/key-commitment diff --git a/test-server/Makefile b/test-server/Makefile index be8df10b..dc3bdad3 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -6,7 +6,7 @@ all: start-all-servers wait-all-servers run-tests # CI target for GitHub Actions -ci: start-all-servers wait-all-servers run-tests stop-servers +ci: start-servers run-tests stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) 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 a63e1518..0cee88ee 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 @@ -409,7 +409,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la .build()); fail("Expected Exception"); } catch (S3EncryptionClientError e) { - if (language.getLanguageName().equals(NET_V3) || language.getLanguageName().equals(NET_V2_CURRENT) + if (language.getLanguageName().equals(NET_V3_CURRENT) || language.getLanguageName().equals(NET_V2_CURRENT) || language.getLanguageName().equals(NET_V3_TRANSITION) || 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 1cb6c3ce..d21d0124 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 @@ -65,8 +65,10 @@ public class TestUtils { public static final String GO_V4 = "Go-V4"; public static final String NET_V2_CURRENT = "NET-V2-Current"; + public static final String NET_V3_CURRENT = "NET-V3-Current"; public static final String NET_V2_TRANSITION = "NET-V2-Transition"; - public static final String NET_V3 = "NET-V3"; + public static final String NET_V3_TRANSITION = "NET-V3-Transition"; + public static final String NET_V4 = "NET-V4"; public static final String CPP_V2_CURRENT = "CPP-V2-Current"; public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; @@ -89,15 +91,15 @@ 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); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3, NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = - Set.of(NET_V2_CURRENT, NET_V3); + Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION); // .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); + Set.of(NET_V2_CURRENT, NET_V2_TRANSITION, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); // Go does not write with instruction files public static final Set INSTRUCTION_FILE_PUT_UNSUPPORTED = @@ -118,6 +120,7 @@ public class TestUtils { JAVA_V3_CURRENT, GO_V3_CURRENT, NET_V2_CURRENT, + NET_V3_CURRENT, CPP_V2_CURRENT, RUBY_V2_CURRENT, PHP_V2_CURRENT @@ -128,6 +131,7 @@ public class TestUtils { // JAVA_V3_TRANSITION, // GO_V3_TRANSITION, // NET_V2_TRANSITION, + NET_V3_TRANSITION, CPP_V2_TRANSITION // PHP_V2_TRANSITION, // RUBY_V2_TRANSITION @@ -138,7 +142,7 @@ public class TestUtils { // JAVA_V4, // PYTHON_V3, GO_V4, - // NET_V3, + // NET_V4, CPP_V3 // PHP_V3, // RUBY_V3 @@ -152,7 +156,7 @@ public class TestUtils { servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); - servers.put(NET_V3, new LanguageServerTarget(NET_V3, "8084")); + servers.put(NET_V3_CURRENT, new LanguageServerTarget(NET_V3_CURRENT, "8084")); servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); @@ -168,6 +172,7 @@ public class TestUtils { // 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, "8090")); + servers.put(NET_V3_TRANSITION, new LanguageServerTarget(NET_V3_TRANSITION, "8100")); serverMap = filterServers(servers); } diff --git a/test-server/net-v3-transition-server/.duvet/.gitignore b/test-server/net-v3-transition-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/net-v3-transition-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/net-v3-transition-server/.duvet/config.toml b/test-server/net-v3-transition-server/.duvet/config.toml new file mode 100644 index 00000000..416dcfb9 --- /dev/null +++ b/test-server/net-v3-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-v3-transition-server/.gitignore b/test-server/net-v3-transition-server/.gitignore new file mode 100644 index 00000000..4c20cbc8 --- /dev/null +++ b/test-server/net-v3-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-v3-transition-server/Controllers/ClientController.cs b/test-server/net-v3-transition-server/Controllers/ClientController.cs new file mode 100644 index 00000000..b2fc2d4e --- /dev/null +++ b/test-server/net-v3-transition-server/Controllers/ClientController.cs @@ -0,0 +1,101 @@ +using System.Text.Json; +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Microsoft.AspNetCore.Mvc; +using NetV3TransitionServer.Models; +using NetV3TransitionServer.Services; + +namespace NetV3TransitionServer.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" }); + if (request.Config.KeyMaterial.RsaKey != null) + return StatusCode(501, new GenericServerError { Message = "RsaKey not supported" }); + if (request.Config.KeyMaterial.AesKey != null) + return StatusCode(501, new GenericServerError { Message = "AesKey not supported" }); + + try + { + var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; + var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; + var commitmentPolicy = MapCommitmentPolicy(request.Config.CommitmentPolicy); + + // 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 encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); + logger.LogInformation( + "[NET-V3-Transitional] Created EncryptionMaterialsV2: KMS={KmsKeyId}", + kmsKeyId); + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot + var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; + var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; + + logger.LogInformation("[NET-V3-Transitional] Created securityProfile= {securityProfile}", securityProfile.ToString()); + + // Currently, tests does not send EncryptionAlgorithm. Tests only validates EncryptionAlgorithm from metadata of the response. + // var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); + var encryptionAlgorithm = commitmentPolicy == Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt ? ContentEncryptionAlgorithm.AesGcm : ContentEncryptionAlgorithm.AesGcmWithCommitment; + logger.LogInformation("[NET-V3-Transitional] Created commitmentPolicy= {commitmentPolicy}", commitmentPolicy); + logger.LogInformation("[NET-V3-Transitional] Created encryptionAlgorithm= {encryptionAlgorithm}", encryptionAlgorithm); + + var configuration = new AmazonS3CryptoConfigurationV2(securityProfile, commitmentPolicy, encryptionAlgorithm); + // 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-V3-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-V3-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-v3-transition-server/Controllers/ObjectController.cs b/test-server/net-v3-transition-server/Controllers/ObjectController.cs new file mode 100644 index 00000000..76548815 --- /dev/null +++ b/test-server/net-v3-transition-server/Controllers/ObjectController.cs @@ -0,0 +1,105 @@ +using System.Text.Json; +using Amazon.S3.Model; +using Microsoft.AspNetCore.Mvc; +using NetV3TransitionServer.Models; +using NetV3TransitionServer.Services; + +namespace NetV3TransitionServer.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-V3-Transitional] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V3-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-V3-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-V3-Transitional] Failed to put object from S3 for bucket={bucket}, key={key}", bucket, key); + return StatusCode(500, new S3EncryptionClientError { Message = $"[NET-V3-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-V3-Transitional] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V3-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-V3-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-V3-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-v3-transition-server/Makefile b/test-server/net-v3-transition-server/Makefile new file mode 100644 index 00000000..ca863f28 --- /dev/null +++ b/test-server/net-v3-transition-server/Makefile @@ -0,0 +1,34 @@ +# Makefile for S3 Encryption Client .NET Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE_NET_V3_TRANSITION := net-v3-transition-server.pid +PORT_NET_V3_TRANSITION := 8100 + +start-server: + $(MAKE) start-net-v3-transition-server + +stop-server: + @if [ -f $(PID_FILE_NET_V3_TRANSITION) ]; then \ + kill $$(cat $(PID_FILE_NET_V3_TRANSITION)) 2>/dev/null || true; \ + rm $(PID_FILE_NET_V3_TRANSITION); \ + fi + +# Start .NET V3 transition server in background +start-net-v3-transition-server: + @echo "Starting .NET V3 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 & echo $! > net-v3-transition-server.pid + @echo ".NET V3 server starting..." + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3_TRANSITION) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/net-v3-transition-server/Models/ClientRequest.cs b/test-server/net-v3-transition-server/Models/ClientRequest.cs new file mode 100644 index 00000000..05b9a37e --- /dev/null +++ b/test-server/net-v3-transition-server/Models/ClientRequest.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace NetV3TransitionServer.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 class KeyMaterial +{ + public byte[]? RsaKey { get; set; } + public byte[]? AesKey { get; set; } + + [Required] + public string KmsKeyId { get; set; } = string.Empty; +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CommitmentPolicy +{ + REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + REQUIRE_ENCRYPT_ALLOW_DECRYPT, + FORBID_ENCRYPT_ALLOW_DECRYPT +} + +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 +} \ No newline at end of file diff --git a/test-server/net-v3-transition-server/Models/ClientResponse.cs b/test-server/net-v3-transition-server/Models/ClientResponse.cs new file mode 100644 index 00000000..43c94a3e --- /dev/null +++ b/test-server/net-v3-transition-server/Models/ClientResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace NetV3TransitionServer.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-v3-transition-server/Models/ErrorModels.cs b/test-server/net-v3-transition-server/Models/ErrorModels.cs new file mode 100644 index 00000000..7fbf6680 --- /dev/null +++ b/test-server/net-v3-transition-server/Models/ErrorModels.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace NetV3TransitionServer.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-v3-transition-server/NetV3TransitionServer.csproj b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj new file mode 100644 index 00000000..269f555f --- /dev/null +++ b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + false + + + + false + + + + + + + + + + + + + + + + diff --git a/test-server/net-v3-transition-server/Program.cs b/test-server/net-v3-transition-server/Program.cs new file mode 100644 index 00000000..138743c9 --- /dev/null +++ b/test-server/net-v3-transition-server/Program.cs @@ -0,0 +1,17 @@ +using NetV3TransitionServer.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddSingleton(); + +const int port = 8100; + +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-v3-transition-server/README.md b/test-server/net-v3-transition-server/README.md new file mode 100644 index 00000000..7634a4e7 --- /dev/null +++ b/test-server/net-v3-transition-server/README.md @@ -0,0 +1,66 @@ +# Net-V2-V3-Server + +A .NET test server for Amazon S3 encryption client .NET v3 transition. + +## Project Structure + +``` +net-v2-v3-server/ +├── Controllers/ # API controllers +├── Models/ # Data models +├── Services/ # Business logic services +├── Program.cs # Application entry point +├── NetV3TransitionServer.csproj # Project file +└── README.md # This file +``` + +## Running the Server + +For S3 Encryption Client v3 transition (runs on port 8100): + +```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:8100/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-v3-transition-server/Services/ClientCacheService.cs b/test-server/net-v3-transition-server/Services/ClientCacheService.cs new file mode 100644 index 00000000..0e7332ca --- /dev/null +++ b/test-server/net-v3-transition-server/Services/ClientCacheService.cs @@ -0,0 +1,28 @@ +using Amazon.Extensions.S3.Encryption; +using System.Collections.Concurrent; + +namespace NetV3TransitionServer.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-v3-transition-server/s3ec-v3-transition-branch b/test-server/net-v3-transition-server/s3ec-v3-transition-branch new file mode 160000 index 00000000..56008baf --- /dev/null +++ b/test-server/net-v3-transition-server/s3ec-v3-transition-branch @@ -0,0 +1 @@ +Subproject commit 56008baf1ef63b084a01a30db69af32e870a655b From c4026473f34349494e1cdd895a8a9699c227f52a Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:12:56 -0500 Subject: [PATCH 129/201] duvet (#67) --- test-server/cpp-v3-server/compliance.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-server/cpp-v3-server/compliance.txt b/test-server/cpp-v3-server/compliance.txt index 253f164f..8225d8a9 100644 --- a/test-server/cpp-v3-server/compliance.txt +++ b/test-server/cpp-v3-server/compliance.txt @@ -1,3 +1,12 @@ +** The C++ S3EC does not support re-encryption, nor custom instruction file suffixes +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC MAY support re-encryption/key rotation via Instruction Files. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC SHOULD support providing a custom Instruction File suffix on GetObject requests, regardless of whether or not re-encryption is supported. + ** We're not doing double encoding yet //= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata //= type=exception @@ -11,6 +20,10 @@ //= type=exception //# This encryption context string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. +//= ../specification/s3-encryption/data-format/content-metadata.md#v1-v2-shared +//= type=exception +//# This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + ** The C++ S3EC does not support key rings nor cmms //= ../specification/s3-encryption/client.md#cryptographic-materials From 570af0f56391fff10f13dd5944fde0aaf378784a Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 7 Nov 2025 15:07:42 -0800 Subject: [PATCH 130/201] Ruby with all annotations (#57) Update Ruby to the latest version --- .../amazon/encryption/s3/TestUtils.java | 23 ++++++++++++++----- test-server/ruby-v2-server/Gemfile.lock | 15 +++++++++--- test-server/ruby-v2-server/Makefile | 1 - test-server/ruby-v2-server/app.rb | 3 +++ test-server/ruby-v2-server/lib/logger.rb | 2 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/Gemfile.lock | 15 +++++++++--- test-server/ruby-v3-server/Makefile | 1 - test-server/ruby-v3-server/app.rb | 3 +++ test-server/ruby-v3-server/lib/logger.rb | 2 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 11 files changed, 51 insertions(+), 18 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 d21d0124..5e3d692b 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 @@ -132,9 +132,9 @@ public class TestUtils { // GO_V3_TRANSITION, // NET_V2_TRANSITION, NET_V3_TRANSITION, - CPP_V2_TRANSITION + CPP_V2_TRANSITION, // PHP_V2_TRANSITION, - // RUBY_V2_TRANSITION + RUBY_V2_TRANSITION ); public static final Set IMPROVED_VERSIONS = @@ -143,9 +143,9 @@ public class TestUtils { // PYTHON_V3, GO_V4, // NET_V4, - CPP_V3 + CPP_V3, // PHP_V3, - // RUBY_V3 + RUBY_V3 ); private static final Map serverMap; @@ -163,17 +163,25 @@ public class TestUtils { // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); - // servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); + servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers 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(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); + 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, "8090")); servers.put(NET_V3_TRANSITION, new LanguageServerTarget(NET_V3_TRANSITION, "8100")); serverMap = filterServers(servers); + + System.out.println("=== Configured Test Servers ==="); + System.out.println("\nServers:"); + serverMap.forEach((name, target) -> { + System.out.println(" " + name + " -> " + target.getServerURI()); + }); + System.out.println("\nTotal servers configured: " + serverMap.size()); + System.out.println("================================"); } public static class LanguageServerTarget { @@ -226,6 +234,8 @@ private static Map filterServers(Map filterServers(Map { String key = entry.getKey().toLowerCase(); + System.out.println("Checking server name:" + key); return Arrays.stream(filters).anyMatch(key::contains); }) .collect(Collectors.toMap( diff --git a/test-server/ruby-v2-server/Gemfile.lock b/test-server/ruby-v2-server/Gemfile.lock index 04815a40..c253552a 100644 --- a/test-server/ruby-v2-server/Gemfile.lock +++ b/test-server/ruby-v2-server/Gemfile.lock @@ -18,8 +18,8 @@ GEM specs: ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1160.0) - aws-sdk-core (3.232.0) + aws-partitions (1.1180.0) + aws-sdk-core (3.236.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -30,18 +30,23 @@ GEM aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bigdecimal (3.2.3) + bigdecimal (3.3.1) + bigdecimal (3.3.1-java) concurrent-ruby (1.3.5) jmespath (1.6.2) json (2.13.2) + json (2.13.2-java) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) mustermann (3.0.4) ruby2_keywords (~> 0.0.1) nio4r (2.7.4) + nio4r (2.7.4-java) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + nokogiri (1.18.10-java) + racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) @@ -49,7 +54,10 @@ GEM prism (1.5.1) puma (6.6.1) nio4r (~> 2.0) + puma (6.6.1-java) + nio4r (~> 2.0) racc (1.8.1) + racc (1.8.1-java) rack (2.2.17) rack-protection (3.2.0) base64 (>= 0.1.0) @@ -84,6 +92,7 @@ GEM PLATFORMS arm64-darwin-24 + universal-java-21 DEPENDENCIES aws-sdk-kms! diff --git a/test-server/ruby-v2-server/Makefile b/test-server/ruby-v2-server/Makefile index 2b2b59e1..f4297eac 100644 --- a/test-server/ruby-v2-server/Makefile +++ b/test-server/ruby-v2-server/Makefile @@ -11,7 +11,6 @@ start-server: exit 1; \ fi; @echo "Starting Ruby V2 server..." - gem install openssl bundle install AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \ AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb index adc7ade4..cde757a3 100644 --- a/test-server/ruby-v2-server/app.rb +++ b/test-server/ruby-v2-server/app.rb @@ -5,6 +5,9 @@ require_relative 'lib/error_handlers' require_relative 'lib/logger' +# See: https://github.com/ruby/openssl/issues/949 +Aws.use_bundled_cert! + class S3ECRubyServer < Sinatra::Base configure do set :port, ENV['PORT'] || 8098 diff --git a/test-server/ruby-v2-server/lib/logger.rb b/test-server/ruby-v2-server/lib/logger.rb index 2febcbab..3e820c7f 100644 --- a/test-server/ruby-v2-server/lib/logger.rb +++ b/test-server/ruby-v2-server/lib/logger.rb @@ -11,7 +11,7 @@ def initialize @logger = Logger.new(STDOUT) @logger.level = ENV['LOG_LEVEL'] ? Logger.const_get(ENV['LOG_LEVEL'].upcase) : Logger::INFO @logger.formatter = proc do |severity, datetime, progname, msg| - "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" + "[RUBY TRANSITIONAL #{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" end end diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index f04deb72..cb851a05 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit f04deb7227dca1ad1a193d11f7c57803843a197e +Subproject commit cb851a050a95e8615fd5d8d6ea9c8dfa1d362ae9 diff --git a/test-server/ruby-v3-server/Gemfile.lock b/test-server/ruby-v3-server/Gemfile.lock index 9edf1f5d..c253552a 100644 --- a/test-server/ruby-v3-server/Gemfile.lock +++ b/test-server/ruby-v3-server/Gemfile.lock @@ -18,8 +18,8 @@ GEM specs: ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1161.0) - aws-sdk-core (3.232.0) + aws-partitions (1.1180.0) + aws-sdk-core (3.236.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -30,18 +30,23 @@ GEM aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bigdecimal (3.2.3) + bigdecimal (3.3.1) + bigdecimal (3.3.1-java) concurrent-ruby (1.3.5) jmespath (1.6.2) json (2.13.2) + json (2.13.2-java) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) mustermann (3.0.4) ruby2_keywords (~> 0.0.1) nio4r (2.7.4) + nio4r (2.7.4-java) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + nokogiri (1.18.10-java) + racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) @@ -49,7 +54,10 @@ GEM prism (1.5.1) puma (6.6.1) nio4r (~> 2.0) + puma (6.6.1-java) + nio4r (~> 2.0) racc (1.8.1) + racc (1.8.1-java) rack (2.2.17) rack-protection (3.2.0) base64 (>= 0.1.0) @@ -84,6 +92,7 @@ GEM PLATFORMS arm64-darwin-24 + universal-java-21 DEPENDENCIES aws-sdk-kms! diff --git a/test-server/ruby-v3-server/Makefile b/test-server/ruby-v3-server/Makefile index 7e256788..ec463bad 100644 --- a/test-server/ruby-v3-server/Makefile +++ b/test-server/ruby-v3-server/Makefile @@ -11,7 +11,6 @@ start-server: exit 1; \ fi; @echo "Starting Ruby V3 server..." - gem install openssl bundle install AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \ AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ diff --git a/test-server/ruby-v3-server/app.rb b/test-server/ruby-v3-server/app.rb index f6e28817..8ebceac0 100644 --- a/test-server/ruby-v3-server/app.rb +++ b/test-server/ruby-v3-server/app.rb @@ -5,6 +5,9 @@ require_relative 'lib/error_handlers' require_relative 'lib/logger' +# See: https://github.com/ruby/openssl/issues/949 +Aws.use_bundled_cert! + class S3ECRubyServer < Sinatra::Base configure do set :port, ENV['PORT'] || 8092 diff --git a/test-server/ruby-v3-server/lib/logger.rb b/test-server/ruby-v3-server/lib/logger.rb index 2febcbab..df8ad9db 100644 --- a/test-server/ruby-v3-server/lib/logger.rb +++ b/test-server/ruby-v3-server/lib/logger.rb @@ -11,7 +11,7 @@ def initialize @logger = Logger.new(STDOUT) @logger.level = ENV['LOG_LEVEL'] ? Logger.const_get(ENV['LOG_LEVEL'].upcase) : Logger::INFO @logger.formatter = proc do |severity, datetime, progname, msg| - "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" + "[RUBY IMPROVED #{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" end end diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index f04deb72..cb851a05 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit f04deb7227dca1ad1a193d11f7c57803843a197e +Subproject commit cb851a050a95e8615fd5d8d6ea9c8dfa1d362ae9 From 47179c5eeee34a9d8b6dc4df2b2ce0ed7b59c624 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:22:57 -0800 Subject: [PATCH 131/201] chore: s3ec-java v4 improved tests server (#42) --- .gitmodules | 9 ++- .../amazon/encryption/s3/CBCDecryptTests.java | 2 + .../s3/ExhaustiveRoundTripTests1_25.java | 4 + .../amazon/encryption/s3/GCMTests.java | 2 + .../amazon/encryption/s3/KC_GCMTests.java | 1 + .../amazon/encryption/s3/RoundTripTests.java | 13 ++++ .../amazon/encryption/s3/TestUtils.java | 6 +- test-server/java-v3-server/.duvet/config.toml | 10 +-- test-server/java-v3-server/specification | 1 + .../.duvet/.gitignore | 3 + .../.duvet/config.toml | 27 +++++++ .../java-v3-transition-server/.gitignore | 1 + .../java-v3-transition-server/Makefile | 13 +++- .../java-v3-transition-server/s3ec-staging | 2 +- .../java-v3-transition-server/specification | 1 + .../s3/CreateClientOperationImpl.java | 75 ++++++++++++++---- test-server/java-v4-server/.duvet/.gitignore | 3 + test-server/java-v4-server/.duvet/config.toml | 27 +++++++ test-server/java-v4-server/.gitignore | 1 + test-server/java-v4-server/Makefile | 11 ++- test-server/java-v4-server/README.md | 2 +- test-server/java-v4-server/build.gradle.kts | 2 +- test-server/java-v4-server/s3ec-staging | 2 +- test-server/java-v4-server/specification | 1 + .../s3/CreateClientOperationImpl.java | 78 +++++++++++++++---- .../encryption/s3/S3ECJavaTestServer.java | 2 +- .../Controllers/ClientController.cs | 5 +- .../Models/ClientRequest.cs | 1 + 28 files changed, 248 insertions(+), 57 deletions(-) create mode 120000 test-server/java-v3-server/specification create mode 100644 test-server/java-v3-transition-server/.duvet/.gitignore create mode 100644 test-server/java-v3-transition-server/.duvet/config.toml create mode 100644 test-server/java-v3-transition-server/.gitignore create mode 120000 test-server/java-v3-transition-server/specification create mode 100644 test-server/java-v4-server/.duvet/.gitignore create mode 100644 test-server/java-v4-server/.duvet/config.toml create mode 100644 test-server/java-v4-server/.gitignore create mode 120000 test-server/java-v4-server/specification diff --git a/.gitmodules b/.gitmodules index 952fb220..fe27b90e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,11 +18,16 @@ [submodule "test-server/java-v3-transition-server/s3ec-staging"] path = test-server/java-v3-transition-server/s3ec-staging url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git - branch = s3ec/transitional + branch = imabhichow/s3ec-transition [submodule "test-server/java-v4-server/s3ec-staging"] path = test-server/java-v4-server/s3ec-staging url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git - branch = s3ec/improved + branch = imabhichow/add-kc +; branch = s3ec/improved +[submodule "test-server/java-v4-server/specification"] + path = test-server/java-v4-server/specification + url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git + branch = fire-egg-staging [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java index 4de6aef4..093ba2ab 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java @@ -107,6 +107,7 @@ void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc( .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()) @@ -124,6 +125,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_cbc(Te .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()) 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 eb48d6bd..100925a9 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 @@ -26,6 +26,7 @@ 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; @@ -99,6 +100,7 @@ public void GIVEN_CBCEncryptedData_AND_ImprovedClientDecryptingWithForbidEncrypt .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .enableLegacyWrappingAlgorithms(true) .build() ) @@ -135,6 +137,7 @@ public void GIVEN_GCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncrypt .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String s3ECId = output1.getClientId(); @@ -203,6 +206,7 @@ public void GIVEN_KCGCMEncryptedData_AND_ImprovedClientDecryptingWithForbidEncry .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String decS3ECId = decClientOutput.getClientId(); 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 index deb1571d..6eef0b5f 100644 --- 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 @@ -74,6 +74,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_encrypt_gcm(Te .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(); @@ -123,6 +124,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_gcm(Te .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(); 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 index 9e77e20e..99e8d37d 100644 --- 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 @@ -157,6 +157,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm .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(); 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 0cee88ee..ed107f75 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 @@ -70,6 +70,7 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar .builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -85,6 +86,7 @@ public void crossLanguageTestKms(LanguageServerTarget encLang, LanguageServerTar .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -120,6 +122,7 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -137,6 +140,7 @@ public void crossLanguageTestKmsWithEncCtx(LanguageServerTarget encLang, Languag .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -176,6 +180,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String encS3ECId = encClientOutput.getClientId(); @@ -192,6 +197,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -232,6 +238,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -249,6 +256,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build() ) .build()); @@ -288,6 +296,7 @@ public void kmsV1Legacy(TestUtils.LanguageServerTarget language) { .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String s3ECId = output1.getClientId(); @@ -331,6 +340,7 @@ public void kmsV1LegacyWithEncCtx(TestUtils.LanguageServerTarget language) { .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String s3ECId = output1.getClientId(); @@ -381,6 +391,7 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la .enableLegacyWrappingAlgorithms(false) .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String s3ECId = output1.getClientId(); @@ -444,6 +455,7 @@ public void instructionFileReadV2Format(TestUtils.LanguageServerTarget language) .enableLegacyWrappingAlgorithms(true) .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String s3ECId = output1.getClientId(); @@ -508,6 +520,7 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe .config(S3ECConfig.builder() .keyMaterial(kmsKeyArn) .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .build()) .build()); String decS3ECId = decOutput.getClientId(); 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 5e3d692b..fcff35da 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 @@ -139,7 +139,7 @@ public class TestUtils { public static final Set IMPROVED_VERSIONS = Set.of( - // JAVA_V4, + JAVA_V4, // PYTHON_V3, GO_V4, // NET_V4, @@ -166,12 +166,12 @@ public class TestUtils { servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers - servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); + // 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(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, "8090")); + servers.put(JAVA_V4, new LanguageServerTarget(JAVA_V4, "8088")); servers.put(NET_V3_TRANSITION, new LanguageServerTarget(NET_V3_TRANSITION, "8100")); serverMap = filterServers(servers); diff --git a/test-server/java-v3-server/.duvet/config.toml b/test-server/java-v3-server/.duvet/config.toml index b38762ab..f03424b2 100644 --- a/test-server/java-v3-server/.duvet/config.toml +++ b/test-server/java-v3-server/.duvet/config.toml @@ -1,17 +1,17 @@ '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" [[source]] -pattern = "**/*.java" +pattern = "s3ec-staging/*.java" # Include required specifications here [[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" +source = "specification/s3-encryption/data-format/content-metadata.md" [[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" +source = "specification/s3-encryption/data-format/metadata-strategy.md" [[specification]] -source = "../specification/s3-encryption/encryption.md" +source = "specification/s3-encryption/encryption.md" [[specification]] -source = "../specification/s3-encryption/key-derivation.md" +source = "specification/s3-encryption/key-derivation.md" [report.html] enabled = true diff --git a/test-server/java-v3-server/specification b/test-server/java-v3-server/specification new file mode 120000 index 00000000..b173f708 --- /dev/null +++ b/test-server/java-v3-server/specification @@ -0,0 +1 @@ +../specification \ No newline at end of file diff --git a/test-server/java-v3-transition-server/.duvet/.gitignore b/test-server/java-v3-transition-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/java-v3-transition-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/java-v3-transition-server/.duvet/config.toml b/test-server/java-v3-transition-server/.duvet/config.toml new file mode 100644 index 00000000..d07014da --- /dev/null +++ b/test-server/java-v3-transition-server/.duvet/config.toml @@ -0,0 +1,27 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "s3ec-staging/*.java" + +# 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/java-v3-transition-server/.gitignore b/test-server/java-v3-transition-server/.gitignore new file mode 100644 index 00000000..e660fd93 --- /dev/null +++ b/test-server/java-v3-transition-server/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/test-server/java-v3-transition-server/Makefile b/test-server/java-v3-transition-server/Makefile index b4371d0a..3f0358c9 100644 --- a/test-server/java-v3-transition-server/Makefile +++ b/test-server/java-v3-transition-server/Makefile @@ -7,18 +7,17 @@ PORT := 8094 build-s3ec: @echo "Building S3EC from source..." - cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile - cd s3ec-staging && mvn -B -ntp install -DskipTests + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile && mvn -B -ntp install -DskipTests @echo "S3EC build completed." start-server: build-s3ec - @echo "Starting Java V3 server..." + @echo "Starting Java V3 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" \ ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) - @echo "Java V3 server starting..." + @echo "Java V3 Transition server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ @@ -28,3 +27,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html \ No newline at end of file diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging index d20064ea..c8527040 160000 --- a/test-server/java-v3-transition-server/s3ec-staging +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -1 +1 @@ -Subproject commit d20064ea735016288b362bfbf9b0d7cd12115feb +Subproject commit c852704026ebc1c46bd11b6d5ab6d9a37ec1985d diff --git a/test-server/java-v3-transition-server/specification b/test-server/java-v3-transition-server/specification new file mode 120000 index 00000000..b173f708 --- /dev/null +++ b/test-server/java-v3-transition-server/specification @@ -0,0 +1 @@ +../specification \ No newline at end of file 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 8622f1ac..f874c977 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,7 +1,7 @@ package software.amazon.encryption.s3; -import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.S3EncryptionClient; import software.amazon.encryption.s3.materials.AesKeyring; @@ -9,13 +9,13 @@ import software.amazon.encryption.s3.materials.KmsKeyring; import software.amazon.encryption.s3.materials.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RsaKeyring; -import software.amazon.smithy.java.core.schema.Schema; -import software.amazon.smithy.java.server.RequestContext; 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.GenericServerError; import software.amazon.encryption.s3.model.KeyMaterial; import software.amazon.encryption.s3.service.CreateClientOperation; +import software.amazon.smithy.java.server.RequestContext; import javax.crypto.spec.SecretKeySpec; import java.io.PrintWriter; @@ -24,11 +24,12 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Arrays; import java.util.Map; -import java.util.Optional; import java.util.UUID; +import static software.amazon.encryption.s3.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT; +import static software.amazon.encryption.s3.model.EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + public class CreateClientOperationImpl implements CreateClientOperation { private Map clientCache_; @@ -90,18 +91,36 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c throw new RuntimeException("No KeyMaterial found!"); } - // Client Creation - boolean instFilePut = false; - if (input.getConfig().getInstructionFileConfig() != null) { - instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + } + + // V3-Transitional server configuration + S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()) + .keyring(keyring); + + // Configure commitment policy if provided ( feature) + if (input.getConfig().getCommitmentPolicy() != null) { + CommitmentPolicy policy = getCommitmentPolicy(input); + clientBuilder.commitmentPolicy(policy); } - S3Client s3Client = S3EncryptionClient.builder() - .instructionFileConfig(InstructionFileConfig.builder() - .instructionFileClient(S3Client.create()) - .enableInstructionFilePutObject(instFilePut) - .build()) - .keyring(keyring) - .build(); + // V3-Transitional default: No commitment policy (null) for backward compatibility + + // Configure encryption algorithm if provided ( feature) + if (input.getConfig().getEncryptionAlgorithm() != null) { + AlgorithmSuite algorithm = getAlgorithmSuite(input); + clientBuilder.encryptionAlgorithm(algorithm); + } else { + // V3-Transitional default: Legacy algorithm for backward compatibility + clientBuilder.encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + } + + S3Client s3Client = clientBuilder.build(); UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); clientCache_.put(uuidString, s3Client); @@ -117,4 +136,28 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c .build(); } } + + private static AlgorithmSuite getAlgorithmSuite(CreateClientInput input) { + if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; + } else if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + } else if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { + return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + } else { + throw new RuntimeException("Unknown encryption algorithm: " + input.getConfig().getEncryptionAlgorithm()); + } + } + + private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(CreateClientInput input) { + if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { + return FORBID_ENCRYPT_ALLOW_DECRYPT; + } else if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { + return null; + } else if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { + return null; + } else { + throw new RuntimeException("Unknown commitment policy: " + input.getConfig().getCommitmentPolicy()); + } + } } diff --git a/test-server/java-v4-server/.duvet/.gitignore b/test-server/java-v4-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/java-v4-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/java-v4-server/.duvet/config.toml b/test-server/java-v4-server/.duvet/config.toml new file mode 100644 index 00000000..d07014da --- /dev/null +++ b/test-server/java-v4-server/.duvet/config.toml @@ -0,0 +1,27 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "s3ec-staging/*.java" + +# 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/java-v4-server/.gitignore b/test-server/java-v4-server/.gitignore new file mode 100644 index 00000000..e660fd93 --- /dev/null +++ b/test-server/java-v4-server/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/test-server/java-v4-server/Makefile b/test-server/java-v4-server/Makefile index 922499f7..734a7808 100644 --- a/test-server/java-v4-server/Makefile +++ b/test-server/java-v4-server/Makefile @@ -3,12 +3,11 @@ .PHONY: start-server stop-server wait-for-server build-s3ec PID_FILE := server.pid -PORT := 8090 +PORT := 8088 build-s3ec: @echo "Building S3EC from source..." - cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile - cd s3ec-staging && mvn -B -ntp install -DskipTests + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile && mvn -B -ntp install -DskipTests @echo "S3EC build completed." start-server: build-s3ec @@ -28,3 +27,9 @@ stop-server: wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html \ No newline at end of file diff --git a/test-server/java-v4-server/README.md b/test-server/java-v4-server/README.md index d011daa2..70d60914 100644 --- a/test-server/java-v4-server/README.md +++ b/test-server/java-v4-server/README.md @@ -18,6 +18,6 @@ To run the server: gradle run ``` -This will start the server running on port `8090`. +This will start the server running on port `8088`. The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v4-server/build.gradle.kts b/test-server/java-v4-server/build.gradle.kts index de3ea1f3..fcb3c3ca 100644 --- a/test-server/java-v4-server/build.gradle.kts +++ b/test-server/java-v4-server/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") // S3EC from local Maven repository (installed by mvn install) - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-IMPROVED") + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-add-kc") } // Use that application plugin to start the service via the `run` task. diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging index a48d2b8d..c101d149 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit a48d2b8d951246fef0363dd3ef2bd82c4bf04988 +Subproject commit c101d14946a8279387b73482a31c03c2f269c9a4 diff --git a/test-server/java-v4-server/specification b/test-server/java-v4-server/specification new file mode 120000 index 00000000..b173f708 --- /dev/null +++ b/test-server/java-v4-server/specification @@ -0,0 +1 @@ +../specification \ No newline at end of file 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 8622f1ac..3607bbf0 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 @@ -3,19 +3,19 @@ import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.internal.InstructionFileConfig; -import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; 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.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RsaKeyring; -import software.amazon.smithy.java.core.schema.Schema; -import software.amazon.smithy.java.server.RequestContext; 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.GenericServerError; import software.amazon.encryption.s3.model.KeyMaterial; import software.amazon.encryption.s3.service.CreateClientOperation; +import software.amazon.smithy.java.server.RequestContext; import javax.crypto.spec.SecretKeySpec; import java.io.PrintWriter; @@ -24,11 +24,13 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Arrays; import java.util.Map; -import java.util.Optional; import java.util.UUID; +import static software.amazon.encryption.s3.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT; +import static software.amazon.encryption.s3.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT; +import static software.amazon.encryption.s3.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT; + public class CreateClientOperationImpl implements CreateClientOperation { private Map clientCache_; @@ -90,18 +92,38 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c throw new RuntimeException("No KeyMaterial found!"); } - // Client Creation - boolean instFilePut = false; - if (input.getConfig().getInstructionFileConfig() != null) { - instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + + // Client Creation + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + } + + // Configure commitment policy if provided + software.amazon.encryption.s3.CommitmentPolicy policy = CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT; + if (input.getConfig().getCommitmentPolicy() != null) { + policy = getCommitmentPolicy(input.getConfig().getCommitmentPolicy()); } - S3Client s3Client = S3EncryptionClient.builder() - .instructionFileConfig(InstructionFileConfig.builder() - .instructionFileClient(S3Client.create()) - .enableInstructionFilePutObject(instFilePut) - .build()) - .keyring(keyring) - .build(); + + // Configure encryption algorithm if provided + AlgorithmSuite algorithm = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + if (input.getConfig().getEncryptionAlgorithm() != null) { + algorithm = getAlgorithmSuite(input.getConfig().getEncryptionAlgorithm()); + } + + // V4-Improved server configuration + S3EncryptionClient s3Client = S3EncryptionClient.builderV4() + .instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()) + .keyring(keyring) + .commitmentPolicy(policy) + .encryptionAlgorithm(algorithm) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()) + .build(); + UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); clientCache_.put(uuidString, s3Client); @@ -117,4 +139,28 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c .build(); } } + + private static AlgorithmSuite getAlgorithmSuite(EncryptionAlgorithm input) { + if (input.equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { + return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + } else { + throw new RuntimeException("Unknown encryption algorithm: " + input); + } + } + + private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(software.amazon.encryption.s3.model.CommitmentPolicy input) { + if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { + return FORBID_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { + return REQUIRE_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { + return REQUIRE_ENCRYPT_REQUIRE_DECRYPT; + } else { + throw new RuntimeException("Unknown commitment policy: " + input); + } + } } 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 7b73ffd1..c3fee4f1 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 @@ -15,7 +15,7 @@ import software.amazon.encryption.s3.service.S3ECTestServer; public class S3ECJavaTestServer implements Runnable { - static final URI endpoint = URI.create("http://localhost:8090"); + static final URI endpoint = URI.create("http://localhost:8088"); public static void main(String[] args) { new S3ECJavaTestServer().run(); diff --git a/test-server/net-v3-transition-server/Controllers/ClientController.cs b/test-server/net-v3-transition-server/Controllers/ClientController.cs index b2fc2d4e..16630e9c 100644 --- a/test-server/net-v3-transition-server/Controllers/ClientController.cs +++ b/test-server/net-v3-transition-server/Controllers/ClientController.cs @@ -45,9 +45,8 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation("[NET-V3-Transitional] Created securityProfile= {securityProfile}", securityProfile.ToString()); - // Currently, tests does not send EncryptionAlgorithm. Tests only validates EncryptionAlgorithm from metadata of the response. - // var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); - var encryptionAlgorithm = commitmentPolicy == Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt ? ContentEncryptionAlgorithm.AesGcm : ContentEncryptionAlgorithm.AesGcmWithCommitment; + var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); + // var encryptionAlgorithm = commitmentPolicy == Amazon.Extensions.S3.Encryption.CommitmentPolicy.ForbidEncryptAllowDecrypt ? ContentEncryptionAlgorithm.AesGcm : ContentEncryptionAlgorithm.AesGcmWithCommitment; logger.LogInformation("[NET-V3-Transitional] Created commitmentPolicy= {commitmentPolicy}", commitmentPolicy); logger.LogInformation("[NET-V3-Transitional] Created encryptionAlgorithm= {encryptionAlgorithm}", encryptionAlgorithm); diff --git a/test-server/net-v3-transition-server/Models/ClientRequest.cs b/test-server/net-v3-transition-server/Models/ClientRequest.cs index 05b9a37e..cd5fa406 100644 --- a/test-server/net-v3-transition-server/Models/ClientRequest.cs +++ b/test-server/net-v3-transition-server/Models/ClientRequest.cs @@ -40,6 +40,7 @@ public enum CommitmentPolicy FORBID_ENCRYPT_ALLOW_DECRYPT } +[JsonConverter(typeof(JsonStringEnumConverter))] public enum EncryptionAlgorithm { ALG_AES_256_CBC_IV16_NO_KDF, From 3c40ebbb5ec25300c9ff773d419d9dd429ad582f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:11:24 -0800 Subject: [PATCH 132/201] chore: fix instruction file tests for php (#66) * chore: fix php instruction files * refactor put_object routes --- .../software/amazon/encryption/s3/RoundTripTests.java | 4 ++-- test-server/php-v2-server/src/index.php | 1 + test-server/php-v2-server/src/put_object.php | 9 ++++++++- test-server/php-v2-transition-server/src/client.php | 1 + test-server/php-v2-transition-server/src/index.php | 1 + test-server/php-v2-transition-server/src/put_object.php | 9 ++++++++- test-server/php-v3-server/src/index.php | 1 + test-server/php-v3-server/src/put_object.php | 9 ++++++++- 8 files changed, 30 insertions(+), 5 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 ed107f75..007f36ae 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 @@ -542,8 +542,8 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe .build()); } // Check for inst file key - if (!encLang.getLanguageName().contains("Ruby")) { - // Ruby doesn't include it :( + if (!encLang.getLanguageName().startsWith("Ruby") && !encLang.getLanguageName().startsWith("PHP")) { + // Ruby and PHP do not include it :( assertTrue(ptInstFile.response().metadata().containsKey("x-amz-crypto-instr-file")); } assertFalse(ptInstFile.asUtf8String().isEmpty()); diff --git a/test-server/php-v2-server/src/index.php b/test-server/php-v2-server/src/index.php index cc5dee29..167834e0 100644 --- a/test-server/php-v2-server/src/index.php +++ b/test-server/php-v2-server/src/index.php @@ -163,6 +163,7 @@ function getCachedClient($clientId) $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); return [ + 's3Client' => $s3Client, 'encryptionClient' => $encryptionClient, 'materialsProvider' => $materialsProvider, 'config' => $config diff --git a/test-server/php-v2-server/src/put_object.php b/test-server/php-v2-server/src/put_object.php index 63058f7d..c6c592fe 100644 --- a/test-server/php-v2-server/src/put_object.php +++ b/test-server/php-v2-server/src/put_object.php @@ -1,5 +1,8 @@ putObject([ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, + '@MetadataStrategy' => $strategy, '@CipherOptions' => $cipherOptions, 'Bucket' => $bucket, 'Key' => $key, diff --git a/test-server/php-v2-transition-server/src/client.php b/test-server/php-v2-transition-server/src/client.php index beda557a..9c39d540 100644 --- a/test-server/php-v2-transition-server/src/client.php +++ b/test-server/php-v2-transition-server/src/client.php @@ -19,6 +19,7 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $instFileConfig = $configData['instructionFileConfig'] ?? null; $instFilePut = false; if ($instFileConfig != null) { $instFilePut = $instFileConfig['enableInstructionFilePutObject'] ?? false; diff --git a/test-server/php-v2-transition-server/src/index.php b/test-server/php-v2-transition-server/src/index.php index cc5dee29..167834e0 100644 --- a/test-server/php-v2-transition-server/src/index.php +++ b/test-server/php-v2-transition-server/src/index.php @@ -163,6 +163,7 @@ function getCachedClient($clientId) $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); return [ + 's3Client' => $s3Client, 'encryptionClient' => $encryptionClient, 'materialsProvider' => $materialsProvider, 'config' => $config diff --git a/test-server/php-v2-transition-server/src/put_object.php b/test-server/php-v2-transition-server/src/put_object.php index c7de4bb4..405257cc 100644 --- a/test-server/php-v2-transition-server/src/put_object.php +++ b/test-server/php-v2-transition-server/src/put_object.php @@ -1,5 +1,8 @@ putObject([ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, + '@MetadataStrategy' => $strategy, '@CipherOptions' => $cipherOptions, 'Bucket' => $bucket, 'Key' => $key, diff --git a/test-server/php-v3-server/src/index.php b/test-server/php-v3-server/src/index.php index cc5dee29..167834e0 100644 --- a/test-server/php-v3-server/src/index.php +++ b/test-server/php-v3-server/src/index.php @@ -163,6 +163,7 @@ function getCachedClient($clientId) $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); return [ + 's3Client' => $s3Client, 'encryptionClient' => $encryptionClient, 'materialsProvider' => $materialsProvider, 'config' => $config diff --git a/test-server/php-v3-server/src/put_object.php b/test-server/php-v3-server/src/put_object.php index 63058f7d..9cad796b 100644 --- a/test-server/php-v3-server/src/put_object.php +++ b/test-server/php-v3-server/src/put_object.php @@ -1,5 +1,8 @@ putObject([ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, + '@MetadataStrategy' => $strategy, '@CipherOptions' => $cipherOptions, 'Bucket' => $bucket, 'Key' => $key, From 7468927896c587e00ad18878e25b6be819c7df53 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 10 Nov 2025 13:15:32 -0800 Subject: [PATCH 133/201] Update ruby to do better auth tag magic (#71) --- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index cb851a05..9bffb525 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit cb851a050a95e8615fd5d8d6ea9c8dfa1d362ae9 +Subproject commit 9bffb525765b2aeebd6203072bf6e94d2ab53f90 diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index cb851a05..9bffb525 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit cb851a050a95e8615fd5d8d6ea9c8dfa1d362ae9 +Subproject commit 9bffb525765b2aeebd6203072bf6e94d2ab53f90 From fec0319a09e2f851e76b4bfccfb18eed27fed384 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:48:20 -0800 Subject: [PATCH 134/201] add all-examples directory, Ruby V2/V3 example (#64) * add v3 Ruby example (#68) Co-authored-by: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> --------- Co-authored-by: seebees Co-authored-by: Lucas McDonald --- all-examples/README.md | 74 ++++++++++++++ all-examples/ruby/v2/Gemfile | 12 +++ all-examples/ruby/v2/Gemfile.lock | 82 ++++++++++++++++ all-examples/ruby/v2/Makefile | 70 +++++++++++++ all-examples/ruby/v2/local-ruby-sdk | 1 + all-examples/ruby/v2/main.rb | 147 ++++++++++++++++++++++++++++ all-examples/ruby/v3/Gemfile | 12 +++ all-examples/ruby/v3/Makefile | 70 +++++++++++++ all-examples/ruby/v3/local-ruby-sdk | 1 + all-examples/ruby/v3/main.rb | 145 +++++++++++++++++++++++++++ 10 files changed, 614 insertions(+) create mode 100644 all-examples/README.md create mode 100644 all-examples/ruby/v2/Gemfile create mode 100644 all-examples/ruby/v2/Gemfile.lock create mode 100644 all-examples/ruby/v2/Makefile create mode 120000 all-examples/ruby/v2/local-ruby-sdk create mode 100644 all-examples/ruby/v2/main.rb create mode 100644 all-examples/ruby/v3/Gemfile create mode 100644 all-examples/ruby/v3/Makefile create mode 120000 all-examples/ruby/v3/local-ruby-sdk create mode 100644 all-examples/ruby/v3/main.rb diff --git a/all-examples/README.md b/all-examples/README.md new file mode 100644 index 00000000..59bc2d6c --- /dev/null +++ b/all-examples/README.md @@ -0,0 +1,74 @@ +# S3 Encryption Client Examples + +This directory contains example projects for the Amazon S3 Encryption Client across different programming languages and major versions. + +## Directory Structure + +Each language has subdirectories for different major versions of the S3 Encryption Client: + +- `cpp/` - C++ examples + - `v2/` - S3EC C++ v2 example (transitional) + - `v3/` - S3EC C++ v3 example (improved) +- `dotnet/` - .NET examples + - `v3/` - S3EC .NET v3 example (transitional) + - `v4/` - S3EC .NET v4 example (improved) +- `go/` - Go examples + - `v3/` - S3EC Go v3 example (transitional) + - `v4/` - S3EC Go v4 example (improved) +- `java/` - Java examples + - `v3/` - S3EC Java v3 example (transitional) + - `v4/` - S3EC Java v4 example (improved) +- `php/` - PHP examples + - `v2/` - S3EC PHP v2 example (transitional) + - `v3/` - S3EC PHP v3 example (improved) +- `ruby/` - Ruby examples + - `v2/` - S3EC Ruby v2 example (transitional) + - `v3/` - S3EC Ruby v3 example (improved) + +## Setup Instructions + +### Prerequisites + +1. **Git Submodules**: Some examples depend on staging versions of the S3EC libraries that are included as git submodules. Initialize and update submodules: + + ```bash + git submodule update --init --recursive + ``` + +2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: + - AWS CLI: `aws configure` + - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + - IAM roles (for EC2 instances) + +3. **KMS Key**: Use "arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01" by default, or create a KMS key in your AWS account and note the key ID for use in examples. + +### Language-Specific Setup + +Each language directory contains specific setup instructions in its README file. Generally: + +- **Java**: Requires JDK 11+ and Gradle +- **Go**: Requires Go 1.21+ +- **.NET**: Requires .NET 8.0+ +- **PHP**: Requires PHP 7.4+ and Composer +- **Ruby**: Requires Ruby 3.0+ and Bundler +- **C++**: Requires CMake 3.16+ and C++17 compiler + +## Usage + +Each example directory contains: + +- Build configuration files (e.g., `build.gradle.kts`, `go.mod`, `composer.json`) +- Source code demonstrating basic S3EC usage +- README with specific setup and run instructions + +## Dependencies + +Examples use different dependency sources based on version: + +- **Released versions**: Use public package repositories (Maven Central, npm, etc.) +- **Staging versions**: Use git submodules pointing to staging repositories +- **Local versions**: Reference locally built libraries + +## Support + +For issues with specific examples, refer to the individual README files in each language/version directory. diff --git a/all-examples/ruby/v2/Gemfile b/all-examples/ruby/v2/Gemfile new file mode 100644 index 00000000..5f51bf18 --- /dev/null +++ b/all-examples/ruby/v2/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +ruby '>= 2.7.0' + +gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' +gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' +gem 'json', '~> 2.0' +gem 'rexml', '~> 3.0' + +group :development do + gem 'rubocop', '~> 1.0' +end diff --git a/all-examples/ruby/v2/Gemfile.lock b/all-examples/ruby/v2/Gemfile.lock new file mode 100644 index 00000000..7c4a32c4 --- /dev/null +++ b/all-examples/ruby/v2/Gemfile.lock @@ -0,0 +1,82 @@ +PATH + remote: local-ruby-sdk/gems/aws-sdk-kms + specs: + aws-sdk-kms (1.115.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sigv4 (~> 1.5) + +PATH + remote: local-ruby-sdk/gems/aws-sdk-s3 + specs: + aws-sdk-s3 (1.201.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1177.0) + aws-sdk-core (3.235.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bigdecimal (3.3.1) + jmespath (1.6.2) + json (2.15.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + prism (1.6.0) + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.11.3) + rexml (3.4.4) + rubocop (1.81.6) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + aws-sdk-kms! + aws-sdk-s3! + json (~> 2.0) + rexml (~> 3.0) + rubocop (~> 1.0) + +RUBY VERSION + ruby 3.4.7p58 + +BUNDLED WITH + 2.7.2 diff --git a/all-examples/ruby/v2/Makefile b/all-examples/ruby/v2/Makefile new file mode 100644 index 00000000..cfa1adef --- /dev/null +++ b/all-examples/ruby/v2/Makefile @@ -0,0 +1,70 @@ +# Makefile for S3 Encryption Client Ruby v2 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.rb + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-ruby-v2 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Bundler +install: + @echo "Installing Ruby dependencies..." + @bundle install + @echo "Dependencies installed successfully!" + +# Clean bundle artifacts +clean: + @echo "Cleaning bundle artifacts..." + @bundle clean --force + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v2 Ruby example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client Ruby v2 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install Ruby dependencies using Bundler" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove bundle artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Ruby 3.0+ installed on the system" + @echo " - Bundler gem installed (gem install bundler)" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v2 Ruby SDK (included in local-ruby-sdk)" diff --git a/all-examples/ruby/v2/local-ruby-sdk b/all-examples/ruby/v2/local-ruby-sdk new file mode 120000 index 00000000..7abd378c --- /dev/null +++ b/all-examples/ruby/v2/local-ruby-sdk @@ -0,0 +1 @@ +../../../test-server/ruby-v2-server/local-ruby-sdk \ No newline at end of file diff --git a/all-examples/ruby/v2/main.rb b/all-examples/ruby/v2/main.rb new file mode 100644 index 00000000..51d85cc9 --- /dev/null +++ b/all-examples/ruby/v2/main.rb @@ -0,0 +1,147 @@ +#!/usr/bin/env ruby + +require 'aws-sdk-s3' +require 'aws-sdk-kms' +require 'json' + +def main + # Check command line arguments + if ARGV.length != 4 + puts "Usage: #{$0} " + puts "Example: #{$0} avp-21638 s3ec-ruby-v2 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" + exit 1 + end + + bucket_name = ARGV[0] + object_key = ARGV[1] + kms_key_id = ARGV[2] + region = ARGV[3] + + puts "=== S3 Encryption Client v2 Example (Ruby) ===" + puts "Bucket: #{bucket_name}" + puts "Object Key: #{object_key}" + puts "KMS Key ID: #{kms_key_id}" + puts "Region: #{region}" + puts + + begin + # Test data for encryption + test_data = "Hello, World! This is a test message for S3 encryption client v2 in Ruby." + puts "Original data: #{test_data}" + puts "Data length: #{test_data.length} bytes" + puts + + puts "--- Initialize S3 Encryption Client v2 ---" + + # Create regular S3 client + s3_client = Aws::S3::Client.new(region: region) + + # Create KMS client + kms_client = Aws::KMS::Client.new(region: region) + + # Create S3 Encryption Client v2 + encryption_client = Aws::S3::EncryptionV2::Client.new( + client: s3_client, + kms_key_id: kms_key_id, + kms_client: kms_client, + key_wrap_schema: :kms_context, + content_encryption_schema: :aes_gcm_no_padding, + security_profile: :v2 + ) + + puts "Successfully initialized S3 Encryption Client v2" + puts "--- Encrypt and Upload Object to S3 ---" + + # Add encryption context + encryption_context = { + 'purpose' => 'example', + 'version' => 'v2', + 'language' => 'ruby' + } + + # Upload encrypted object using S3 Encryption Client + put_response = encryption_client.put_object({ + bucket: bucket_name, + key: object_key, + body: test_data, + kms_encryption_context: encryption_context + }) + + puts "Successfully uploaded encrypted object to S3!" + puts " Bucket: #{bucket_name}" + puts " Key: #{object_key}" + puts " Encryption Context: #{encryption_context}" + puts + + puts "--- Download and Decrypt Object from S3 ---" + + # Download and decrypt object using S3 Encryption Client + get_response = encryption_client.get_object({ + bucket: bucket_name, + key: object_key, + kms_encryption_context: encryption_context + }) + + # Read the decrypted data + decrypted_data = get_response.body.read + + puts "Successfully downloaded and decrypted object from S3!" + puts " Object size: #{decrypted_data.length} bytes" + puts " Decrypted data: #{decrypted_data}" + puts + + puts "--- Verify Roundtrip Success ---" + + # Verify the roundtrip was successful + if decrypted_data == test_data + puts "SUCCESS: Roundtrip encryption/decryption completed successfully!" + puts " Original data matches decrypted data" + puts " Data integrity verified" + else + puts "ERROR: Roundtrip failed - data mismatch" + puts " Original: #{test_data}" + puts " Decrypted: #{decrypted_data}" + exit 1 + end + + # Optionally Delete the Object + #puts "--- Cleanup ---" + # Clean up the test object using regular S3 client + # s3_client.delete_object({ + # bucket: bucket_name, + # key: object_key + # }) + # puts "Test object deleted from S3" + + puts + puts "=== Example completed successfully! ===" + + rescue Aws::S3::Errors::NoSuchBucket => e + puts "Error: S3 bucket '#{bucket_name}' does not exist or is not accessible" + puts " #{e.message}" + exit 1 + rescue Aws::KMS::Errors::NotFoundException => e + puts "Error: KMS key '#{kms_key_id}' not found or not accessible" + puts " #{e.message}" + exit 1 + rescue Aws::S3::EncryptionV2::Errors::EncryptionError => e + puts "S3 Encryption Error: #{e.message}" + exit 1 + rescue Aws::S3::EncryptionV2::Errors::DecryptionError => e + puts "S3 Decryption Error: #{e.message}" + exit 1 + rescue Aws::Errors::ServiceError => e + puts "AWS Service Error: #{e.message}" + puts " Error Code: #{e.code}" if e.respond_to?(:code) + exit 1 + rescue StandardError => e + puts "Unexpected error: #{e.message}" + puts e.backtrace.first(5) + exit 1 + end +end + +# Run the main function if this script is executed directly +if __FILE__ == $0 + main +end diff --git a/all-examples/ruby/v3/Gemfile b/all-examples/ruby/v3/Gemfile new file mode 100644 index 00000000..5f51bf18 --- /dev/null +++ b/all-examples/ruby/v3/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +ruby '>= 2.7.0' + +gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' +gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' +gem 'json', '~> 2.0' +gem 'rexml', '~> 3.0' + +group :development do + gem 'rubocop', '~> 1.0' +end diff --git a/all-examples/ruby/v3/Makefile b/all-examples/ruby/v3/Makefile new file mode 100644 index 00000000..b27bf29f --- /dev/null +++ b/all-examples/ruby/v3/Makefile @@ -0,0 +1,70 @@ +# Makefile for S3 Encryption Client Ruby v3 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.rb + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-ruby-v3 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Bundler +install: + @echo "Installing Ruby dependencies..." + @bundle install + @echo "Dependencies installed successfully!" + +# Clean bundle artifacts +clean: + @echo "Cleaning bundle artifacts..." + @bundle clean --force + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v3 Ruby example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client Ruby v3 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install Ruby dependencies using Bundler" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove bundle artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Ruby 3.0+ installed on the system" + @echo " - Bundler gem installed (gem install bundler)" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v3 Ruby SDK (included in local-ruby-sdk)" diff --git a/all-examples/ruby/v3/local-ruby-sdk b/all-examples/ruby/v3/local-ruby-sdk new file mode 120000 index 00000000..49be2657 --- /dev/null +++ b/all-examples/ruby/v3/local-ruby-sdk @@ -0,0 +1 @@ +../../../test-server/ruby-v3-server/local-ruby-sdk \ No newline at end of file diff --git a/all-examples/ruby/v3/main.rb b/all-examples/ruby/v3/main.rb new file mode 100644 index 00000000..fb15f317 --- /dev/null +++ b/all-examples/ruby/v3/main.rb @@ -0,0 +1,145 @@ +#!/usr/bin/env ruby + +require 'aws-sdk-s3' +require 'aws-sdk-kms' +require 'json' + +def main + # Check command line arguments + if ARGV.length != 4 + puts "Usage: #{$0} " + puts "Example: #{$0} avp-21638 s3ec-ruby-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" + exit 1 + end + + bucket_name = ARGV[0] + object_key = ARGV[1] + kms_key_id = ARGV[2] + region = ARGV[3] + + puts "=== S3 Encryption Client v3 Example (Ruby) ===" + puts "Bucket: #{bucket_name}" + puts "Object Key: #{object_key}" + puts "KMS Key ID: #{kms_key_id}" + puts "Region: #{region}" + puts + + begin + # Test data for encryption + test_data = "Hello, World! This is a test message for S3 encryption client v3 in Ruby." + puts "Original data: #{test_data}" + puts "Data length: #{test_data.length} bytes" + puts + + puts "--- Initialize S3 Encryption Client v3 ---" + + # Create regular S3 client + s3_client = Aws::S3::Client.new(region: region) + + # Create KMS client + kms_client = Aws::KMS::Client.new(region: region) + + # Create S3 Encryption Client v3 + encryption_client = Aws::S3::EncryptionV3::Client.new( + client: s3_client, + kms_key_id: kms_key_id, + kms_client: kms_client, + key_wrap_schema: :kms_context + ) + + puts "Successfully initialized S3 Encryption Client v3" + puts "--- Encrypt and Upload Object to S3 ---" + + # Add encryption context + encryption_context = { + 'purpose' => 'example', + 'version' => 'v3', + 'language' => 'ruby' + } + + # Upload encrypted object using S3 Encryption Client + put_response = encryption_client.put_object({ + bucket: bucket_name, + key: object_key, + body: test_data, + kms_encryption_context: encryption_context + }) + + puts "Successfully uploaded encrypted object to S3!" + puts " Bucket: #{bucket_name}" + puts " Key: #{object_key}" + puts " Encryption Context: #{encryption_context}" + puts + + puts "--- Download and Decrypt Object from S3 ---" + + # Download and decrypt object using S3 Encryption Client + get_response = encryption_client.get_object({ + bucket: bucket_name, + key: object_key, + kms_encryption_context: encryption_context + }) + + # Read the decrypted data + decrypted_data = get_response.body.read + + puts "Successfully downloaded and decrypted object from S3!" + puts " Object size: #{decrypted_data.length} bytes" + puts " Decrypted data: #{decrypted_data}" + puts + + puts "--- Verify Roundtrip Success ---" + + # Verify the roundtrip was successful + if decrypted_data == test_data + puts "SUCCESS: Roundtrip encryption/decryption completed successfully!" + puts " Original data matches decrypted data" + puts " Data integrity verified" + else + puts "ERROR: Roundtrip failed - data mismatch" + puts " Original: #{test_data}" + puts " Decrypted: #{decrypted_data}" + exit 1 + end + + # Optionally Delete the Object + #puts "--- Cleanup ---" + # Clean up the test object using regular S3 client + # s3_client.delete_object({ + # bucket: bucket_name, + # key: object_key + # }) + # puts "Test object deleted from S3" + + puts + puts "=== Example completed successfully! ===" + + rescue Aws::S3::Errors::NoSuchBucket => e + puts "Error: S3 bucket '#{bucket_name}' does not exist or is not accessible" + puts " #{e.message}" + exit 1 + rescue Aws::KMS::Errors::NotFoundException => e + puts "Error: KMS key '#{kms_key_id}' not found or not accessible" + puts " #{e.message}" + exit 1 + rescue Aws::S3::EncryptionV3::Errors::EncryptionError => e + puts "S3 Encryption Error: #{e.message}" + exit 1 + rescue Aws::S3::EncryptionV3::Errors::DecryptionError => e + puts "S3 Decryption Error: #{e.message}" + exit 1 + rescue Aws::Errors::ServiceError => e + puts "AWS Service Error: #{e.message}" + puts " Error Code: #{e.code}" if e.respond_to?(:code) + exit 1 + rescue StandardError => e + puts "Unexpected error: #{e.message}" + puts e.backtrace.first(5) + exit 1 + end +end + +# Run the main function if this script is executed directly +if __FILE__ == $0 + main +end From 3bc32ed4b7fc539ab2e1d5451bc411413bd7a1a0 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:42:46 -0800 Subject: [PATCH 135/201] chore: Add 's3:HeadObject' action to S3 bucket policy (#72) --- cdk/lib/cdk-stack.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index a56d6b26..1fad4b74 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -101,6 +101,7 @@ export class S3ECPythonGithub extends cdk.Stack { new PolicyStatement({ effect: Effect.ALLOW, actions: [ + "s3:HeadObject", // Only get object metadata "s3:PutObject", "s3:GetObject", "s3:DeleteObject", From 3e29c21a994f1d616541a9d902455a77f32abaa4 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Nov 2025 15:16:07 -0800 Subject: [PATCH 136/201] Go V3 transition server, duvet'ed (#60) --- .gitmodules | 4 + .../.duvet/.gitignore | 2 +- .../.duvet/config.toml | 12 +- test-server/go-v3-transition-server/Makefile | 31 ++ test-server/go-v3-transition-server/README.md | 23 ++ test-server/go-v3-transition-server/go.mod | 35 ++ test-server/go-v3-transition-server/go.sum | 45 +++ .../go-v3-transition-server/local-go-s3ec | 1 + test-server/go-v3-transition-server/main.go | 367 ++++++++++++++++++ .../amazon/encryption/s3/TestUtils.java | 4 +- 10 files changed, 518 insertions(+), 6 deletions(-) rename test-server/{go-v3-server => go-v3-transition-server}/.duvet/.gitignore (60%) rename test-server/{go-v3-server => go-v3-transition-server}/.duvet/config.toml (69%) create mode 100644 test-server/go-v3-transition-server/Makefile create mode 100644 test-server/go-v3-transition-server/README.md create mode 100644 test-server/go-v3-transition-server/go.mod create mode 100644 test-server/go-v3-transition-server/go.sum create mode 160000 test-server/go-v3-transition-server/local-go-s3ec create mode 100644 test-server/go-v3-transition-server/main.go diff --git a/.gitmodules b/.gitmodules index fe27b90e..e3080fcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,6 +44,10 @@ path = test-server/net-v2-v3-server/s3ec-net-v3 url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git branch = s3ec-v3 +[submodule "test-server/go-v3-transition-server/local-go-s3ec"] + path = test-server/go-v3-transition-server/local-go-s3ec + url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging + branch = v3-strip [submodule "test-server/net-v3-transition-server/s3ec-v3-transition-branch"] path = test-server/net-v3-transition-server/s3ec-v3-transition-branch url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git diff --git a/test-server/go-v3-server/.duvet/.gitignore b/test-server/go-v3-transition-server/.duvet/.gitignore similarity index 60% rename from test-server/go-v3-server/.duvet/.gitignore rename to test-server/go-v3-transition-server/.duvet/.gitignore index 93956e36..32ad579b 100644 --- a/test-server/go-v3-server/.duvet/.gitignore +++ b/test-server/go-v3-transition-server/.duvet/.gitignore @@ -1,3 +1,3 @@ reports/ requirements/ -specification/ \ No newline at end of file +specification/ diff --git a/test-server/go-v3-server/.duvet/config.toml b/test-server/go-v3-transition-server/.duvet/config.toml similarity index 69% rename from test-server/go-v3-server/.duvet/config.toml rename to test-server/go-v3-transition-server/.duvet/config.toml index 4729a668..713e72d3 100644 --- a/test-server/go-v3-server/.duvet/config.toml +++ b/test-server/go-v3-transition-server/.duvet/config.toml @@ -1,17 +1,23 @@ '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" [[source]] -pattern = "**/*.go" +pattern = "local-go-s3ec/v4/**/*.go" # 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/go-v3-transition-server/Makefile b/test-server/go-v3-transition-server/Makefile new file mode 100644 index 00000000..b03ea80b --- /dev/null +++ b/test-server/go-v3-transition-server/Makefile @@ -0,0 +1,31 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE := server.pid +PORT := 8095 + +start-server: + @echo "Starting Go V3 Transition server..." + go mod tidy + 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" \ + go run . & echo $$! > $(PID_FILE) + @echo "Go V3 Transition server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/go-v3-transition-server/README.md b/test-server/go-v3-transition-server/README.md new file mode 100644 index 00000000..e7e226f7 --- /dev/null +++ b/test-server/go-v3-transition-server/README.md @@ -0,0 +1,23 @@ +# S3EC Go V3 Transition Test Server + +This is the Go implementation of the S3ECTestServer framework for S3EC Go V3 Transition. It provides a server implementation for testing Go S3 Encryption Client V3 Transition functionality. + +## Overview + +The S3EC Go test server implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +go run . +``` + +This will start the server running on port `8095`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/go-v3-transition-server/go.mod b/test-server/go-v3-transition-server/go.mod new file mode 100644 index 00000000..50f1259a --- /dev/null +++ b/test-server/go-v3-transition-server/go.mod @@ -0,0 +1,35 @@ +module github.com/aws/amazon-s3-encryption-client-python/test-server/go-server + +go 1.21 + +require ( + github.com/aws/amazon-s3-encryption-client-go/v3 v3.0.0 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 + github.com/google/uuid v1.5.0 + github.com/gorilla/mux v1.8.1 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) + +// S3EC Go V4 is not released to pkg.go.dev as of writing. +// It is included as a submodule and referenced locally. +replace github.com/aws/amazon-s3-encryption-client-go/v3 => ./local-go-s3ec/v3 diff --git a/test-server/go-v3-transition-server/go.sum b/test-server/go-v3-transition-server/go.sum new file mode 100644 index 00000000..1bb969a3 --- /dev/null +++ b/test-server/go-v3-transition-server/go.sum @@ -0,0 +1,45 @@ + +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/test-server/go-v3-transition-server/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec new file mode 160000 index 00000000..7a29344c --- /dev/null +++ b/test-server/go-v3-transition-server/local-go-s3ec @@ -0,0 +1 @@ +Subproject commit 7a29344cc0c431fd5ac6d0a08ce4db455d75c175 diff --git a/test-server/go-v3-transition-server/main.go b/test-server/go-v3-transition-server/main.go new file mode 100644 index 00000000..799a9668 --- /dev/null +++ b/test-server/go-v3-transition-server/main.go @@ -0,0 +1,367 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/aws/amazon-s3-encryption-client-go/v3/client" + "github.com/aws/amazon-s3-encryption-client-go/v3/materials" + "github.com/aws/amazon-s3-encryption-client-go/v3/commitment" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" + "github.com/gorilla/mux" +) + +// Server represents the Go test server +type Server struct { + clientCache map[string]*client.S3EncryptionClientV3 + kmsClient *kms.Client +} + +// CreateClientInput represents the input for creating a client +type CreateClientInput struct { + Config S3ECConfig `json:"config"` +} + +// CreateClientOutput represents the output for creating a client +type CreateClientOutput struct { + ClientID string `json:"clientId"` +} + +// S3ECConfig represents the S3 encryption client configuration +type S3ECConfig struct { + EnableLegacyUnauthenticatedModes bool `json:"enableLegacyUnauthenticatedModes"` + EnableDelayedAuthenticationMode bool `json:"enableDelayedAuthenticationMode"` + EnableLegacyWrappingAlgorithms bool `json:"enableLegacyWrappingAlgorithms"` + SetBufferSize int64 `json:"setBufferSize"` + KeyMaterial KeyMaterial `json:"keyMaterial"` + CommitmentPolicy string `json:"commitmentPolicy"` +} + +// KeyMaterial represents the key material for encryption +type KeyMaterial struct { + RSAKey []byte `json:"rsaKey"` + AESKey []byte `json:"aesKey"` + KMSKeyID string `json:"kmsKeyId"` +} + +// PutObjectOutput represents the output for put object operation +type PutObjectOutput struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + Metadata []string `json:"metadata"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Type string `json:"__type"` + Message string `json:"message"` +} + +// NewServer creates a new server instance +func NewServer() (*Server, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + return &Server{ + clientCache: make(map[string]*client.S3EncryptionClientV3), + kmsClient: kms.NewFromConfig(cfg), + }, nil +} + +// createGenericServerError creates a generic server error response +func (s *Server) createGenericServerError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("[Go V3-Transition] GenericServerError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#GenericServerError", + Message: message, + }) +} + +// createS3EncryptionClientError creates an S3 encryption client error response +func (s *Server) createS3EncryptionClientError(w http.ResponseWriter, message string, statusCode int) { + // Echo error to console + log.Printf("[Go V3-Transition] S3EncryptionClientError: %s (Status: %d)", message, statusCode) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(ErrorResponse{ + Type: "software.amazon.encryption.s3#S3EncryptionClientError", + Message: message, + }) +} + +// metadataStringToMap converts metadata string to map +func metadataStringToMap(mdString string) (map[string]string, error) { + md := make(map[string]string) + if mdString == "" { + return md, nil + } + + mdList := strings.Split(mdString, ",") + for _, entry := range mdList { + // Split on "]:[" to separate key and value + parts := strings.Split(entry, "]:[") + if len(parts) == 2 { + // Remove remaining brackets from start and end + key := parts[0][1:] // Remove first character + value := parts[1][:len(parts[1])-1] // Remove last character + md[key] = value + } else { + return nil, fmt.Errorf("malformed metadata list entry: %s", entry) + } + } + return md, nil +} + +// createClient handles POST /client +func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { + // Read body + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var input CreateClientInput + if err := json.Unmarshal(body, &input); err != nil { + s.createGenericServerError(w, "Invalid JSON in request body", http.StatusBadRequest) + return + } + + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to load AWS config: %v", err), http.StatusInternalServerError) + return + } + + var commitmentPolicy commitment.CommitmentPolicy + switch input.Config.CommitmentPolicy { + case "REQUIRE_ENCRYPT_REQUIRE_DECRYPT": + commitmentPolicy = commitment.REQUIRE_ENCRYPT_REQUIRE_DECRYPT + case "REQUIRE_ENCRYPT_ALLOW_DECRYPT": + commitmentPolicy = commitment.REQUIRE_ENCRYPT_ALLOW_DECRYPT + case "FORBID_ENCRYPT_ALLOW_DECRYPT": + commitmentPolicy = commitment.FORBID_ENCRYPT_ALLOW_DECRYPT + } + + // Create KMS keyring + kmsClient := kms.NewFromConfig(cfg) + keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { + options.EnableLegacyWrappingAlgorithms = input.Config.EnableLegacyWrappingAlgorithms + }) + cmm, err := materials.NewCryptographicMaterialsManager(keyring) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create CMM: %v", err), http.StatusInternalServerError) + return + } + + // Create S3 encryption client + var s3EncryptionClient *client.S3EncryptionClientV3 + s3PlaintextClient := s3.NewFromConfig(cfg) + s3EncryptionClient, err = client.New(s3PlaintextClient, cmm, func(clientOptions *client.EncryptionClientOptions) { + if input.Config.CommitmentPolicy != "" { + clientOptions.CommitmentPolicy = commitmentPolicy + } + clientOptions.EnableLegacyUnauthenticatedModes = input.Config.EnableLegacyUnauthenticatedModes + }) + + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create S3EC: %v", err), http.StatusInternalServerError) + return + } + + // Generate client ID + clientID := uuid.New().String() + + // Store client in cache + s.clientCache[clientID] = s3EncryptionClient + + // Return response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CreateClientOutput{ + ClientID: clientID, + }) +} + +// putObject handles PUT /object/{bucket}/{key} +func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Read body + body, err := io.ReadAll(r.Body) + if err != nil { + s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) + return + } + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + // Create context with encryption context + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + // Create put object input + putInput := &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + Body: strings.NewReader(string(body)), + } + + // Add metadata if present + if len(encCtx) > 0 { + putInput.Metadata = encCtx + } + + // Make the put object request using the encryption client + _, err = client.PutObject(encryptionContext, putInput) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to put object: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("[Go V3-Transition] PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + + // Return response + w.Header().Set("Content-Type", "application/json") + resp := PutObjectOutput{ + Bucket: bucket, + Key: key, + Metadata: []string{}, // TODO: pass metadata back in response + } + json.NewEncoder(w).Encode(resp) +} + +// getObject handles GET /object/{bucket}/{key} +func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucket := vars["bucket"] + key := vars["key"] + + clientID := r.Header.Get("ClientID") + if clientID == "" { + s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) + return + } + + // Get client from cache + client, exists := s.clientCache[clientID] + + if !exists { + s.createGenericServerError(w, fmt.Sprintf("No client found for ClientID: %s", clientID), http.StatusNotFound) + return + } + + // Get metadata from header + metadataHeader := r.Header.Get("Content-Metadata") + encCtx, err := metadataStringToMap(metadataHeader) + + ctx := context.Background() + encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) + return + } + + // Create get object input + getInput := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + + // Make the get object request using the encryption client + result, err := client.GetObject(encryptionContext, getInput) + if err != nil { + errMsg := err.Error() + // Shim the S3EC error message to the error message expected by the test server. + // We don't want to change the S3EC error message but the test server expects a specific error message; + // This is the appropriate place to rewrite the error message. + if strings.Contains(errMsg, "to decrypt x-amz-cek-alg value `kms` you must enable legacyWrappingAlgorithms on the keyring") { + s.createS3EncryptionClientError(w, "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms", http.StatusInternalServerError) + return + } + + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to get object: %v", err), http.StatusInternalServerError) + return + } + defer result.Body.Close() + + // Read the body + body, err := io.ReadAll(result.Body) + if err != nil { + s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to read object body: %v", err), http.StatusInternalServerError) + return + } + + // Convert metadata to string format + var metadataList []string + if result.Metadata != nil { + for k, v := range result.Metadata { + metadataList = append(metadataList, fmt.Sprintf("%s=%s", k, v)) + } + } + + metadataStr := strings.Join(metadataList, ",") + + log.Printf("[Go V3-Transition] GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) + + // Set response headers + w.Header().Set("Content-Metadata", metadataStr) + + // Return the body as response + w.Write(body) +} + +func main() { + server, err := NewServer() + if err != nil { + log.Fatalf("[Go V3-Transition] Failed to create Go V3 Transition server: %v", err) + } + + r := mux.NewRouter() + + // Register routes + r.HandleFunc("/client", server.createClient).Methods("POST") + r.HandleFunc("/object/{bucket}/{key}", server.putObject).Methods("PUT") + r.HandleFunc("/object/{bucket}/{key}", server.getObject).Methods("GET") + + fmt.Println("[Go V3-Transition] Starting Go V3 Transition server on :8095...") + log.Fatal(http.ListenAndServe(":8095", r)) +} 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 fcff35da..26afb9ae 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 @@ -129,7 +129,7 @@ public class TestUtils { public static final Set TRANSITION_VERSIONS = Set.of( // JAVA_V3_TRANSITION, - // GO_V3_TRANSITION, + GO_V3_TRANSITION, // NET_V2_TRANSITION, NET_V3_TRANSITION, CPP_V2_TRANSITION, @@ -167,7 +167,7 @@ public class TestUtils { servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers // servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); - // servers.put(GO_V3_TRANSITION, new LanguageServerTarget(GO_V3_TRANSITION, "8095")); + servers.put(GO_V3_TRANSITION, new LanguageServerTarget(GO_V3_TRANSITION, "8095")); // 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")); From 9dd9344bb6af8469976a59e6632d94d4dc130a19 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Nov 2025 09:31:12 -0800 Subject: [PATCH 137/201] chore: Go v4 example, run examples in CI (#65) --- .github/workflows/examples.yml | 115 +++++++++++++++++ .github/workflows/main.yml | 8 ++ all-examples/Makefile | 110 ++++++++++++++++ all-examples/go/v4/Makefile | 69 ++++++++++ all-examples/go/v4/README.md | 55 ++++++++ all-examples/go/v4/go.mod | 32 +++++ all-examples/go/v4/go.sum | 40 ++++++ all-examples/go/v4/local-go-s3ec | 1 + all-examples/go/v4/main.go | 171 +++++++++++++++++++++++++ all-examples/ruby/v2/main.rb | 3 + all-examples/ruby/v3/main.rb | 3 + test-server/go-v4-server/local-go-s3ec | 2 +- 12 files changed, 608 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/examples.yml create mode 100644 all-examples/Makefile create mode 100644 all-examples/go/v4/Makefile create mode 100644 all-examples/go/v4/README.md create mode 100644 all-examples/go/v4/go.mod create mode 100644 all-examples/go/v4/go.sum create mode 120000 all-examples/go/v4/local-go-s3ec create mode 100644 all-examples/go/v4/main.go diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 00000000..30e47a06 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,115 @@ +name: Run Examples + +on: + workflow_call: + +jobs: + run-examples: + runs-on: macos-14-large + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + submodules: true + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + + - name: Checkout CPP code for cpp-v2-transition + uses: actions/checkout@v5 + with: + submodules: recursive + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ + + - name: Checkout CPP code cpp-v3 + uses: actions/checkout@v5 + with: + submodules: recursive + token: ${{ secrets.PAT_FOR_CPP }} + repository: awslabs/aws-sdk-cpp-staging + ref: fire-egg-dev + path: test-server/cpp-v3-server/aws-sdk-cpp/ + + - name: Checkout .NET V2 code + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + ref: v3sdk-development + path: test-server/net-v2-v3-server/s3ec-net-v2/ + + - name: Checkout .NET V3 code + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + ref: s3ec-v3 + path: test-server/net-v2-v3-server/s3ec-net-v3 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + + - name: Set up PHP with Composer + uses: shivammathur/setup-php@verbose + with: + php-version: "8.1" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24 + + # Cache uv dependencies + - name: Cache uv dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Install Uv + run: pip install uv + + # Cache Gradle dependencies and build outputs + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + test-server/java-v3-server/.gradle + test-server/java-tests/.gradle + key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-v3-server/**/*.gradle*', 'test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*', 'test-server/java-v3-server/**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role + aws-region: us-west-2 + + - name: Install dependencies for all examples + working-directory: ./all-examples + run: make install + + - name: Run all examples + working-directory: ./all-examples + run: make run + env: + AWS_REGION: us-west-2 + BUCKET_NAME: ${{ vars.TEST_SERVER_S3_BUCKET }} + KMS_KEY_ID: ${{ vars.TEST_SERVER_KMS_KEY_ARN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52c3e465..d53b9dca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,3 +35,11 @@ jobs: name: Run Duvet uses: ./.github/workflows/duvet.yml secrets: inherit + + run-examples: + permissions: + id-token: write + contents: read + name: Run Examples + uses: ./.github/workflows/examples.yml + secrets: inherit diff --git a/all-examples/Makefile b/all-examples/Makefile new file mode 100644 index 00000000..7dac8ce6 --- /dev/null +++ b/all-examples/Makefile @@ -0,0 +1,110 @@ +# Makefile for S3 Encryption Client Examples +# Runs make commands across all language/version directories + +# Default target +.PHONY: all install clean run help list-examples + +# Find all directories with Makefiles +EXAMPLE_DIRS := $(shell find . -name Makefile -not -path "./Makefile" | xargs dirname | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) + +all: install + +# Install dependencies for all examples +install: + @echo "Installing dependencies for all examples..." + @failed=0; \ + for dir in $(EXAMPLE_DIRS); do \ + echo ""; \ + echo "=== Installing dependencies in $$dir ==="; \ + if (cd $$dir && $(MAKE) install); then \ + echo "✓ Successfully installed dependencies in $$dir"; \ + else \ + echo "✗ Failed to install dependencies in $$dir"; \ + failed=$$((failed + 1)); \ + fi; \ + done; \ + echo ""; \ + if [ $$failed -eq 0 ]; then \ + echo "All dependencies installed successfully!"; \ + else \ + echo "$$failed example(s) failed to install dependencies"; \ + exit 1; \ + fi + +# Clean all examples +clean: + @echo "Cleaning all examples..." + @failed=0; \ + for dir in $(EXAMPLE_DIRS); do \ + echo ""; \ + echo "=== Cleaning $$dir ==="; \ + if (cd $$dir && $(MAKE) clean); then \ + echo "✓ Successfully cleaned $$dir"; \ + else \ + echo "✗ Failed to clean $$dir"; \ + failed=$$((failed + 1)); \ + fi; \ + done; \ + echo ""; \ + if [ $$failed -eq 0 ]; then \ + echo "All examples cleaned successfully!"; \ + else \ + echo "$$failed example(s) failed to clean"; \ + exit 1; \ + fi + +# Run all examples with default parameters +run: + @echo "Running all examples with default parameters..." + @failed=0; \ + for dir in $(EXAMPLE_DIRS); do \ + echo ""; \ + echo "=== Running example in $$dir ==="; \ + if (cd $$dir && $(MAKE) run); then \ + echo "✓ Successfully ran example in $$dir"; \ + else \ + echo "✗ Failed to run example in $$dir"; \ + failed=$$((failed + 1)); \ + fi; \ + done; \ + echo ""; \ + if [ $$failed -eq 0 ]; then \ + echo "All examples completed successfully!"; \ + else \ + echo "$$failed example(s) failed to run"; \ + exit 1; \ + fi + +# List all available examples +list-examples: + @echo "Available S3 Encryption Client examples:" + @for dir in $(EXAMPLE_DIRS); do \ + echo " $$dir"; \ + done + +# Show help +help: + @echo "S3 Encryption Client Examples Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install dependencies for all examples" + @echo " run - Run all examples with default parameters" + @echo " clean - Clean all examples" + @echo " list-examples - List all available example directories" + @echo " help - Show this help message" + @echo "" + @echo "Filtering examples:" + @echo " Use FILTER to run commands on specific examples:" + @echo " make install FILTER=go # Only Go examples" + @echo " make run FILTER=v4 # Only v4 examples" + @echo " make clean FILTER=go/v3,ruby # Go v3 and Ruby examples" + @echo "" + @echo "Individual example usage:" + @echo " To work with a specific example, cd into its directory and use its Makefile:" + @echo " cd go/v4 && make run" + @echo " cd ruby/v2 && make install" + @echo "" + @echo "Available examples:" + @for dir in $(EXAMPLE_DIRS); do \ + echo " $$dir"; \ + done diff --git a/all-examples/go/v4/Makefile b/all-examples/go/v4/Makefile new file mode 100644 index 00000000..1e8307e7 --- /dev/null +++ b/all-examples/go/v4/Makefile @@ -0,0 +1,69 @@ +# Makefile for S3 Encryption Client Go v4 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.go + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-go-v4 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Go modules +install: + @echo "Installing Go dependencies..." + @go mod tidy + @echo "Dependencies installed successfully!" + +# Clean Go artifacts +clean: + @echo "Cleaning Go artifacts..." + @go clean + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v4 Go example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client Go v4 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install Go dependencies using Go modules" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove Go artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Go 1.24+ installed on the system" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v4 Go SDK (included in local-go-s3ec)" diff --git a/all-examples/go/v4/README.md b/all-examples/go/v4/README.md new file mode 100644 index 00000000..b6972f26 --- /dev/null +++ b/all-examples/go/v4/README.md @@ -0,0 +1,55 @@ +# S3 Encryption Client Go v4 Example + +This example demonstrates how to use the Amazon S3 Encryption Client v4 for Go to perform client-side encryption and decryption of objects. + +## Prerequisites + +1. **Go**: Requires Go 1.24 or later +2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: + - AWS CLI: `aws configure` + - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + - IAM roles (for EC2 instances) +3. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` +4. **S3 Bucket**: An existing S3 bucket where you have read/write permissions + +## Setup + +1. Initialize submodules and download dependencies: + ```bash + make install + ``` + + Or manually: + ```bash + go mod tidy + ``` + + **Note**: This example uses a local submodule for the S3EC Go v4 library via the `replace` directive in `go.mod`. + +## Usage + +### Using Make (Recommended) + +Run the example with default parameters: +```bash +make run +``` + +Run with custom parameters: +```bash +make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +``` + +### Manual Usage + +Run the example with the following command: + +```bash +go run main.go +``` + +### Example: + +```bash +go run main.go my-test-bucket s3ec-go-v4-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2 +``` diff --git a/all-examples/go/v4/go.mod b/all-examples/go/v4/go.mod new file mode 100644 index 00000000..48bea56e --- /dev/null +++ b/all-examples/go/v4/go.mod @@ -0,0 +1,32 @@ +module github.com/aws/amazon-s3-encryption-client-python/all-examples/go/v4 + +go 1.24 + +require ( + github.com/aws/amazon-s3-encryption-client-go/v4 v4.0.0 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) + +// S3EC Go V4 uses a local submodule for development +replace github.com/aws/amazon-s3-encryption-client-go/v4 => ./local-go-s3ec/v4 diff --git a/all-examples/go/v4/go.sum b/all-examples/go/v4/go.sum new file mode 100644 index 00000000..244c8814 --- /dev/null +++ b/all-examples/go/v4/go.sum @@ -0,0 +1,40 @@ +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/all-examples/go/v4/local-go-s3ec b/all-examples/go/v4/local-go-s3ec new file mode 120000 index 00000000..d06737d9 --- /dev/null +++ b/all-examples/go/v4/local-go-s3ec @@ -0,0 +1 @@ +../../../test-server/go-v4-server/local-go-s3ec \ No newline at end of file diff --git a/all-examples/go/v4/main.go b/all-examples/go/v4/main.go new file mode 100644 index 00000000..a1227790 --- /dev/null +++ b/all-examples/go/v4/main.go @@ -0,0 +1,171 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/aws/amazon-s3-encryption-client-go/v4/client" + "github.com/aws/amazon-s3-encryption-client-go/v4/commitment" + "github.com/aws/amazon-s3-encryption-client-go/v4/materials" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func main() { + // Check command line arguments + if len(os.Args) != 5 { + fmt.Printf("Usage: %s \n", os.Args[0]) + fmt.Printf("Example: %s avp-21638 s3ec-go-v4 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n", os.Args[0]) + os.Exit(1) + } + + bucketName := os.Args[1] + objectKey := os.Args[2] + kmsKeyID := os.Args[3] + region := os.Args[4] + + fmt.Println("=== S3 Encryption Client v4 Example (Go) ===") + fmt.Printf("Bucket: %s\n", bucketName) + fmt.Printf("Object Key: %s\n", objectKey) + fmt.Printf("KMS Key ID: %s\n", kmsKeyID) + fmt.Printf("Region: %s\n", region) + fmt.Println() + + // Test data for encryption + testData := "Hello, World! This is a test message for S3 encryption client v4 in Go." + fmt.Printf("Original data: %s\n", testData) + fmt.Printf("Data length: %d bytes\n", len(testData)) + fmt.Println() + + fmt.Println("--- Initialize S3 Encryption Client v4 ---") + + // Create regular S3 client + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + fmt.Printf("Error loading AWS config: %v\n", err) + os.Exit(1) + } + s3Client := s3.NewFromConfig(cfg) + + // Create KMS client + kmsClient := kms.NewFromConfig(cfg) + + // Create KMS keyring + keyring := materials.NewKmsKeyring(kmsClient, kmsKeyID) + + // Create Cryptographic Materials Manager + cmm, err := materials.NewCryptographicMaterialsManager(keyring) + if err != nil { + fmt.Printf("Error creating CMM: %v\n", err) + os.Exit(1) + } + + // Create S3 Encryption Client v4 + encryptionClient, err := client.New(s3Client, cmm, func(options *client.EncryptionClientOptions) { + options.CommitmentPolicy = commitment.REQUIRE_ENCRYPT_ALLOW_DECRYPT + }) + if err != nil { + fmt.Printf("Error creating S3 Encryption Client: %v\n", err) + os.Exit(1) + } + + fmt.Println("Successfully initialized S3 Encryption Client v4") + fmt.Println("--- Encrypt and Upload Object to S3 ---") + + // Add encryption context + encryptionContext := map[string]string{ + "purpose": "example", + "version": "v4", + "language": "go", + } + + // Create context with encryption context + ctx := context.WithValue(context.Background(), "EncryptionContext", encryptionContext) + + // Upload encrypted object using S3 Encryption Client + putInput := &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + Body: strings.NewReader(testData), + } + + _, err = encryptionClient.PutObject(ctx, putInput) + if err != nil { + if strings.Contains(err.Error(), "NoSuchBucket") { + fmt.Printf("Error: S3 bucket '%s' does not exist or is not accessible\n", bucketName) + } else if strings.Contains(err.Error(), "NotFoundException") { + fmt.Printf("Error: KMS key '%s' not found or not accessible\n", kmsKeyID) + } else { + fmt.Printf("Error uploading encrypted object: %v\n", err) + } + os.Exit(1) + } + + fmt.Println("Successfully uploaded encrypted object to S3!") + fmt.Printf(" Bucket: %s\n", bucketName) + fmt.Printf(" Key: %s\n", objectKey) + fmt.Printf(" Encryption Context: %v\n", encryptionContext) + fmt.Println() + + fmt.Println("--- Download and Decrypt Object from S3 ---") + + // Download and decrypt object using S3 Encryption Client + getInput := &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + } + + getResponse, err := encryptionClient.GetObject(ctx, getInput) + if err != nil { + fmt.Printf("Error downloading and decrypting object: %v\n", err) + os.Exit(1) + } + defer getResponse.Body.Close() + + // Read the decrypted data + decryptedData, err := io.ReadAll(getResponse.Body) + if err != nil { + fmt.Printf("Error reading decrypted data: %v\n", err) + os.Exit(1) + } + + fmt.Println("Successfully downloaded and decrypted object from S3!") + fmt.Printf(" Object size: %d bytes\n", len(decryptedData)) + fmt.Printf(" Decrypted data: %s\n", string(decryptedData)) + fmt.Println() + + fmt.Println("--- Verify Roundtrip Success ---") + + // Verify the roundtrip was successful + if string(decryptedData) == testData { + fmt.Println("SUCCESS: Roundtrip encryption/decryption completed successfully!") + fmt.Println(" Original data matches decrypted data") + fmt.Println(" Data integrity verified") + } else { + fmt.Println("ERROR: Roundtrip failed - data mismatch") + fmt.Printf(" Original: %s\n", testData) + fmt.Printf(" Decrypted: %s\n", string(decryptedData)) + os.Exit(1) + } + + // Optionally Delete the Object + //fmt.Println("--- Cleanup ---") + // Clean up the test object using regular S3 client + // _, err = s3Client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ + // Bucket: aws.String(bucketName), + // Key: aws.String(objectKey), + // }) + // if err != nil { + // fmt.Printf("Error deleting test object: %v\n", err) + // } else { + // fmt.Println("Test object deleted from S3") + // } + + fmt.Println() + fmt.Println("=== Example completed successfully! ===") +} diff --git a/all-examples/ruby/v2/main.rb b/all-examples/ruby/v2/main.rb index 51d85cc9..3fc86f7c 100644 --- a/all-examples/ruby/v2/main.rb +++ b/all-examples/ruby/v2/main.rb @@ -4,6 +4,9 @@ require 'aws-sdk-kms' require 'json' +# See: https://github.com/ruby/openssl/issues/949 +Aws.use_bundled_cert! + def main # Check command line arguments if ARGV.length != 4 diff --git a/all-examples/ruby/v3/main.rb b/all-examples/ruby/v3/main.rb index fb15f317..59743515 100644 --- a/all-examples/ruby/v3/main.rb +++ b/all-examples/ruby/v3/main.rb @@ -4,6 +4,9 @@ require 'aws-sdk-kms' require 'json' +# See: https://github.com/ruby/openssl/issues/949 +Aws.use_bundled_cert! + def main # Check command line arguments if ARGV.length != 4 diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index 6a2a7fe0..9946186d 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit 6a2a7fe0418ceebc1b555c0f79b6328896e81939 +Subproject commit 9946186d6b760074750a535b663d6c84c5815308 From 64db72a7aed0485e34bfd50a6fd2582f41eebd1d Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:19:15 -0800 Subject: [PATCH 138/201] chore: add RSA support to Java and Dotnet test servers and a simple RSA roundtrip test (#63) * add RSA support to Java and Dotnet test servers and a simple RSA roundtrip test * Update TestUtils.java * update for net v3 transition * disable net v3 transition * specify classic gcm * commitpol --- .gitignore | 3 + .../amazon/encryption/s3/RoundTripTests.java | 130 ++++++++++++------ .../amazon/encryption/s3/TestUtils.java | 7 + .../s3/CreateClientOperationImpl.java | 19 ++- .../s3/CreateClientOperationImpl.java | 19 ++- .../s3/CreateClientOperationImpl.java | 19 ++- .../Controllers/ClientController.cs | 43 ++++-- .../net-v2-v3-server/Models/ClientRequest.cs | 4 +- .../Controllers/ClientController.cs | 40 ++++-- .../Models/ClientRequest.cs | 4 +- 10 files changed, 210 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 9a2c0f8a..5cd8f239 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ gradle-app.setting .DS_Store smithy-java-core/out + +# test server +*.pid 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 007f36ae..91805ab3 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 @@ -13,6 +13,8 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -174,40 +176,40 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa encCtx.put("user-defined-enc-ctx-key-2", "user-defined-enc-ctx-value-2"); final List mdAsList = metadataMapToList(encCtx); KeyMaterial kmsKeyArn = KeyMaterial.builder() - .kmsKeyId(KMS_KEY_ARN) - .build(); + .kmsKeyId(KMS_KEY_ARN) + .build(); CreateClientOutput encClientOutput = encClient.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()); + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build()) + .build()); String encS3ECId = encClientOutput.getClientId(); encClient.putObject(PutObjectInput.builder() - .clientID(encS3ECId) - .key(objectKey) - .bucket(BUCKET) - .metadata(mdAsList) - .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) - .build()); + .clientID(encS3ECId) + .key(objectKey) + .bucket(BUCKET) + .metadata(mdAsList) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); S3ECTestServerClient decClient = testServerClientFor(decLang); CreateClientOutput decClientOutput = decClient.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()); + .config(S3ECConfig.builder() + .keyMaterial(kmsKeyArn) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .build() + ) + .build()); String decS3ECId = decClientOutput.getClientId(); try { decClient.getObject(GetObjectInput.builder() - .clientID(decS3ECId) - .bucket(BUCKET) - .key(objectKey) - .build()); + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { @@ -275,9 +277,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")); } 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")); } } } @@ -426,15 +428,62 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la "The requested object is encrypted with V1 encryption schemas that have been disabled by client configuration" )); } 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." + 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 { - 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")); } } } + @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") + public void rsaRoundTrip(LanguageServerTarget encLang, LanguageServerTarget decLang) throws Exception { + if (!RAW_SUPPORTED.contains(encLang.getLanguageName())) { + throw new TestAbortedException("not encrypting raw keyrings with: " + encLang.getLanguageName()); + } + if (!RAW_SUPPORTED.contains(decLang.getLanguageName())) { + throw new TestAbortedException("not decrypting raw keyrings with: " + decLang.getLanguageName()); + } + S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); + final String objectKey = appendTestSuffix(String.format("rsa-write-%s-read-%s", encLang.getLanguageName(), decLang.getLanguageName())); + final String input = "simple-test-input-rsa"; + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + + KeyMaterial rsaKeyOne = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(RSA_KEY_PAIR_1.getPrivate().getEncoded())) + .build(); + CreateClientOutput encClientOutput = encClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + // TODO: use this for now to satisfy current. think about long term soln for this + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .keyMaterial(rsaKeyOne).build()) + .build()); + String encS3ECId = encClientOutput.getClientId(); + CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(rsaKeyOne).build()) + .build()); + String decS3ECId = decClientOutput.getClientId(); + encClient.putObject(PutObjectInput.builder() + .clientID(encS3ECId) + .key(objectKey) + .bucket(BUCKET) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); + GetObjectOutput output = decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + assertEquals(input, new String(output.getBody().array())); + } + @ParameterizedTest(name = "{displayName} for Encrypt: Java, Decrypt: {0}") @MethodSource("software.amazon.encryption.s3.TestUtils#clientsForTest") public void instructionFileReadV2Format(TestUtils.LanguageServerTarget language) { @@ -545,16 +594,15 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe if (!encLang.getLanguageName().startsWith("Ruby") && !encLang.getLanguageName().startsWith("PHP")) { // Ruby and PHP do not include it :( 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() - .clientID(decS3ECId) - .bucket(BUCKET) - .key(objectKey) - .build()); + assertFalse(ptInstFile.asUtf8String().isEmpty()); + // Read should be enabled by default + GetObjectOutput output = decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); - assertEquals(input, new String(output.getBody().array())); + assertEquals(input, new String(output.getBody().array())); + } } -} +} \ No newline at end of file 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 26afb9ae..14f9ea6d 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 @@ -96,6 +96,13 @@ public class TestUtils { public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION); + // For now, only .NET and Java have RSA support + 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 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 = 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 1415fd01..1d198590 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 @@ -22,8 +22,11 @@ import java.io.StringWriter; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; import java.util.Arrays; import java.util.Map; import java.util.Optional; @@ -74,13 +77,25 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c key.getRsaKey().get(keyBytes); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + + // Generate public key + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + keyring = RsaKeyring.builder() .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .wrappingKeyPair(PartialRsaKeyPair.builder() - .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .publicKey(publicKey) + .privateKey(privateKey).build()) .build(); } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw new RuntimeException(nse); + throw GenericServerError.builder() + .message(nse.getMessage()) + .build(); } } else if (key.getKmsKeyId() != null) { keyring = KmsKeyring.builder() 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 f874c977..3ef7ee60 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 @@ -22,7 +22,10 @@ import java.io.StringWriter; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Map; import java.util.UUID; @@ -74,13 +77,25 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c key.getRsaKey().get(keyBytes); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + + // Generate public key + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + keyring = RsaKeyring.builder() .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .wrappingKeyPair(PartialRsaKeyPair.builder() - .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .publicKey(publicKey) + .privateKey(privateKey).build()) .build(); } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw new RuntimeException(nse); + throw GenericServerError.builder() + .message(nse.getMessage()) + .build(); } } else if (key.getKmsKeyId() != null) { keyring = KmsKeyring.builder() 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 3607bbf0..ab85df62 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 @@ -22,7 +22,10 @@ import java.io.StringWriter; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Map; import java.util.UUID; @@ -75,13 +78,25 @@ public CreateClientOutput createClient(CreateClientInput input, RequestContext c key.getRsaKey().get(keyBytes); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + + // Generate public key + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + keyring = RsaKeyring.builder() .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) .wrappingKeyPair(PartialRsaKeyPair.builder() - .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .publicKey(publicKey) + .privateKey(privateKey).build()) .build(); } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw new RuntimeException(nse); + throw GenericServerError.builder() + .message(nse.getMessage()) + .build(); } } else if (key.getKmsKeyId() != null) { keyring = KmsKeyring.builder() diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index 01bf610c..7e626e56 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Security.Cryptography; using System.Text.Json; using Amazon.Extensions.S3.Encryption; using Amazon.Extensions.S3.Encryption.Primitives; @@ -19,25 +21,40 @@ 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.RsaKey != null) - return StatusCode(501, new GenericServerError { Message = "RsaKey not supported" }); if (request.Config.KeyMaterial.AesKey != null) return StatusCode(501, new GenericServerError { Message = "AesKey not supported" }); - var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; - var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; - var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; - try { - // 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 encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); - logger.LogInformation( - "Created EncryptionMaterialsV2: KMS={KmsKeyId}", + 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( + "Created EncryptionMaterialsV2: KMS={KmsKeyId}", kmsKeyId); + } + 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; + // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index 6882b4f9..95644524 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -22,7 +22,5 @@ public class KeyMaterial { public byte[]? RsaKey { get; set; } public byte[]? AesKey { get; set; } - - [Required] - public string KmsKeyId { get; set; } = string.Empty; + public string? KmsKeyId { get; set; } } \ 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 16630e9c..0746618e 100644 --- a/test-server/net-v3-transition-server/Controllers/ClientController.cs +++ b/test-server/net-v3-transition-server/Controllers/ClientController.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Security.Cryptography; using System.Text.Json; using Amazon.Extensions.S3.Encryption; using Amazon.Extensions.S3.Encryption.Primitives; @@ -19,30 +21,44 @@ 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.RsaKey != null) - return StatusCode(501, new GenericServerError { Message = "RsaKey not supported" }); if (request.Config.KeyMaterial.AesKey != null) return StatusCode(501, new GenericServerError { Message = "AesKey not supported" }); try { - var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + 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-V3-Transitional] Created EncryptionMaterialsV2: KMS={KmsKeyId}", + kmsKeyId); + } + 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); - // 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 encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); - logger.LogInformation( - "[NET-V3-Transitional] Created EncryptionMaterialsV2: KMS={KmsKeyId}", - kmsKeyId); // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; - logger.LogInformation("[NET-V3-Transitional] Created securityProfile= {securityProfile}", securityProfile.ToString()); var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); diff --git a/test-server/net-v3-transition-server/Models/ClientRequest.cs b/test-server/net-v3-transition-server/Models/ClientRequest.cs index cd5fa406..cd963e53 100644 --- a/test-server/net-v3-transition-server/Models/ClientRequest.cs +++ b/test-server/net-v3-transition-server/Models/ClientRequest.cs @@ -27,9 +27,7 @@ public class KeyMaterial { public byte[]? RsaKey { get; set; } public byte[]? AesKey { get; set; } - - [Required] - public string KmsKeyId { get; set; } = string.Empty; + public string KmsKeyId { get; set; } } [JsonConverter(typeof(JsonStringEnumConverter))] From 34cbbcf5a595e7b6ef519e084f2a09e045cc3302 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:31:33 -0500 Subject: [PATCH 139/201] c++ example (#79) * c++ example --- .github/workflows/examples.yml | 13 +-- all-examples/Makefile | 2 +- all-examples/cpp/CMakeLists.txt | 23 ++++ all-examples/cpp/Makefile | 60 +++++++++++ all-examples/cpp/README.md | 15 +++ all-examples/cpp/main.cpp | 181 ++++++++++++++++++++++++++++++++ 6 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 all-examples/cpp/CMakeLists.txt create mode 100644 all-examples/cpp/Makefile create mode 100644 all-examples/cpp/README.md create mode 100644 all-examples/cpp/main.cpp diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 30e47a06..cecb6989 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -17,23 +17,14 @@ jobs: submodules: true token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - - name: Checkout CPP code for cpp-v2-transition + - name: Checkout CPP code cpp-examples uses: actions/checkout@v5 with: submodules: recursive token: ${{ secrets.PAT_FOR_CPP }} repository: awslabs/aws-sdk-cpp-staging ref: fire-egg-dev - path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ - - - name: Checkout CPP code cpp-v3 - uses: actions/checkout@v5 - with: - submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev - path: test-server/cpp-v3-server/aws-sdk-cpp/ + path: all-examples/cpp/aws-sdk-cpp/ - name: Checkout .NET V2 code uses: actions/checkout@v5 diff --git a/all-examples/Makefile b/all-examples/Makefile index 7dac8ce6..eeb0949b 100644 --- a/all-examples/Makefile +++ b/all-examples/Makefile @@ -5,7 +5,7 @@ .PHONY: all install clean run help list-examples # Find all directories with Makefiles -EXAMPLE_DIRS := $(shell find . -name Makefile -not -path "./Makefile" | xargs dirname | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) +EXAMPLE_DIRS := $(shell find . -name Makefile -not -path "./Makefile" -not -path "./cpp/aws-sdk-cpp/**" -not -path "./cpp/build/**" | xargs dirname | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) all: install diff --git a/all-examples/cpp/CMakeLists.txt b/all-examples/cpp/CMakeLists.txt new file mode 100644 index 00000000..906951b8 --- /dev/null +++ b/all-examples/cpp/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) +project(s3ec-test) + +set(CMAKE_CXX_STANDARD 14) + +# Configure AWS SDK build options +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") + +# Add AWS SDK as subdirectory +add_subdirectory(aws-sdk-cpp) + +find_package(PkgConfig REQUIRED) + +add_executable(s3ec-test main.cpp) + +target_link_libraries(s3ec-test + aws-cpp-sdk-core + aws-cpp-sdk-kms + aws-cpp-sdk-s3 + aws-cpp-sdk-s3-encryption +) diff --git a/all-examples/cpp/Makefile b/all-examples/cpp/Makefile new file mode 100644 index 00000000..64550528 --- /dev/null +++ b/all-examples/cpp/Makefile @@ -0,0 +1,60 @@ +# Makefile for S3 Encryption Client C++ Example + +.PHONY: all install clean run help + +# Default arguments for running the example +# Override these when calling make run +VERSION ?= V3 +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-cpp-test +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: run + +install: build/s3ec-test + +aws-sdk-cpp: + git clone --recurse-submodules -b fire-egg-dev https://github.com/awslabs/aws-sdk-cpp-staging.git aws-sdk-cpp + +build/s3ec-test: aws-sdk-cpp + mkdir -p build && cd build && cmake .. && make + +clean: + rm -rf build + +# Run the example with default arguments +run: build/s3ec-test + @echo "Running S3 Encryption Client C++ example..." + @echo "Version: $(VERSION)" + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + ./build/s3ec-test $(VERSION) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client C++ Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install Go dependencies using Go modules" + @echo " run - Install dependencies and run the example" + @echo " clean - Remove C++ artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " VERSION = $(VERSION) (must be V2 or V3)" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run VERSION=your-version BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Read access to https://github.com/awslabs/aws-sdk-cpp-staging.git" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" diff --git a/all-examples/cpp/README.md b/all-examples/cpp/README.md new file mode 100644 index 00000000..8875fb07 --- /dev/null +++ b/all-examples/cpp/README.md @@ -0,0 +1,15 @@ +# C++ S3 Encryption Test + +Minimal C++ use of S3 Encryption + +## Build + +```bash +make install +``` + +## Run + +```bash +make run +``` diff --git a/all-examples/cpp/main.cpp b/all-examples/cpp/main.cpp new file mode 100644 index 00000000..db8627d7 --- /dev/null +++ b/all-examples/cpp/main.cpp @@ -0,0 +1,181 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Aws::S3Encryption; +using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; + +static Aws::Map get_encryption_context(const char * version) +{ + return { + {"purpose", "example"}, + {"version", version}, + {"language", "c++"} + }; +} + +static int test_v3(const char *bucket, const char *object, const char *kms_key_id, const char *region) +{ + Aws::Client::ClientConfiguration s3ClientConfig; + s3ClientConfig.region = region; + + auto materials = std::make_shared(kms_key_id, s3ClientConfig); + CryptoConfigurationV3 config(materials); + // config.AllowLegacy(); + // config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + // config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + + + auto client = std::make_shared(config, s3ClientConfig); + + auto encryption_context = get_encryption_context("V3"); + + Aws::S3::Model::PutObjectRequest put_request; + put_request.SetBucket(bucket); + put_request.SetKey(object); + + auto data = std::string("This is the sample content."); + + auto stream = std::make_shared(data); + put_request.SetBody(stream); + + auto put_outcome = client->PutObject(put_request, encryption_context); + if (put_outcome.IsSuccess()) + { + fprintf(stderr, "PutObject V3 Successful.\n"); + } + else + { + fprintf(stderr, "PutObject V3 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); + return 1; + } + + Aws::S3::Model::GetObjectRequest get_request; + get_request.SetBucket(bucket); + get_request.SetKey(object); + auto get_outcome = client->GetObject(get_request, encryption_context); + if (get_outcome.IsSuccess()) + { + fprintf(stderr, "GetObject V3 Successful.\n"); + Aws::StringStream response_stream; + response_stream << get_outcome.GetResult().GetBody().rdbuf(); + if (response_stream.str() != data) + { + fprintf(stderr, "GetObject V3 returned the wrong data.\n"); + return 1; + } + } + else + { + fprintf(stderr, "GetObject V3 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); + return 1; + } + return 0; +} + +static int test_v2(const char *bucket, const char *object, const char *kms_key_id, const char *region) +{ + Aws::Client::ClientConfiguration s3ClientConfig; + s3ClientConfig.region = region; + + auto materials = std::make_shared(kms_key_id, s3ClientConfig); + CryptoConfigurationV2 config(materials); + // config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + // config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); + + + auto client = std::make_shared(config, s3ClientConfig); + + auto encryption_context = get_encryption_context("V2"); + + Aws::S3::Model::PutObjectRequest put_request; + put_request.SetBucket(bucket); + put_request.SetKey(object); + + auto data = std::string("This is the sample content."); + + auto stream = std::make_shared(data); + put_request.SetBody(stream); + + auto put_outcome = client->PutObject(put_request, encryption_context); + if (put_outcome.IsSuccess()) + { + fprintf(stderr, "PutObject V2 Successful.\n"); + } + else + { + fprintf(stderr, "PutObject V2 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); + return 1; + } + + Aws::S3::Model::GetObjectRequest get_request; + get_request.SetBucket(bucket); + get_request.SetKey(object); + auto get_outcome = client->GetObject(get_request, encryption_context); + if (get_outcome.IsSuccess()) + { + fprintf(stderr, "GetObject V2 Successful.\n"); + Aws::StringStream response_stream; + response_stream << get_outcome.GetResult().GetBody().rdbuf(); + if (response_stream.str() != data) + { + fprintf(stderr, "GetObject V2 returned the wrong data.\n"); + return 1; + } + } + else + { + fprintf(stderr, "GetObject V2 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); + return 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + if (argc != 6) + { + fprintf(stderr, "USAGE : s3ec-test version bucket object key_id region"); + return 1; + } + + auto version_str = argv[1]; + auto bucket = argv[2]; + auto object = argv[3]; + auto kms_key_id = argv[4]; + auto region = argv[5]; + + bool is_v3; + if (strcasecmp(version_str, "v3") == 0) + { + is_v3 = true; + } + else if (strcasecmp(version_str, "v2") == 0) + { + is_v3 = false; + } + else + { + fprintf(stderr, "Version was <%s> must be V2 or V3\n", version_str); + return 1; + } + + Aws::SDKOptions options; + Aws::InitAPI(options); + + if (is_v3) + test_v3(bucket, object, kms_key_id, region); + else + test_v2(bucket, object, kms_key_id, region); + + Aws::ShutdownAPI(options); +} From ae51a0110e1f4c696093e0955e9f0a094e72cf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:18:27 -0800 Subject: [PATCH 140/201] chore: add php v3 s3ec and php v2 transitional (#45) --------- Co-authored-by: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .../amazon/encryption/s3/RoundTripTests.java | 2 + .../amazon/encryption/s3/TestUtils.java | 4 +- test-server/php-v2-server/local-php-sdk | 2 +- test-server/php-v2-transition-server/Makefile | 4 +- .../php-v2-transition-server/local-php-sdk | 2 +- .../php-v2-transition-server/src/client.php | 8 + .../src/get_object.php | 3 + test-server/php-v3-server/.duvet/config.toml | 15 ++ test-server/php-v3-server/Makefile | 4 +- .../compliance_exceptions/client.txt | 170 ++++++++++++++++++ .../content-metadata-strategy.txt | 34 ++++ .../content-metadata.txt | 50 ++++++ .../compliance_exceptions/decryption.txt | 25 +++ .../compliance_exceptions/encryption.txt | 26 +++ test-server/php-v3-server/local-php-sdk | 2 +- test-server/php-v3-server/src/client.php | 2 + test-server/php-v3-server/src/get_object.php | 15 +- test-server/php-v3-server/src/index.php | 12 +- test-server/php-v3-server/src/put_object.php | 5 +- 20 files changed, 366 insertions(+), 21 deletions(-) create mode 100644 test-server/php-v3-server/compliance_exceptions/client.txt create mode 100644 test-server/php-v3-server/compliance_exceptions/content-metadata-strategy.txt create mode 100644 test-server/php-v3-server/compliance_exceptions/content-metadata.txt create mode 100644 test-server/php-v3-server/compliance_exceptions/decryption.txt create mode 100644 test-server/php-v3-server/compliance_exceptions/encryption.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c29cfb5a..497855ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: token: ${{ secrets.PAT_FOR_DOTNET }} repository: aws/private-amazon-s3-encryption-client-dotnet-staging # This is the branch for S3EC .NET V2 - ref: v3sdk-development + ref: v3sdk-development path: test-server/net-v2-v3-server/s3ec-net-v2/ - name: Checkout .NET V3 code 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 91805ab3..b59a809b 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 @@ -431,6 +431,8 @@ public void kmsV1LegacyFailsWhenLegacyDisabled(TestUtils.LanguageServerTarget la 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."));; } else { assertTrue(e.getMessage().contains("Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms")); } 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 14f9ea6d..a80ddb58 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 @@ -140,7 +140,7 @@ public class TestUtils { // NET_V2_TRANSITION, NET_V3_TRANSITION, CPP_V2_TRANSITION, - // PHP_V2_TRANSITION, + PHP_V2_TRANSITION, RUBY_V2_TRANSITION ); @@ -151,7 +151,7 @@ public class TestUtils { GO_V4, // NET_V4, CPP_V3, - // PHP_V3, + PHP_V3, RUBY_V3 ); diff --git a/test-server/php-v2-server/local-php-sdk b/test-server/php-v2-server/local-php-sdk index d78bd3b2..ab8aee74 160000 --- a/test-server/php-v2-server/local-php-sdk +++ b/test-server/php-v2-server/local-php-sdk @@ -1 +1 @@ -Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 +Subproject commit ab8aee74db1141da07c9c979cf313418fddae256 diff --git a/test-server/php-v2-transition-server/Makefile b/test-server/php-v2-transition-server/Makefile index 536d5cdb..2544679d 100644 --- a/test-server/php-v2-transition-server/Makefile +++ b/test-server/php-v2-transition-server/Makefile @@ -6,13 +6,13 @@ PID_FILE := server.pid PORT := 8099 start-server: - @echo "Starting PHP V2 server..." + @echo "Starting PHP 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" \ composer run start & echo $$! > $(PID_FILE) - @echo "PHP V2 server starting..." + @echo "PHP V2 Transition server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk index d78bd3b2..35a52086 160000 --- a/test-server/php-v2-transition-server/local-php-sdk +++ b/test-server/php-v2-transition-server/local-php-sdk @@ -1 +1 @@ -Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 +Subproject commit 35a52086c5ccf7f5e62e3c17e210923e129c823b diff --git a/test-server/php-v2-transition-server/src/client.php b/test-server/php-v2-transition-server/src/client.php index 9c39d540..534d47a7 100644 --- a/test-server/php-v2-transition-server/src/client.php +++ b/test-server/php-v2-transition-server/src/client.php @@ -19,6 +19,7 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $commitmentPolicy = $configData['commitmentPolicy'] ?? "FORBID_ENCRYPT_ALLOW_DECRYPT"; $instFileConfig = $configData['instructionFileConfig'] ?? null; $instFilePut = false; if ($instFileConfig != null) { @@ -31,6 +32,12 @@ function handleCreateClient() if (($keyMaterial || $kmsKeyId) === null) { return GenericServerError("Invalid keyMaterial in config", 400); } + if ($commitmentPolicy !== "FORBID_ENCRYPT_ALLOW_DECRYPT") { + return GenericServerError( + "Transition server only supports FORBID_ENCRYPT_ALLOW_DECRYPT" + . "commitment policy but received {$commitmentPolicy}" + ); + } // Store client configuration instead of objects (AWS objects can't be serialized) $_SESSION['s3ecCache'][$clientId] = [ @@ -60,6 +67,7 @@ function handleCreateClient() ], 'kmsKeyId' => $kmsKeyId, 'legacy' => $legacyAlgorithms, + 'commitmentPolicy' => $commitmentPolicy, 'instFilePut' => $instFilePut, 'created' => time() ]; 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 41875f54..5800e850 100644 --- a/test-server/php-v2-transition-server/src/get_object.php +++ b/test-server/php-v2-transition-server/src/get_object.php @@ -38,6 +38,7 @@ function handleGetObject($params) } else { $legacy = "V2_AND_LEGACY"; } + $commitmentPolicy = $s3ecClientTuple['config']['commitmentPolicy']; try { // Start output buffering before the AWS call to capture any unwanted output @@ -47,6 +48,7 @@ function handleGetObject($params) '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, + '@CommitmentPolicy' => $commitmentPolicy, 'Bucket' => $bucket, 'Key' => $key, ]); @@ -79,6 +81,7 @@ 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"); } else { + error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server error: " . $e->getMessage(), 500); } } diff --git a/test-server/php-v3-server/.duvet/config.toml b/test-server/php-v3-server/.duvet/config.toml index 64b00927..d7627473 100644 --- a/test-server/php-v3-server/.duvet/config.toml +++ b/test-server/php-v3-server/.duvet/config.toml @@ -6,15 +6,30 @@ pattern = "local-php-sdk/src/S3/**/*.php" [[source]] pattern = "local-php-sdk/src/Crypto/**/*.php" +[[source]] +pattern = "local-php-sdk/tests/S3/**/*.php" + +[[source]] +pattern = "local-php-sdk/tests/Crypto/**/*.php" + +[[source]] +pattern = "compliance_exceptions/*.txt" + # Include required specifications here [[specification]] source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] source = "../specification/s3-encryption/data-format/metadata-strategy.md" [[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-derivation.md" +[[specification]] +source = "../specification/s3-encryption/key-commitment.md" [report.html] enabled = true diff --git a/test-server/php-v3-server/Makefile b/test-server/php-v3-server/Makefile index 7b386f71..0ec40802 100644 --- a/test-server/php-v3-server/Makefile +++ b/test-server/php-v3-server/Makefile @@ -6,13 +6,13 @@ PID_FILE := server.pid PORT := 8093 start-server: - @echo "Starting PHP V2 server..." + @echo "Starting PHP V3 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" \ composer run start & echo $$! > $(PID_FILE) - @echo "PHP V2 server starting..." + @echo "PHP V3 server starting..." stop-server: @if [ -f $(PID_FILE) ]; then \ diff --git a/test-server/php-v3-server/compliance_exceptions/client.txt b/test-server/php-v3-server/compliance_exceptions/client.txt new file mode 100644 index 00000000..0efb20bd --- /dev/null +++ b/test-server/php-v3-server/compliance_exceptions/client.txt @@ -0,0 +1,170 @@ +// +// The PHP V3 implementation is missing the following features: +// +// 1. Client Configuration Options: +// - Legacy algorithm support controls (wrapping algorithms, unauthenticated modes) +// - Uses V3/V3_AND_LEGACY instead +// - Delayed authentication mode configuration +// - Buffer size configuration for memory management +// - Raw keyring material (RSA, AES) +// - SDK client configuration inheritance (credentials, KMS client config) +// - Custom randomness source configuration +// +// 2. Api Operations: +// - DeleteObject and DeleteObjects (with instruction file cleanup) +// - Multipart upload operations (UploadPart, CompleteMultipartUpload, AbortMultipartUpload) +// - ReEncryptInstructionFile for key rotation +// - Non-encryption related S3 operations + +//= ../specification/s3-encryption/client.md#aws-sdk-compatibility +//= type=exception +//# The S3EC SHOULD support invoking operations unrelated to client-side encryption e.g. + +//= ../specification/s3-encryption/client.md#cryptographic-materials +//= type=exception +//# If both a CMM and a Keyring are provided, the S3EC MUST throw an exception. + +//= ../specification/s3-encryption/client.md#cryptographic-materials +//= type=exception +//# When a Keyring is provided, the S3EC MUST create an instance of the DefaultCMM using the provided Keyring. + +//= ../specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms +//= type=exception +//# The option to enable legacy wrapping algorithms MUST be set to false by default. + +//= ../specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes +//= type=exception +//# The S3EC MUST support the option to enable or disable legacy unauthenticated modes (content encryption algorithms). + +//= ../specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes +//= type=exception +//# The option to enable legacy unauthenticated modes MUST be set to false by default. + +//= ../specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes +//= type=exception +//# When enabled, the S3EC MUST be able to decrypt objects encrypted with all content encryption algorithms (both legacy and fully supported). + +//= ../specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes +//= type=exception +//# When disabled, the S3EC MUST NOT decrypt objects encrypted using legacy content encryption algorithms; +//# it MUST throw an exception when attempting to decrypt an object encrypted with a legacy content encryption algorithm. + +//= ../specification/s3-encryption/client.md#enable-delayed-authentication +//= type=exception +//# The S3EC MUST support the option to enable or disable Delayed Authentication mode. + +//= ../specification/s3-encryption/client.md#enable-delayed-authentication +//= type=exception +//# Delayed Authentication mode MUST be set to false by default. + +//= ../specification/s3-encryption/client.md#enable-delayed-authentication +//= type=exception +//# When enabled, the S3EC MAY release plaintext from a stream which has not been authenticated. + +//= ../specification/s3-encryption/client.md#enable-delayed-authentication +//= type=exception +//# When disabled the S3EC MUST NOT release plaintext from a stream which has not been authenticated. + +//= ../specification/s3-encryption/client.md#set-buffer-size +//= type=exception +//# The S3EC SHOULD accept a configurable buffer size which refers to the maximum ciphertext length in bytes to store in memory when Delayed Authentication mode is disabled. + +//= ../specification/s3-encryption/client.md#set-buffer-size +//= type=exception +//# If Delayed Authentication mode is enabled, and the buffer size has been set to a value other than its default, the S3EC MUST throw an exception. + +//= ../specification/s3-encryption/client.md#set-buffer-size +//= type=exception +//# If Delayed Authentication mode is disabled, and no buffer size is provided, the S3EC MUST set the buffer size to a reasonable default. + +//= ../specification/s3-encryption/client.md#cryptographic-materials +//= type=exception +//# The S3EC MAY accept key material directly. + +//= ../specification/s3-encryption/client.md#inherited-sdk-configuration +//= type=exception +//# The S3EC MAY support directly configuring the wrapped SDK clients through its initialization. + +//= ../specification/s3-encryption/client.md#inherited-sdk-configuration +//= type=exception +//# For example, the S3EC MAY accept a credentials provider instance during its initialization. + +//= ../specification/s3-encryption/client.md#inherited-sdk-configuration +//= type=exception +//# If the S3EC accepts SDK client configuration, the configuration MUST be applied to all wrapped S3 clients. + +//= ../specification/s3-encryption/client.md#inherited-sdk-configuration +//= type=exception +//# If the S3EC accepts SDK client configuration, the configuration MUST be applied to all wrapped SDK clients including the KMS client. + +//= ../specification/s3-encryption/client.md#randomness +//= type=exception +//# The S3EC MAY accept a source of randomness during client initialization. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObject MUST be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObject MUST delete the given object key. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObject MUST delete the associated instruction file using the default instruction file suffix. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObjects MUST be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObjects MUST delete each of the given objects. + +//= ../specification/s3-encryption/client.md#required-api-operations +//= type=exception +//# - DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - UploadPart MAY be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - UploadPart MUST encrypt each part. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - Each part MUST be encrypted in sequence. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - Each part MUST be encrypted using the same cipher instance for each part. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - CompleteMultipartUpload MAY be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - CompleteMultipartUpload MUST complete the multipart upload. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - AbortMultipartUpload MAY be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - AbortMultipartUpload MUST abort the multipart upload. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - ReEncryptInstructionFile MAY be implemented by the S3EC. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. + +//= ../specification/s3-encryption/client.md#optional-api-operations +//= type=exception +//# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. diff --git a/test-server/php-v3-server/compliance_exceptions/content-metadata-strategy.txt b/test-server/php-v3-server/compliance_exceptions/content-metadata-strategy.txt new file mode 100644 index 00000000..bb86da72 --- /dev/null +++ b/test-server/php-v3-server/compliance_exceptions/content-metadata-strategy.txt @@ -0,0 +1,34 @@ +// +// The PHP V3 implementation is missing the following features: +// +// 1. METADATA ENCODING: +// - S3 Server "double encoding" support for proper metadata decoding +// +// 2. INSTRUCTION FILE OPERATIONS: +// - Re-encryption/key rotation via instruction files +// - Custom instruction file suffix support for GetObject requests +// + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata +//= type=exception +//# The S3EC SHOULD support decoding the S3 Server's "double encoding". + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata +//= type=exception +//# If the S3EC does not support decoding the S3 Server's "double encoding" then it MUST return the content metadata untouched. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC MAY support re-encryption/key rotation via Instruction Files. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC MUST NOT support providing a custom Instruction File suffix on ordinary writes; custom suffixes MUST only be used during re-encryption. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file +//= type=exception +//# The S3EC SHOULD support providing a custom Instruction File suffix on GetObject requests, regardless of whether or not re-encryption is supported. + +//= ../specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files +//= type=exception +//# - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File. diff --git a/test-server/php-v3-server/compliance_exceptions/content-metadata.txt b/test-server/php-v3-server/compliance_exceptions/content-metadata.txt new file mode 100644 index 00000000..6053a0a6 --- /dev/null +++ b/test-server/php-v3-server/compliance_exceptions/content-metadata.txt @@ -0,0 +1,50 @@ +// +// The PHP V3 implementation is missing the following features: +// +// - Instruction file fallback when object doesn't match V1/V2/V3 formats +// - S3 Server "double encoding" scheme support +// - Writing raw keyring formats (RSA, AES) + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# - The mapkey "x-amz-key" MUST be present for V1 format objects. + +//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys +//= type=exception +//# - The mapkey "x-amz-m" SHOULD be present for V3 format objects that use Raw Keyring Material Description. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# This material description string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# This encryption context string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# - The wrapping algorithm value "02" MUST be translated to AES/GCM upon retrieval, and vice versa on write. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# - The wrapping algorithm value "22" MUST be translated to RSA-OAEP-SHA1 upon retrieval, and vice versa on write. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v1-v2-shared +//= type=exception +//# This string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server. + +//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status +//= type=exception +//# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. + +//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status +//= type=exception +//# If the object matches none of the V1/V2/V3 formats, the S3EC MUST attempt to get the instruction file. + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# The Material Description MUST be used for wrapping algorithms `AES/GCM` (`02`) and `RSA-OAEP-SHA1` (`22`). + +//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only +//= type=exception +//# If the mapkey is not present, the default Material Description value MUST be set to an empty map (`{}`). diff --git a/test-server/php-v3-server/compliance_exceptions/decryption.txt b/test-server/php-v3-server/compliance_exceptions/decryption.txt new file mode 100644 index 00000000..df86d896 --- /dev/null +++ b/test-server/php-v3-server/compliance_exceptions/decryption.txt @@ -0,0 +1,25 @@ +// +// The PHP V3 implementation is missing the following features: +// +// - Support for "range" parameter on GetObject for partial downloads and decryption +// + +//= ../specification/s3-encryption/decryption.md#ranged-gets +//= type=exception +//# The S3EC MAY support the "range" parameter on GetObject which specifies a subset of bytes to download and decrypt. + +//= ../specification/s3-encryption/decryption.md#ranged-gets +//= type=exception +//# If the S3EC supports Ranged Gets, the S3EC MUST adjust the customer-provided range to include the beginning and end of the cipher blocks for the given range. + +//= ../specification/s3-encryption/decryption.md#ranged-gets +//= type=exception +//# If the object was encrypted with ALG_AES_256_GCM_IV12_TAG16_NO_KDF, then ALG_AES_256_CTR_IV16_TAG16_NO_KDF MUST be used to decrypt the range of the object. + +//= ../specification/s3-encryption/decryption.md#ranged-gets +//= type=exception +//# If the object was encrypted with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, then ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY MUST be used to decrypt the range of the object. + +//= ../specification/s3-encryption/decryption.md#ranged-gets +//= type=exception +//# If the GetObject response contains a range, but the GetObject request does not contain a range, the S3EC MUST throw an exception. diff --git a/test-server/php-v3-server/compliance_exceptions/encryption.txt b/test-server/php-v3-server/compliance_exceptions/encryption.txt new file mode 100644 index 00000000..5ae44c91 --- /dev/null +++ b/test-server/php-v3-server/compliance_exceptions/encryption.txt @@ -0,0 +1,26 @@ +// +// The PHP V3 implementation is missing the following features: +// +// - Support for "range" parameter on GetObject for partial downloads and decryption +// +// The PHP V3 implementation has an extra "feature". +// NOTE that using this feature will cause the message to be unable to be decrypted by other language implementations. + +// - Support for AAD during content encryption +// + +//= ../specification/s3-encryption/encryption.md#alg-aes-256-ctr-iv16-tag16-no-kdf +//= type=exception +//# Attempts to encrypt using AES-CTR MUST fail. + +//= ../specification/s3-encryption/encryption.md#alg-aes-256-ctr-hkdf-sha512-commit-key +//= type=exception +//# Attempts to encrypt using key committing AES-CTR MUST fail. + +//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-iv12-tag16-no-kdf +//= type=exception +//# The client MUST NOT provide any AAD when encrypting with ALG_AES_256_GCM_IV12_TAG16_NO_KDF. + +//= ../specification/s3-encryption/encryption.md#cipher-initialization +//= type=exception +//# The client SHOULD validate that the generated IV or Message ID is not zeros. diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index d78bd3b2..35a52086 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit d78bd3b221890aac679ec3b6cb5abcb01fd42699 +Subproject commit 35a52086c5ccf7f5e62e3c17e210923e129c823b diff --git a/test-server/php-v3-server/src/client.php b/test-server/php-v3-server/src/client.php index 2c6204bb..f57c643a 100644 --- a/test-server/php-v3-server/src/client.php +++ b/test-server/php-v3-server/src/client.php @@ -19,6 +19,7 @@ function handleCreateClient() $legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false; $clientId = Uuid::uuid4()->toString(); $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; + $commitmentPolicy = $configData['commitmentPolicy'] ?? "REQUIRE_ENCRYPT_REQUIRE_DECRYPT"; $instFileConfig = $configData['instructionFileConfig'] ?? null; $instFilePut = false; if ($instFileConfig != null) { @@ -61,6 +62,7 @@ function handleCreateClient() ], 'kmsKeyId' => $kmsKeyId, 'legacy' => $legacyAlgorithms, + 'commitmentPolicy' => $commitmentPolicy, 'instFilePut' => $instFilePut, 'created' => time() ]; diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php index 59e2192c..3de7f779 100644 --- a/test-server/php-v3-server/src/get_object.php +++ b/test-server/php-v3-server/src/get_object.php @@ -34,10 +34,11 @@ function handleGetObject($params) $legacyConfig = $clientConfig["legacy"] ?? false; $legacy = null; if ($legacyConfig === false) { - $legacy = "V2"; + $legacy = "V3"; } else { - $legacy = "V2_AND_LEGACY"; + $legacy = "V3_AND_LEGACY"; } + $commitmentPolicy = $s3ecClientTuple['config']['commitmentPolicy']; try { // Start output buffering before the AWS call to capture any unwanted output @@ -47,6 +48,7 @@ function handleGetObject($params) '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, '@KmsEncryptionContext' => $encryptionContext, + '@CommitmentPolicy' => $commitmentPolicy, 'Bucket' => $bucket, 'Key' => $key, ]); @@ -76,9 +78,14 @@ function handleGetObject($params) if (ob_get_level()) { ob_end_clean(); } - if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { - return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); + if (strpos($e->getMessage(), "@SecurityProfile=V3") !== false) { + return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Provided encryption context does not match information retrieved from S3") !== false) { + 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()); } else { + error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server argument: " . $e->getMessage(), 500); } } diff --git a/test-server/php-v3-server/src/index.php b/test-server/php-v3-server/src/index.php index 167834e0..f5f5cdb5 100644 --- a/test-server/php-v3-server/src/index.php +++ b/test-server/php-v3-server/src/index.php @@ -5,8 +5,8 @@ require_once __DIR__ . '/get_object.php'; require_once __DIR__ . '/put_object.php'; -use Aws\S3\Crypto\S3EncryptionClientV2; -use Aws\Crypto\KmsMaterialsProviderV2; +use Aws\S3\Crypto\S3EncryptionClientV3; +use Aws\Crypto\KmsMaterialsProviderV3; use Aws\S3\S3Client; use Aws\Kms\KmsClient; @@ -157,10 +157,10 @@ function getCachedClient($clientId) // Recreate the AWS clients from stored configuration $s3Client = new S3Client($config['s3Config']); - $encryptionClient = new S3EncryptionClientV2($s3Client); + $encryptionClient = new S3EncryptionClientV3($s3Client); $kmsClient = new KmsClient($config['kmsConfig']); - $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); + $materialsProvider = new KmsMaterialsProviderV3($kmsClient, $config['kmsKeyId']); return [ 's3Client' => $s3Client, @@ -184,7 +184,7 @@ function createDefaultClientTuple(): array ] ] ]); - $encryptionClient = new S3EncryptionClientV2($s3Client); + $encryptionClient = new S3EncryptionClientV3($s3Client); $kmsClient = new KmsClient([ 'region' => 'us-west-2', @@ -198,7 +198,7 @@ function createDefaultClientTuple(): array ] ] ]); - $materialsProvider = new KmsMaterialsProviderV2($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); + $materialsProvider = new KmsMaterialsProviderV3($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); return [ 'encryptionClient' => $encryptionClient, diff --git a/test-server/php-v3-server/src/put_object.php b/test-server/php-v3-server/src/put_object.php index 9cad796b..2f882b1e 100644 --- a/test-server/php-v3-server/src/put_object.php +++ b/test-server/php-v3-server/src/put_object.php @@ -31,7 +31,7 @@ function handlePutObject($params) $key = $params['key'] ?? null; if (is_null($bucket) || is_null($key)) { - return GenericServerError("Invalidb bucket or key parameters", 400); + return GenericServerError("Invalid bucket or key parameters", 400); } $s3Client = $s3ecClientTuple["s3Client"]; @@ -48,13 +48,16 @@ function handlePutObject($params) } else { $legacy = "V2_AND_LEGACY"; } + $commitmentPolicy = $s3ecClientTuple['config']['commitmentPolicy']; $strategy = $s3ecClientTuple["config"]["instFilePut"] ? new InstructionFileMetadataStrategy($s3Client) : new HeadersMetadataStrategy(); + try { $result = $s3ec->putObject([ '@SecurityProfile' => $legacy, '@MaterialsProvider' => $materialProvider, + '@CommitmentPolicy' => $commitmentPolicy, '@KmsEncryptionContext' => $encryptionContext, '@MetadataStrategy' => $strategy, '@CipherOptions' => $cipherOptions, From 79a6d46b595fcbc541b3f9afad8f227880bb0ac3 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Tue, 11 Nov 2025 15:13:55 -0800 Subject: [PATCH 141/201] chore: Run raw keyring test for transitional version (#80) * auto commit * auto commit --------- Co-authored-by: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> --- .../src/it/java/software/amazon/encryption/s3/TestUtils.java | 3 +-- test-server/net-v3-transition-server/Models/ClientRequest.cs | 2 +- 2 files changed, 2 insertions(+), 3 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 a80ddb58..0a4284bb 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,8 +99,7 @@ public class TestUtils { // For now, only .NET and Java have RSA support 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_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION ); // .NET only supports decrypting instruction files using AES and RSA. diff --git a/test-server/net-v3-transition-server/Models/ClientRequest.cs b/test-server/net-v3-transition-server/Models/ClientRequest.cs index cd963e53..3c80fc59 100644 --- a/test-server/net-v3-transition-server/Models/ClientRequest.cs +++ b/test-server/net-v3-transition-server/Models/ClientRequest.cs @@ -27,7 +27,7 @@ public class KeyMaterial { public byte[]? RsaKey { get; set; } public byte[]? AesKey { get; set; } - public string KmsKeyId { get; set; } + public string? KmsKeyId { get; set; } } [JsonConverter(typeof(JsonStringEnumConverter))] From 99cafcf2c5dcd7d76d41026b6edc8cf134847d8d Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Tue, 11 Nov 2025 23:36:13 -0800 Subject: [PATCH 142/201] chore: add .NET improved server (#54) --- .github/workflows/test.yml | 9 ++ .gitmodules | 4 + .../amazon/encryption/s3/RoundTripTests.java | 2 +- .../amazon/encryption/s3/TestUtils.java | 7 +- test-server/net-v4-server/.duvet/.gitignore | 3 + test-server/net-v4-server/.duvet/config.toml | 27 +++++ test-server/net-v4-server/.gitignore | 44 +++++++ .../Controllers/ClientController.cs | 113 ++++++++++++++++++ .../Controllers/ObjectController.cs | 105 ++++++++++++++++ test-server/net-v4-server/Makefile | 36 ++++++ .../net-v4-server/Models/ClientRequest.cs | 49 ++++++++ .../net-v4-server/Models/ClientResponse.cs | 8 ++ .../net-v4-server/Models/ErrorModels.cs | 17 +++ test-server/net-v4-server/NetV4Server.csproj | 28 +++++ test-server/net-v4-server/Program.cs | 17 +++ test-server/net-v4-server/README.md | 72 +++++++++++ .../Services/ClientCacheService.cs | 28 +++++ .../net-v4-server/s3ec-net-v4-improved | 1 + 18 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 test-server/net-v4-server/.duvet/.gitignore create mode 100644 test-server/net-v4-server/.duvet/config.toml create mode 100644 test-server/net-v4-server/.gitignore create mode 100644 test-server/net-v4-server/Controllers/ClientController.cs create mode 100644 test-server/net-v4-server/Controllers/ObjectController.cs create mode 100644 test-server/net-v4-server/Makefile create mode 100644 test-server/net-v4-server/Models/ClientRequest.cs create mode 100644 test-server/net-v4-server/Models/ClientResponse.cs create mode 100644 test-server/net-v4-server/Models/ErrorModels.cs create mode 100644 test-server/net-v4-server/NetV4Server.csproj create mode 100644 test-server/net-v4-server/Program.cs create mode 100644 test-server/net-v4-server/README.md create mode 100644 test-server/net-v4-server/Services/ClientCacheService.cs create mode 160000 test-server/net-v4-server/s3ec-net-v4-improved diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 497855ca..23cee4d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,6 +72,15 @@ jobs: ref: rishav/key-commitment path: test-server/net-v3-transition-server/s3ec-v3-transition-branch + - name: Checkout .NET V4 (Improved) code + uses: actions/checkout@v5 + with: + token: ${{ secrets.PAT_FOR_DOTNET }} + repository: aws/private-amazon-s3-encryption-client-dotnet-staging + # This is the branch for S3EC .NET V4 (improved) + ref: s3ec-v4-WIP + path: test-server/net-v4-server/s3ec-net-v4-improved + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.gitmodules b/.gitmodules index e3080fcf..2d5c9a95 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,6 +44,10 @@ path = test-server/net-v2-v3-server/s3ec-net-v3 url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git branch = s3ec-v3 +[submodule "test-server/net-v4-server/s3ec-net-v4-improved"] + path = test-server/net-v4-server/s3ec-net-v4-improved + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = s3ec-v4-WIP [submodule "test-server/go-v3-transition-server/local-go-s3ec"] path = test-server/go-v3-transition-server/local-go-s3ec url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging 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 b59a809b..0aa6d611 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 @@ -422,7 +422,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) + if (language.getLanguageName().equals(NET_V3_CURRENT) || language.getLanguageName().equals(NET_V2_CURRENT) || 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 0a4284bb..df346319 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 @@ -91,10 +91,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); + Set.of(GO_V3_CURRENT, PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3, NET_V2_CURRENT, NET_V3_CURRENT, 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); + 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 = @@ -148,7 +148,7 @@ public class TestUtils { JAVA_V4, // PYTHON_V3, GO_V4, - // NET_V4, + NET_V4, CPP_V3, PHP_V3, RUBY_V3 @@ -169,6 +169,7 @@ public class TestUtils { // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); + servers.put(NET_V4, new LanguageServerTarget(NET_V4, "8090")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers diff --git a/test-server/net-v4-server/.duvet/.gitignore b/test-server/net-v4-server/.duvet/.gitignore new file mode 100644 index 00000000..93956e36 --- /dev/null +++ b/test-server/net-v4-server/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/test-server/net-v4-server/.duvet/config.toml b/test-server/net-v4-server/.duvet/config.toml new file mode 100644 index 00000000..0548b05c --- /dev/null +++ b/test-server/net-v4-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 \ No newline at end of file diff --git a/test-server/net-v4-server/.gitignore b/test-server/net-v4-server/.gitignore new file mode 100644 index 00000000..4c20cbc8 --- /dev/null +++ b/test-server/net-v4-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-v4-server/Controllers/ClientController.cs b/test-server/net-v4-server/Controllers/ClientController.cs new file mode 100644 index 00000000..9e9ae66e --- /dev/null +++ b/test-server/net-v4-server/Controllers/ClientController.cs @@ -0,0 +1,113 @@ +using System.Text.Json; +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Microsoft.AspNetCore.Mvc; +using NetV4Server.Models; +using NetV4Server.Services; + +namespace NetV4Server.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 ?? false) + 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.RsaKey != null) + return StatusCode(501, new GenericServerError { Message = "[NET-V4] RsaKey not supported" }); + if (request.Config.KeyMaterial.AesKey != null) + return StatusCode(501, new GenericServerError { Message = "[NET-V4] AesKey not supported" }); + + try + { + var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes ?? false; + var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms ?? false; + var commitmentPolicy = MapCommitmentPolicy(request.Config.CommitmentPolicy); + var isSecurityProfileProvided = request.Config.EnableLegacyUnauthenticatedModes.HasValue || request.Config.EnableLegacyWrappingAlgorithms.HasValue; + var isCommitmentPolicyProvided = request.Config.CommitmentPolicy.HasValue; + var useDefaultConf = !isCommitmentPolicyProvided; + + logger.LogInformation("[NET-V4] isSecurityProfileProvided: {isSecurityProfileProvided}, isCommitmentPolicyProvided: {isCommitmentPolicyProvided}, useDefaultConf: {useDefaultConf}", isSecurityProfileProvided, isCommitmentPolicyProvided, useDefaultConf); + + // 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 encryptionMaterial = new EncryptionMaterialsV4(kmsKeyId, KmsType.KmsContext, encryptionContext); + logger.LogInformation( + "[NET-V4] Created EncryptionMaterialsV4: KMS={KmsKeyId}", + kmsKeyId); + + // SecurityProfile V4AndLegacy can decrypt from legacy S3EC but V4 cannot + var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; + var securityProfile = enableLegacyMode ? SecurityProfile.V4AndLegacy : SecurityProfile.V4; + + var encryptionAlgorithm = MapEncryptionAlgorithm(request.Config.EncryptionAlgorithm); + + if (!useDefaultConf) + { + logger.LogInformation("[NET-V4] Created securityProfile= {securityProfile}", securityProfile.ToString()); + logger.LogInformation("[NET-V4] Created commitmentPolicy= {commitmentPolicy}", commitmentPolicy); + logger.LogInformation("[NET-V4] Created encryptionAlgorithm= {encryptionAlgorithm}", encryptionAlgorithm); + } else + { + logger.LogInformation("[NET-V4] Using default configuration for securityProfile, commitmentPolicy and encryptionAlgorithm"); + } + + var configuration = useDefaultConf + ? new AmazonS3CryptoConfigurationV4() + : new AmazonS3CryptoConfigurationV4(securityProfile, commitmentPolicy, encryptionAlgorithm); + + // Create S3 encryption client + var encryptionClient = new AmazonS3EncryptionClientV4(configuration, encryptionMaterial); + // Add to cache and return client ID + var clientId = clientCacheService.AddClient(encryptionClient); + var response = new ClientResponse { ClientId = clientId }; + + logger.LogInformation("[NET-V4] 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-V4] Failed to create S3EC client"); + return StatusCode(500, new S3EncryptionClientError + { + Message = $"[NET-V4] 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.RequireEncryptRequireDecrypt + }; + } + + 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.AesGcmWithCommitment + }; + } +} \ No newline at end of file diff --git a/test-server/net-v4-server/Controllers/ObjectController.cs b/test-server/net-v4-server/Controllers/ObjectController.cs new file mode 100644 index 00000000..7ebd8fd1 --- /dev/null +++ b/test-server/net-v4-server/Controllers/ObjectController.cs @@ -0,0 +1,105 @@ +using System.Text.Json; +using Amazon.S3.Model; +using Microsoft.AspNetCore.Mvc; +using NetV4Server.Models; +using NetV4Server.Services; + +namespace NetV4Server.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-V4] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V4] 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-V4] 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-V4] Failed to put object from S3 for bucket={bucket}, key={key}", bucket, key); + return StatusCode(500, new S3EncryptionClientError { Message = $"Failed to put object: {ex.Message}" }); + } + } + + [HttpGet("{bucket}/{key}")] + public async Task GetObject(string bucket, string key) + { + logger.LogInformation("[NET-V4] Starting GetObject"); + var clientId = Request.Headers["clientId"].FirstOrDefault(); + if (string.IsNullOrEmpty(clientId)) + return BadRequest(new GenericServerError { Message = "[NET-V4] ClientID header is required" }); + + var client = clientCacheService.GetClient(clientId); + if (client == null) + return NotFound(new GenericServerError { Message = $"[NET-V4] No client found for ClientID: {clientId}" }); + + try + { + var getRequest = new GetObjectRequest + { + BucketName = bucket, + Key = key + }; + var response = await client.GetObjectAsync(getRequest); + logger.LogInformation("[NET-V4] 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-V4] 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-v4-server/Makefile b/test-server/net-v4-server/Makefile new file mode 100644 index 00000000..49e4db32 --- /dev/null +++ b/test-server/net-v4-server/Makefile @@ -0,0 +1,36 @@ +# Makefile for S3 Encryption Client .NET Testing + +.PHONY: start-server stop-server wait-for-server + +PID_FILE_NET_V4 := net-V4-server.pid +PORT_NET_V4 := 8090 + +start-server: + $(MAKE) start-net-V4-server; + +stop-server: + @if [ -f $(PID_FILE_NET_V4) ]; then \ + kill $$(cat $(PID_FILE_NET_V4)) 2>/dev/null || true; \ + rm $(PID_FILE_NET_V4); \ + fi + +# Start .NET V4 server in background +# This builds first into bin/V4 and runs through dll +# to avoid simultaneous dotnet run conflict +start-net-V4-server: + @echo "Starting .NET V4 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 & echo $! > net-v3-transition-server.pid + @echo ".NET V4 server starting..." + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V4) + +duvet: + duvet report + +view-report-mac: + open .duvet/reports/report.html diff --git a/test-server/net-v4-server/Models/ClientRequest.cs b/test-server/net-v4-server/Models/ClientRequest.cs new file mode 100644 index 00000000..52dd0317 --- /dev/null +++ b/test-server/net-v4-server/Models/ClientRequest.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace NetV4Server.Models; + +public class ClientRequest +{ + [Required] + public ClientConfig Config { get; set; } = new(); +} + +public class ClientConfig +{ + public bool? EnableLegacyUnauthenticatedModes { get; set; } + public bool? EnableLegacyWrappingAlgorithms { get; set; } + public bool? EnableDelayedAuthenticationMode { get; set; } + 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 class KeyMaterial +{ + public byte[]? RsaKey { get; set; } + public byte[]? AesKey { get; set; } + + [Required] + public string KmsKeyId { get; set; } = string.Empty; +} + +[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 +} \ No newline at end of file diff --git a/test-server/net-v4-server/Models/ClientResponse.cs b/test-server/net-v4-server/Models/ClientResponse.cs new file mode 100644 index 00000000..b4dbb494 --- /dev/null +++ b/test-server/net-v4-server/Models/ClientResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace NetV4Server.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-v4-server/Models/ErrorModels.cs b/test-server/net-v4-server/Models/ErrorModels.cs new file mode 100644 index 00000000..e4b818e3 --- /dev/null +++ b/test-server/net-v4-server/Models/ErrorModels.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace NetV4Server.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-v4-server/NetV4Server.csproj b/test-server/net-v4-server/NetV4Server.csproj new file mode 100644 index 00000000..28ddba06 --- /dev/null +++ b/test-server/net-v4-server/NetV4Server.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + NetV2V3Server + + + + false + + + + + + + + + + + + + + + + diff --git a/test-server/net-v4-server/Program.cs b/test-server/net-v4-server/Program.cs new file mode 100644 index 00000000..23cf79d9 --- /dev/null +++ b/test-server/net-v4-server/Program.cs @@ -0,0 +1,17 @@ +using NetV4Server.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddSingleton(); + +const int port = 8090; + +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-v4-server/README.md b/test-server/net-v4-server/README.md new file mode 100644 index 00000000..dd5a6753 --- /dev/null +++ b/test-server/net-v4-server/README.md @@ -0,0 +1,72 @@ +# Net-V2-V3-Server + +A .NET test server for Amazon S3 encryption client .NET v2 and v3. + +## Project Structure + +``` +net-v2-v3-server/ +├── Controllers/ # API controllers +├── Models/ # Data models +├── Services/ # Business logic services +├── Program.cs # Application entry point +├── NetV2V3Server.csproj # Project file +└── README.md # This file +``` + +## Running the Server + +For S3 Encryption Client v2 (runs on port 8083): + +```bash +dotnet run -p:S3EncryptionVersion=v2 +``` + +For S3 Encryption Client v3 (runs on port 8084): + +```bash +dotnet run -p:S3EncryptionVersion=v3 +``` + +## 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":{"keyMaterial":{"kmsKeyId":"arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key"}, "encryptionContext": {"abc": "b"}, "CommitmentPolicy":"FORBID_ENCRYPT_ALLOW_DECRYPT"}}' \ + http://localhost:8090/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-v4-server/Services/ClientCacheService.cs b/test-server/net-v4-server/Services/ClientCacheService.cs new file mode 100644 index 00000000..55764152 --- /dev/null +++ b/test-server/net-v4-server/Services/ClientCacheService.cs @@ -0,0 +1,28 @@ +using Amazon.Extensions.S3.Encryption; +using System.Collections.Concurrent; + +namespace NetV4Server.Services; + +public interface IClientCacheService +{ + string AddClient(AmazonS3EncryptionClientV4 client); + AmazonS3EncryptionClientV4? GetClient(string clientId); +} + +public class ClientCacheService : IClientCacheService +{ + private readonly ConcurrentDictionary _clients = new(); + + public string AddClient(AmazonS3EncryptionClientV4 client) + { + var clientId = Guid.NewGuid().ToString(); + _clients[clientId] = client; + return clientId; + } + + public AmazonS3EncryptionClientV4? GetClient(string clientId) + { + _clients.TryGetValue(clientId, out var client); + return client; + } +} diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved new file mode 160000 index 00000000..ebbc5d84 --- /dev/null +++ b/test-server/net-v4-server/s3ec-net-v4-improved @@ -0,0 +1 @@ +Subproject commit ebbc5d849371fe5d7f5f2dfc2d8f772458f7fcd8 From a2874888de0fe1d22aef8682d12927ba625cacf9 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Wed, 12 Nov 2025 12:38:10 -0800 Subject: [PATCH 143/201] chore: turn on raw aes for improved NET (#83) --- .../amazon/encryption/s3/RoundTripTests.java | 2 + .../amazon/encryption/s3/TestUtils.java | 2 +- .../Controllers/ClientController.cs | 38 +++++++++++++------ .../net-v4-server/Models/ClientRequest.cs | 4 +- .../net-v4-server/s3ec-net-v4-improved | 2 +- 5 files changed, 31 insertions(+), 17 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 0aa6d611..1167e4db 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 @@ -469,6 +469,8 @@ public void rsaRoundTrip(LanguageServerTarget encLang, LanguageServerTarget decL String encS3ECId = encClientOutput.getClientId(); CreateClientOutput decClientOutput = decClient.createClient(CreateClientInput.builder() .config(S3ECConfig.builder() + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) .keyMaterial(rsaKeyOne).build()) .build()); String decS3ECId = decClientOutput.getClientId(); 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 df346319..c20fd5a6 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,7 +99,7 @@ public class TestUtils { // For now, only .NET and Java have RSA support 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_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4 ); // .NET only supports decrypting instruction files using AES and RSA. diff --git a/test-server/net-v4-server/Controllers/ClientController.cs b/test-server/net-v4-server/Controllers/ClientController.cs index 9e9ae66e..5298d758 100644 --- a/test-server/net-v4-server/Controllers/ClientController.cs +++ b/test-server/net-v4-server/Controllers/ClientController.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using System.Text.Json; using Amazon.Extensions.S3.Encryption; using Amazon.Extensions.S3.Encryption.Primitives; @@ -19,14 +20,36 @@ 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.RsaKey != null) - return StatusCode(501, new GenericServerError { Message = "[NET-V4] RsaKey not supported" }); if (request.Config.KeyMaterial.AesKey != null) return StatusCode(501, new GenericServerError { Message = "[NET-V4] AesKey not supported" }); try { - var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; + EncryptionMaterialsV4 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 EncryptionMaterialsV4(kmsKeyId, KmsType.KmsContext, encryptionContext); + logger.LogInformation( + "[NET-V4] Created EncryptionMaterialsV4: KMS={KmsKeyId}", + kmsKeyId); + } + 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 EncryptionMaterialsV4(rsaKey, AsymmetricAlgorithmType.RsaOaepSha1); + logger.LogInformation( + "[NET-V4] Created EncryptionMaterialsV4: RSA"); + } else + { + return StatusCode(501, new GenericServerError { Message = "[NET-V4] Unknown or missing key material!" }); + } var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes ?? false; var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms ?? false; var commitmentPolicy = MapCommitmentPolicy(request.Config.CommitmentPolicy); @@ -36,15 +59,6 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation("[NET-V4] isSecurityProfileProvided: {isSecurityProfileProvided}, isCommitmentPolicyProvided: {isCommitmentPolicyProvided}, useDefaultConf: {useDefaultConf}", isSecurityProfileProvided, isCommitmentPolicyProvided, useDefaultConf); - // 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 encryptionMaterial = new EncryptionMaterialsV4(kmsKeyId, KmsType.KmsContext, encryptionContext); - logger.LogInformation( - "[NET-V4] Created EncryptionMaterialsV4: KMS={KmsKeyId}", - kmsKeyId); - // SecurityProfile V4AndLegacy can decrypt from legacy S3EC but V4 cannot var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V4AndLegacy : SecurityProfile.V4; diff --git a/test-server/net-v4-server/Models/ClientRequest.cs b/test-server/net-v4-server/Models/ClientRequest.cs index 52dd0317..a5eff6f7 100644 --- a/test-server/net-v4-server/Models/ClientRequest.cs +++ b/test-server/net-v4-server/Models/ClientRequest.cs @@ -27,9 +27,7 @@ public class KeyMaterial { public byte[]? RsaKey { get; set; } public byte[]? AesKey { get; set; } - - [Required] - public string KmsKeyId { get; set; } = string.Empty; + public string? KmsKeyId { get; set; } } [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index ebbc5d84..691d22a5 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 ebbc5d849371fe5d7f5f2dfc2d8f772458f7fcd8 +Subproject commit 691d22a504184fd71f2dae7fd354bd669b58cc07 From 3fdf5e5094386d2a501ab65d64489f408ed71b01 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Wed, 12 Nov 2025 13:36:10 -0800 Subject: [PATCH 144/201] chore: Add Go V3 example (#77) --- all-examples/go/v3/Makefile | 69 +++++++++++++ all-examples/go/v3/README.md | 55 ++++++++++ all-examples/go/v3/go.mod | 32 ++++++ all-examples/go/v3/go.sum | 40 ++++++++ all-examples/go/v3/local-go-s3ec | 1 + all-examples/go/v3/main.go | 171 +++++++++++++++++++++++++++++++ 6 files changed, 368 insertions(+) create mode 100644 all-examples/go/v3/Makefile create mode 100644 all-examples/go/v3/README.md create mode 100644 all-examples/go/v3/go.mod create mode 100644 all-examples/go/v3/go.sum create mode 120000 all-examples/go/v3/local-go-s3ec create mode 100644 all-examples/go/v3/main.go diff --git a/all-examples/go/v3/Makefile b/all-examples/go/v3/Makefile new file mode 100644 index 00000000..d7285fe9 --- /dev/null +++ b/all-examples/go/v3/Makefile @@ -0,0 +1,69 @@ +# Makefile for S3 Encryption Client Go v3 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.go + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-go-v3 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Go modules +install: + @echo "Installing Go dependencies..." + @go mod tidy + @echo "Dependencies installed successfully!" + +# Clean Go artifacts +clean: + @echo "Cleaning Go artifacts..." + @go clean + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v3 Go example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client Go v3 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install Go dependencies using Go modules" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove Go artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Go 1.24+ installed on the system" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v3 Go SDK (included in local-go-s3ec)" diff --git a/all-examples/go/v3/README.md b/all-examples/go/v3/README.md new file mode 100644 index 00000000..519dfc36 --- /dev/null +++ b/all-examples/go/v3/README.md @@ -0,0 +1,55 @@ +# S3 Encryption Client Go v3 Example + +This example demonstrates how to use the Amazon S3 Encryption Client v3 for Go to perform client-side encryption and decryption of objects. + +## Prerequisites + +1. **Go**: Requires Go 1.24 or later +2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: + - AWS CLI: `aws configure` + - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + - IAM roles (for EC2 instances) +3. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` +4. **S3 Bucket**: An existing S3 bucket where you have read/write permissions + +## Setup + +1. Initialize submodules and download dependencies: + ```bash + make install + ``` + + Or manually: + ```bash + go mod tidy + ``` + + **Note**: This example uses a local submodule for the S3EC Go v3 library via the `replace` directive in `go.mod`. + +## Usage + +### Using Make (Recommended) + +Run the example with default parameters: +```bash +make run +``` + +Run with custom parameters: +```bash +make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +``` + +### Manual Usage + +Run the example with the following command: + +```bash +go run main.go +``` + +### Example: + +```bash +go run main.go my-test-bucket s3ec-go-v3-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2 +``` diff --git a/all-examples/go/v3/go.mod b/all-examples/go/v3/go.mod new file mode 100644 index 00000000..1821569a --- /dev/null +++ b/all-examples/go/v3/go.mod @@ -0,0 +1,32 @@ +module github.com/aws/amazon-s3-encryption-client-python/all-examples/go/v3 + +go 1.24 + +require ( + github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) + +// S3EC Go V3 uses a local submodule for development +replace github.com/aws/amazon-s3-encryption-client-go/v3 => ./local-go-s3ec/v3 diff --git a/all-examples/go/v3/go.sum b/all-examples/go/v3/go.sum new file mode 100644 index 00000000..244c8814 --- /dev/null +++ b/all-examples/go/v3/go.sum @@ -0,0 +1,40 @@ +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/all-examples/go/v3/local-go-s3ec b/all-examples/go/v3/local-go-s3ec new file mode 120000 index 00000000..7e5e770c --- /dev/null +++ b/all-examples/go/v3/local-go-s3ec @@ -0,0 +1 @@ +../../../test-server/go-v3-transition-server/local-go-s3ec \ No newline at end of file diff --git a/all-examples/go/v3/main.go b/all-examples/go/v3/main.go new file mode 100644 index 00000000..22732bc8 --- /dev/null +++ b/all-examples/go/v3/main.go @@ -0,0 +1,171 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/aws/amazon-s3-encryption-client-go/v3/client" + "github.com/aws/amazon-s3-encryption-client-go/v3/commitment" + "github.com/aws/amazon-s3-encryption-client-go/v3/materials" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func main() { + // Check command line arguments + if len(os.Args) != 5 { + fmt.Printf("Usage: %s \n", os.Args[0]) + fmt.Printf("Example: %s avp-21638 s3ec-go-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n", os.Args[0]) + os.Exit(1) + } + + bucketName := os.Args[1] + objectKey := os.Args[2] + kmsKeyID := os.Args[3] + region := os.Args[4] + + fmt.Println("=== S3 Encryption Client v3 Example (Go) ===") + fmt.Printf("Bucket: %s\n", bucketName) + fmt.Printf("Object Key: %s\n", objectKey) + fmt.Printf("KMS Key ID: %s\n", kmsKeyID) + fmt.Printf("Region: %s\n", region) + fmt.Println() + + // Test data for encryption + testData := "Hello, World! This is a test message for S3 encryption client v3 in Go." + fmt.Printf("Original data: %s\n", testData) + fmt.Printf("Data length: %d bytes\n", len(testData)) + fmt.Println() + + fmt.Println("--- Initialize S3 Encryption Client v3 ---") + + // Create regular S3 client + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + fmt.Printf("Error loading AWS config: %v\n", err) + os.Exit(1) + } + s3Client := s3.NewFromConfig(cfg) + + // Create KMS client + kmsClient := kms.NewFromConfig(cfg) + + // Create KMS keyring + keyring := materials.NewKmsKeyring(kmsClient, kmsKeyID) + + // Create Cryptographic Materials Manager + cmm, err := materials.NewCryptographicMaterialsManager(keyring) + if err != nil { + fmt.Printf("Error creating CMM: %v\n", err) + os.Exit(1) + } + + // Create S3 Encryption Client v3 + encryptionClient, err := client.New(s3Client, cmm, func(options *client.EncryptionClientOptions) { + options.CommitmentPolicy = commitment.FORBID_ENCRYPT_ALLOW_DECRYPT + }) + if err != nil { + fmt.Printf("Error creating S3 Encryption Client: %v\n", err) + os.Exit(1) + } + + fmt.Println("Successfully initialized S3 Encryption Client v3") + fmt.Println("--- Encrypt and Upload Object to S3 ---") + + // Add encryption context + encryptionContext := map[string]string{ + "purpose": "example", + "version": "v3", + "language": "go", + } + + // Create context with encryption context + ctx := context.WithValue(context.Background(), "EncryptionContext", encryptionContext) + + // Upload encrypted object using S3 Encryption Client + putInput := &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + Body: strings.NewReader(testData), + } + + _, err = encryptionClient.PutObject(ctx, putInput) + if err != nil { + if strings.Contains(err.Error(), "NoSuchBucket") { + fmt.Printf("Error: S3 bucket '%s' does not exist or is not accessible\n", bucketName) + } else if strings.Contains(err.Error(), "NotFoundException") { + fmt.Printf("Error: KMS key '%s' not found or not accessible\n", kmsKeyID) + } else { + fmt.Printf("Error uploading encrypted object: %v\n", err) + } + os.Exit(1) + } + + fmt.Println("Successfully uploaded encrypted object to S3!") + fmt.Printf(" Bucket: %s\n", bucketName) + fmt.Printf(" Key: %s\n", objectKey) + fmt.Printf(" Encryption Context: %v\n", encryptionContext) + fmt.Println() + + fmt.Println("--- Download and Decrypt Object from S3 ---") + + // Download and decrypt object using S3 Encryption Client + getInput := &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + } + + getResponse, err := encryptionClient.GetObject(ctx, getInput) + if err != nil { + fmt.Printf("Error downloading and decrypting object: %v\n", err) + os.Exit(1) + } + defer getResponse.Body.Close() + + // Read the decrypted data + decryptedData, err := io.ReadAll(getResponse.Body) + if err != nil { + fmt.Printf("Error reading decrypted data: %v\n", err) + os.Exit(1) + } + + fmt.Println("Successfully downloaded and decrypted object from S3!") + fmt.Printf(" Object size: %d bytes\n", len(decryptedData)) + fmt.Printf(" Decrypted data: %s\n", string(decryptedData)) + fmt.Println() + + fmt.Println("--- Verify Roundtrip Success ---") + + // Verify the roundtrip was successful + if string(decryptedData) == testData { + fmt.Println("SUCCESS: Roundtrip encryption/decryption completed successfully!") + fmt.Println(" Original data matches decrypted data") + fmt.Println(" Data integrity verified") + } else { + fmt.Println("ERROR: Roundtrip failed - data mismatch") + fmt.Printf(" Original: %s\n", testData) + fmt.Printf(" Decrypted: %s\n", string(decryptedData)) + os.Exit(1) + } + + // Optionally Delete the Object + //fmt.Println("--- Cleanup ---") + // Clean up the test object using regular S3 client + // _, err = s3Client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ + // Bucket: aws.String(bucketName), + // Key: aws.String(objectKey), + // }) + // if err != nil { + // fmt.Printf("Error deleting test object: %v\n", err) + // } else { + // fmt.Println("Test object deleted from S3") + // } + + fmt.Println() + fmt.Println("=== Example completed successfully! ===") +} From e5a50e5bd5ab4aab19b676d68f141e88b6fec83a Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Wed, 12 Nov 2025 17:25:57 -0800 Subject: [PATCH 145/201] chore: add net examples (#81) --- .github/workflows/examples.yml | 12 ++--- .gitmodules | 8 ++++ all-examples/README.md | 2 +- all-examples/net/.gitignore | 19 ++++++++ all-examples/net/v3/Makefile | 66 +++++++++++++++++++++++++++ all-examples/net/v3/Program.cs | 76 +++++++++++++++++++++++++++++++ all-examples/net/v3/s3ec-v3-local | 1 + all-examples/net/v3/v3.csproj | 19 ++++++++ all-examples/net/v4/Makefile | 66 +++++++++++++++++++++++++++ all-examples/net/v4/Program.cs | 76 +++++++++++++++++++++++++++++++ all-examples/net/v4/s3ec-v4-local | 1 + all-examples/net/v4/v4.csproj | 19 ++++++++ 12 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 all-examples/net/.gitignore create mode 100644 all-examples/net/v3/Makefile create mode 100644 all-examples/net/v3/Program.cs create mode 160000 all-examples/net/v3/s3ec-v3-local create mode 100644 all-examples/net/v3/v3.csproj create mode 100644 all-examples/net/v4/Makefile create mode 100644 all-examples/net/v4/Program.cs create mode 160000 all-examples/net/v4/s3ec-v4-local create mode 100644 all-examples/net/v4/v4.csproj diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index cecb6989..c4ae10bb 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -26,21 +26,21 @@ jobs: ref: fire-egg-dev path: all-examples/cpp/aws-sdk-cpp/ - - name: Checkout .NET V2 code + - name: Checkout .NET V3 code uses: actions/checkout@v5 with: token: ${{ secrets.PAT_FOR_DOTNET }} repository: aws/private-amazon-s3-encryption-client-dotnet-staging - ref: v3sdk-development - path: test-server/net-v2-v3-server/s3ec-net-v2/ + ref: rishav/key-commitment + path: all-examples/net/v3/s3ec-v3-local - - name: Checkout .NET V3 code + - name: Checkout .NET V4 code uses: actions/checkout@v5 with: token: ${{ secrets.PAT_FOR_DOTNET }} repository: aws/private-amazon-s3-encryption-client-dotnet-staging - ref: s3ec-v3 - path: test-server/net-v2-v3-server/s3ec-net-v3 + ref: s3ec-v4-WIP + path: all-examples/net/v4/s3ec-v4-local - name: Set up Python uses: actions/setup-python@v5 diff --git a/.gitmodules b/.gitmodules index 2d5c9a95..415ac204 100644 --- a/.gitmodules +++ b/.gitmodules @@ -56,3 +56,11 @@ path = test-server/net-v3-transition-server/s3ec-v3-transition-branch url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git branch = rishav/key-commitment +[submodule "all-examples/net/v4/s3ec-v4-local"] + path = all-examples/net/v4/s3ec-v4-local + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = s3ec-v4-WIP +[submodule "all-examples/net/v3/s3ec-v3-local"] + path = all-examples/net/v3/s3ec-v3-local + url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git + branch = rishav/key-commitment diff --git a/all-examples/README.md b/all-examples/README.md index 59bc2d6c..8472de78 100644 --- a/all-examples/README.md +++ b/all-examples/README.md @@ -9,7 +9,7 @@ Each language has subdirectories for different major versions of the S3 Encrypti - `cpp/` - C++ examples - `v2/` - S3EC C++ v2 example (transitional) - `v3/` - S3EC C++ v3 example (improved) -- `dotnet/` - .NET examples +- `net/` - .NET examples - `v3/` - S3EC .NET v3 example (transitional) - `v4/` - S3EC .NET v4 example (improved) - `go/` - Go examples diff --git a/all-examples/net/.gitignore b/all-examples/net/.gitignore new file mode 100644 index 00000000..c6a52ab1 --- /dev/null +++ b/all-examples/net/.gitignore @@ -0,0 +1,19 @@ +# Build results +bin/ +obj/ + +# User-specific files +*.user +*.suo +*.userosscache +*.sln.docstates + +# Visual Studio +.vs/ + +# Rider +.idea/ + +# NuGet packages +packages/ +*.nupkg diff --git a/all-examples/net/v3/Makefile b/all-examples/net/v3/Makefile new file mode 100644 index 00000000..c375acc7 --- /dev/null +++ b/all-examples/net/v3/Makefile @@ -0,0 +1,66 @@ +# Makefile for S3 Encryption Client .NET v3 Example + +# Default target +.PHONY: all install clean run help + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-dotnet-v3 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using .NET modules +install: + @echo "[NET V3] Installing .NET dependencies..." + dotnet restore + @echo "[NET V3] Dependencies installed successfully!" + +# Clean .NET artifacts +clean: + @echo "[NET V3] Cleaning .NET artifacts..." + dotnet clean + @echo "[NET V3] Clean completed!" + +# Run the example with default arguments +run: install + @echo "[NET V3] Running S3 Encryption Client v3 .NET example..." + @echo "[NET V3] Bucket: $(BUCKET_NAME)" + @echo "[NET V3] Object Key: $(OBJECT_KEY)" + @echo "[NET V3] KMS Key ID: $(KMS_KEY_ID)" + @echo "[NET V3] Region: $(AWS_REGION)" + @echo "" + @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client .NET v3 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install .NET dependencies using .NET modules" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove .NET artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Supported .NET framework installed on the system. See https://www.nuget.org/packages/Amazon.Extensions.S3.Encryption for supported one." + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v3 .NET SDK (included in s3ec-v3-local)" \ No newline at end of file diff --git a/all-examples/net/v3/Program.cs b/all-examples/net/v3/Program.cs new file mode 100644 index 00000000..6c1336f6 --- /dev/null +++ b/all-examples/net/v3/Program.cs @@ -0,0 +1,76 @@ +using Amazon; +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Amazon.S3; +using Amazon.S3.Model; + +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Amazon.S3; +using Amazon.S3.Model; + +namespace S3EncryptionClientV3Example +{ + class Program + { + static async Task Main(string[] args) + { + if (args.Length != 4) + { + Console.WriteLine("[NET V3] Usage: dotnet run "); + Environment.Exit(1); + } + + var (bucketName, objectKey, kmsKeyId, region) = (args[0], args[1], args[2], args[3]); + var testData = "Hello, World! This is a test message for S3 encryption client v3 in .NET."; + + Console.WriteLine("=== S3 Encryption Client v3 Example (.NET) ==="); + + try + { + var s3Client = CreateS3ECWithKms(kmsKeyId, region); + + await s3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = objectKey, + ContentBody = testData + }); + + var getResponse = await s3Client.GetObjectAsync(bucketName, objectKey); + using var reader = new StreamReader(getResponse.ResponseStream); + var decryptedData = await reader.ReadToEndAsync(); + + if (decryptedData != testData) + { + Console.WriteLine("[NET V3] ERROR: Roundtrip failed - data mismatch"); + Environment.Exit(1); + } + + Console.WriteLine("[NET V3] SUCCESS: Roundtrip encryption/decryption completed successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"[NET V3] Error: {ex.Message}"); + Environment.Exit(1); + } + } + + private static AmazonS3Client CreateS3ECWithKms(string kmsKeyId, string region) + { + var encryptionContextPerClient = new Dictionary + { + ["purpose"] = "example", + ["version"] = "v3", + ["language"] = "dotnet" + }; + + var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContextPerClient); + var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2, CommitmentPolicy.ForbidEncryptAllowDecrypt, ContentEncryptionAlgorithm.AesGcm) + { + RegionEndpoint = RegionEndpoint.GetBySystemName(region) + }; + return new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); + } + } +} diff --git a/all-examples/net/v3/s3ec-v3-local b/all-examples/net/v3/s3ec-v3-local new file mode 160000 index 00000000..ca1149d9 --- /dev/null +++ b/all-examples/net/v3/s3ec-v3-local @@ -0,0 +1 @@ +Subproject commit ca1149d9b423591c09d35caa649b3f6846e511a6 diff --git a/all-examples/net/v3/v3.csproj b/all-examples/net/v3/v3.csproj new file mode 100644 index 00000000..cfb74fad --- /dev/null +++ b/all-examples/net/v3/v3.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + false + + + + + + + + + + + diff --git a/all-examples/net/v4/Makefile b/all-examples/net/v4/Makefile new file mode 100644 index 00000000..f45fbdfd --- /dev/null +++ b/all-examples/net/v4/Makefile @@ -0,0 +1,66 @@ +# Makefile for S3 Encryption Client .NET v4 Example + +# Default target +.PHONY: all install clean run help + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-dotnet-v4 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using .NET modules +install: + @echo "[NET V4] Installing .NET dependencies..." + dotnet restore + @echo "[NET V4] Dependencies installed successfully!" + +# Clean .NET artifacts +clean: + @echo "[NET V4] Cleaning .NET artifacts..." + dotnet clean + @echo "[NET V4] Clean completed!" + +# Run the example with default arguments +run: install + @echo "[NET V4] Running S3 Encryption Client v4 .NET example..." + @echo "[NET V4] Bucket: $(BUCKET_NAME)" + @echo "[NET V4] Object Key: $(OBJECT_KEY)" + @echo "[NET V4] KMS Key ID: $(KMS_KEY_ID)" + @echo "[NET V4] Region: $(AWS_REGION)" + @echo "" + @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client .NET v4 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install .NET dependencies using .NET modules" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove .NET artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Supported .NET framework installed on the system. See https://www.nuget.org/packages/Amazon.Extensions.S3.Encryption for supported one." + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v4 .NET SDK (included in s3ec-v4-local)" \ No newline at end of file diff --git a/all-examples/net/v4/Program.cs b/all-examples/net/v4/Program.cs new file mode 100644 index 00000000..a8c799a6 --- /dev/null +++ b/all-examples/net/v4/Program.cs @@ -0,0 +1,76 @@ +using Amazon; +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Amazon.S3; +using Amazon.S3.Model; + +using Amazon.Extensions.S3.Encryption; +using Amazon.Extensions.S3.Encryption.Primitives; +using Amazon.S3; +using Amazon.S3.Model; + +namespace S3EncryptionClientV4Example +{ + class Program + { + static async Task Main(string[] args) + { + if (args.Length != 4) + { + Console.WriteLine("[NET V4] Usage: dotnet run "); + Environment.Exit(1); + } + + var (bucketName, objectKey, kmsKeyId, region) = (args[0], args[1], args[2], args[3]); + var testData = "Hello, World! This is a test message for S3 encryption client v4 in .NET."; + + Console.WriteLine("=== S3 Encryption Client v4 Example (.NET) ==="); + + try + { + var s3Client = CreateS3ECWithKms(kmsKeyId, region); + + await s3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = objectKey, + ContentBody = testData + }); + + var getResponse = await s3Client.GetObjectAsync(bucketName, objectKey); + using var reader = new StreamReader(getResponse.ResponseStream); + var decryptedData = await reader.ReadToEndAsync(); + + if (decryptedData != testData) + { + Console.WriteLine("[NET V4] ERROR: Roundtrip failed - data mismatch"); + Environment.Exit(1); + } + + Console.WriteLine("[NET V4] SUCCESS: Roundtrip encryption/decryption completed successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"[NET V4] Error: {ex.Message}"); + Environment.Exit(1); + } + } + + private static AmazonS3Client CreateS3ECWithKms(string kmsKeyId, string region) + { + var encryptionContextPerClient = new Dictionary + { + ["purpose"] = "example", + ["version"] = "v4", + ["language"] = "dotnet" + }; + + var encryptionMaterial = new EncryptionMaterialsV4(kmsKeyId, KmsType.KmsContext, encryptionContextPerClient); + var configuration = new AmazonS3CryptoConfigurationV4(SecurityProfile.V4, CommitmentPolicy.RequireEncryptRequireDecrypt, ContentEncryptionAlgorithm.AesGcmWithCommitment) + { + RegionEndpoint = RegionEndpoint.GetBySystemName(region) + }; + return new AmazonS3EncryptionClientV4(configuration, encryptionMaterial); + } + } +} diff --git a/all-examples/net/v4/s3ec-v4-local b/all-examples/net/v4/s3ec-v4-local new file mode 160000 index 00000000..691d22a5 --- /dev/null +++ b/all-examples/net/v4/s3ec-v4-local @@ -0,0 +1 @@ +Subproject commit 691d22a504184fd71f2dae7fd354bd669b58cc07 diff --git a/all-examples/net/v4/v4.csproj b/all-examples/net/v4/v4.csproj new file mode 100644 index 00000000..6d223a92 --- /dev/null +++ b/all-examples/net/v4/v4.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + false + + + + + + + + + + + From e52714b3e933682364b5bb060ba483540d2ac3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:04:00 -0800 Subject: [PATCH 146/201] chore: add php examples (#73) --- all-examples/php/v2/.gitignore | 4 + all-examples/php/v2/Makefile | 71 ++++++++ all-examples/php/v2/composer.json | 33 ++++ all-examples/php/v2/local-php-sdk | 1 + all-examples/php/v2/main.php | 161 +++++++++++++++++ all-examples/php/v3/.gitignore | 4 + all-examples/php/v3/Makefile | 71 ++++++++ all-examples/php/v3/composer.json | 33 ++++ all-examples/php/v3/local-php-sdk | 1 + all-examples/php/v3/main.php | 163 ++++++++++++++++++ .../php-v2-transition-server/local-php-sdk | 2 +- test-server/php-v3-server/local-php-sdk | 2 +- 12 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 all-examples/php/v2/.gitignore create mode 100644 all-examples/php/v2/Makefile create mode 100644 all-examples/php/v2/composer.json create mode 120000 all-examples/php/v2/local-php-sdk create mode 100755 all-examples/php/v2/main.php create mode 100644 all-examples/php/v3/.gitignore create mode 100644 all-examples/php/v3/Makefile create mode 100644 all-examples/php/v3/composer.json create mode 120000 all-examples/php/v3/local-php-sdk create mode 100755 all-examples/php/v3/main.php mode change 160000 => 120000 test-server/php-v2-transition-server/local-php-sdk diff --git a/all-examples/php/v2/.gitignore b/all-examples/php/v2/.gitignore new file mode 100644 index 00000000..07108589 --- /dev/null +++ b/all-examples/php/v2/.gitignore @@ -0,0 +1,4 @@ +vendor/* +cookies.txt +server.pid +composer.lock \ No newline at end of file diff --git a/all-examples/php/v2/Makefile b/all-examples/php/v2/Makefile new file mode 100644 index 00000000..0747d7b8 --- /dev/null +++ b/all-examples/php/v2/Makefile @@ -0,0 +1,71 @@ +# Makefile for S3 Encryption Client PHP v2 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.php + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-php-v2 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Composer +install: + @echo "Installing PHP dependencies..." + @composer install --no-dev --optimize-autoloader + @echo "Dependencies installed successfully!" + +# Clean composer artifacts +clean: + @echo "Cleaning composer artifacts..." + @rm -rf vendor/ + @rm -f composer.lock + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v2 PHP example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client PHP v2 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install PHP dependencies using Composer" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove composer artifacts (vendor/, composer.lock)" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - PHP 7.4+ installed on the system" + @echo " - Composer installed (https://getcomposer.org/)" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v2 PHP SDK (included in local-php-sdk)" diff --git a/all-examples/php/v2/composer.json b/all-examples/php/v2/composer.json new file mode 100644 index 00000000..914bf900 --- /dev/null +++ b/all-examples/php/v2/composer.json @@ -0,0 +1,33 @@ +{ + "name": "aws/s3ec-php-v2-example", + "description": "PHP v2 example for Amazon S3 Encryption Client", + "type": "project", + "license": "Apache-2.0", + "repositories": [ + { + "type": "path", + "url": "./local-php-sdk", + "options": { + "symlink": true + } + } + ], + "require": { + "php": ">=7.4", + "aws/aws-sdk-php": "@dev", + "ramsey/uuid": "^4.9" + }, + "autoload": { + "psr-4": { + "AWS\\S3EC\\Example\\": "src/" + } + }, + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.1" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/all-examples/php/v2/local-php-sdk b/all-examples/php/v2/local-php-sdk new file mode 120000 index 00000000..04ad0cf7 --- /dev/null +++ b/all-examples/php/v2/local-php-sdk @@ -0,0 +1 @@ +../../../test-server/php-v2-transition-server/local-php-sdk \ No newline at end of file diff --git a/all-examples/php/v2/main.php b/all-examples/php/v2/main.php new file mode 100755 index 00000000..df2fdd32 --- /dev/null +++ b/all-examples/php/v2/main.php @@ -0,0 +1,161 @@ +#!/usr/bin/env php + \n"; + echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v2 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; + exit(1); + } + + $bucketName = $GLOBALS['argv'][1]; + $objectKey = $GLOBALS['argv'][2]; + $kmsKeyId = $GLOBALS['argv'][3]; + $region = $GLOBALS['argv'][4]; + + echo "=== S3 Encryption Client v2 Example (PHP) ===\n"; + echo "Bucket: {$bucketName}\n"; + echo "Object Key: {$objectKey}\n"; + echo "KMS Key ID: {$kmsKeyId}\n"; + echo "Region: {$region}\n"; + echo "\n"; + + try { + // Test data for encryption + $testData = "Hello, World! This is a test message for S3 encryption client v2 in PHP."; + echo "Original data: {$testData}\n"; + echo "Data length: " . strlen($testData) . " bytes\n"; + echo "\n"; + + echo "--- Initialize S3 Encryption Client v2 ---\n"; + + // Create regular S3 client + $s3Client = new S3Client([ + 'region' => $region, + 'version' => 'latest' + ]); + + // Create KMS client + $kmsClient = new KmsClient([ + 'region' => $region, + 'version' => 'latest' + ]); + + // Create S3 Encryption Client v2 + $encryptionClient = new S3EncryptionClientV2($s3Client); + $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $kmsKeyId); + + echo "Successfully initialized S3 Encryption Client v2\n"; + echo "--- Encrypt and Upload Object to S3 ---\n"; + + // Add encryption context + $encryptionContext = [ + 'purpose' => 'example', + 'version' => 'v2', + 'language' => 'php' + ]; + + $cipherOptions = [ + 'Cipher' => 'gcm', + 'KeySize' => 256, + ]; + + // Upload encrypted object using S3 Encryption Client + $putResponse = $encryptionClient->putObject([ + 'Bucket' => $bucketName, + 'Key' => $objectKey, + 'Body' => $testData, + '@MaterialsProvider' => $materialsProvider, + '@KmsEncryptionContext' => $encryptionContext, + '@CipherOptions' => $cipherOptions, + ]); + + echo "Successfully uploaded encrypted object to S3!\n"; + echo " Bucket: {$bucketName}\n"; + echo " Key: {$objectKey}\n"; + echo " Encryption Context: " . json_encode($encryptionContext) . "\n"; + echo "\n"; + + echo "--- Download and Decrypt Object from S3 ---\n"; + + // Download and decrypt object using S3 Encryption Client + $getResponse = $encryptionClient->getObject([ + 'Bucket' => $bucketName, + 'Key' => $objectKey, + '@KmsEncryptionContext' => $encryptionContext, + '@MaterialsProvider' => $materialsProvider, + '@CommitmentPolicy' => 'FORBID_ENCRYPT_ALLOW_DECRYPT', + '@SecurityProfile' => 'V2' + ]); + + // Read the decrypted data + $decryptedData = (string) $getResponse['Body']; + + echo "Successfully downloaded and decrypted object from S3!\n"; + echo " Object size: " . strlen($decryptedData) . " bytes\n"; + echo " Decrypted data: {$decryptedData}\n"; + echo "\n"; + + echo "--- Verify Roundtrip Success ---\n"; + + // Verify the roundtrip was successful + if ($decryptedData === $testData) { + echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; + echo " Original data matches decrypted data\n"; + echo " Data integrity verified\n"; + } else { + echo "ERROR: Roundtrip failed - data mismatch\n"; + echo " Original: {$testData}\n"; + echo " Decrypted: {$decryptedData}\n"; + exit(1); + } + + // Optionally Delete the Object + // echo "--- Cleanup ---\n"; + // Clean up the test object using regular S3 client + // $s3Client->deleteObject([ + // 'Bucket' => $bucketName, + // 'Key' => $objectKey + // ]); + // echo "Test object deleted from S3\n"; + + echo "\n"; + echo "=== Example completed successfully! ===\n"; + + } catch (AwsException $e) { + $errorCode = $e->getAwsErrorCode(); + $errorMessage = $e->getMessage(); + + if (strpos($errorCode, 'NoSuchBucket') !== false) { + echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorCode, 'NotFoundException') !== false) { + echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorMessage, 'encryption') !== false) { + echo "S3 Encryption Error: {$errorMessage}\n"; + } else { + echo "AWS Service Error: {$errorMessage}\n"; + echo " Error Code: {$errorCode}\n"; + } + exit(1); + } catch (Exception $e) { + echo "Unexpected error: {$e->getMessage()}\n"; + echo " File: {$e->getFile()}:{$e->getLine()}\n"; + exit(1); + } +} + +// Run the main function if this script is executed directly +if (php_sapi_name() === 'cli' && isset($GLOBALS['argv']) && basename($GLOBALS['argv'][0]) === basename(__FILE__)) { + main(); +} diff --git a/all-examples/php/v3/.gitignore b/all-examples/php/v3/.gitignore new file mode 100644 index 00000000..07108589 --- /dev/null +++ b/all-examples/php/v3/.gitignore @@ -0,0 +1,4 @@ +vendor/* +cookies.txt +server.pid +composer.lock \ No newline at end of file diff --git a/all-examples/php/v3/Makefile b/all-examples/php/v3/Makefile new file mode 100644 index 00000000..328a901a --- /dev/null +++ b/all-examples/php/v3/Makefile @@ -0,0 +1,71 @@ +# Makefile for S3 Encryption Client PHP v3 Example + +# Default target +.PHONY: all install clean run help + +# Variables +SCRIPT = main.php + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-php-v3 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install dependencies using Composer +install: + @echo "Installing PHP dependencies..." + @composer install --no-dev --optimize-autoloader + @echo "Dependencies installed successfully!" + +# Clean composer artifacts +clean: + @echo "Cleaning composer artifacts..." + @rm -rf vendor/ + @rm -f composer.lock + @echo "Clean completed!" + +# Run the example with default arguments +run: install + @echo "Running S3 Encryption Client v3 PHP example..." + @echo "Bucket: $(BUCKET_NAME)" + @echo "Object Key: $(OBJECT_KEY)" + @echo "KMS Key ID: $(KMS_KEY_ID)" + @echo "Region: $(AWS_REGION)" + @echo "" + @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) + +# Show help +help: + @echo "S3 Encryption Client PHP v3 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " install - Install PHP dependencies using Composer" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove composer artifacts (vendor/, composer.lock)" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - PHP 7.4+ installed on the system" + @echo " - Composer installed (https://getcomposer.org/)" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v3 PHP SDK (included in local-php-sdk)" diff --git a/all-examples/php/v3/composer.json b/all-examples/php/v3/composer.json new file mode 100644 index 00000000..2ad2469a --- /dev/null +++ b/all-examples/php/v3/composer.json @@ -0,0 +1,33 @@ +{ + "name": "aws/s3ec-php-v3-example", + "description": "PHP v3 example for Amazon S3 Encryption Client", + "type": "project", + "license": "Apache-2.0", + "repositories": [ + { + "type": "path", + "url": "./local-php-sdk", + "options": { + "symlink": true + } + } + ], + "require": { + "php": ">=7.4", + "aws/aws-sdk-php": "@dev", + "ramsey/uuid": "^4.9" + }, + "autoload": { + "psr-4": { + "AWS\\S3EC\\Example\\": "src/" + } + }, + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.1" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/all-examples/php/v3/local-php-sdk b/all-examples/php/v3/local-php-sdk new file mode 120000 index 00000000..3b9b4cd7 --- /dev/null +++ b/all-examples/php/v3/local-php-sdk @@ -0,0 +1 @@ +../../../test-server/php-v3-server/local-php-sdk \ No newline at end of file diff --git a/all-examples/php/v3/main.php b/all-examples/php/v3/main.php new file mode 100755 index 00000000..e22519c3 --- /dev/null +++ b/all-examples/php/v3/main.php @@ -0,0 +1,163 @@ +#!/usr/bin/env php + \n"; + echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; + exit(1); + } + + $bucketName = $GLOBALS['argv'][1]; + $objectKey = $GLOBALS['argv'][2]; + $kmsKeyId = $GLOBALS['argv'][3]; + $region = $GLOBALS['argv'][4]; + + echo "=== S3 Encryption Client v3 Example (PHP) ===\n"; + echo "Bucket: {$bucketName}\n"; + echo "Object Key: {$objectKey}\n"; + echo "KMS Key ID: {$kmsKeyId}\n"; + echo "Region: {$region}\n"; + echo "\n"; + + try { + // Test data for encryption + $testData = "Hello, World! This is a test message for S3 encryption client v3 in PHP."; + echo "Original data: {$testData}\n"; + echo "Data length: " . strlen($testData) . " bytes\n"; + echo "\n"; + + echo "--- Initialize S3 Encryption Client v3 ---\n"; + + // Create regular S3 client + $s3Client = new S3Client([ + 'region' => $region, + 'version' => 'latest' + ]); + + // Create KMS client + $kmsClient = new KmsClient([ + 'region' => $region, + 'version' => 'latest' + ]); + + // Create S3 Encryption Client v3 + // Create S3 Encryption Client v2 + $encryptionClient = new S3EncryptionClientV3($s3Client); + $materialsProvider = new KmsMaterialsProviderV3($kmsClient, $kmsKeyId); + + echo "Successfully initialized S3 Encryption Client v3\n"; + echo "--- Encrypt and Upload Object to S3 ---\n"; + + // Add encryption context + $encryptionContext = [ + 'purpose' => 'example', + 'version' => 'v3', + 'language' => 'php' + ]; + + $cipherOptions = [ + 'Cipher' => 'gcm', + 'KeySize' => 256, + ]; + + // Upload encrypted object using S3 Encryption Client + $putResponse = $encryptionClient->putObject([ + 'Bucket' => $bucketName, + 'Key' => $objectKey, + 'Body' => $testData, + '@MaterialsProvider' => $materialsProvider, + '@KmsEncryptionContext' => $encryptionContext, + '@CommitmentPolicy' => "REQUIRE_ENCRYPT_REQUIRE_DECRYPT", + '@CipherOptions' => $cipherOptions, + ]); + + echo "Successfully uploaded encrypted object to S3!\n"; + echo " Bucket: {$bucketName}\n"; + echo " Key: {$objectKey}\n"; + echo " Encryption Context: " . json_encode($encryptionContext) . "\n"; + echo "\n"; + + echo "--- Download and Decrypt Object from S3 ---\n"; + + // Download and decrypt object using S3 Encryption Client + $getResponse = $encryptionClient->getObject([ + 'Bucket' => $bucketName, + 'Key' => $objectKey, + '@KmsEncryptionContext' => $encryptionContext, + '@MaterialsProvider' => $materialsProvider, + '@CommitmentPolicy' => "REQUIRE_ENCRYPT_REQUIRE_DECRYPT", + '@SecurityProfile' => 'V3' + ]); + + // Read the decrypted data + $decryptedData = (string) $getResponse['Body']; + + echo "Successfully downloaded and decrypted object from S3!\n"; + echo " Object size: " . strlen($decryptedData) . " bytes\n"; + echo " Decrypted data: {$decryptedData}\n"; + echo "\n"; + + echo "--- Verify Roundtrip Success ---\n"; + + // Verify the roundtrip was successful + if ($decryptedData === $testData) { + echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; + echo " Original data matches decrypted data\n"; + echo " Data integrity verified\n"; + } else { + echo "ERROR: Roundtrip failed - data mismatch\n"; + echo " Original: {$testData}\n"; + echo " Decrypted: {$decryptedData}\n"; + exit(1); + } + + // Optionally Delete the Object + // echo "--- Cleanup ---\n"; + // Clean up the test object using regular S3 client + // $s3Client->deleteObject([ + // 'Bucket' => $bucketName, + // 'Key' => $objectKey + // ]); + // echo "Test object deleted from S3\n"; + + echo "\n"; + echo "=== Example completed successfully! ===\n"; + + } catch (AwsException $e) { + $errorCode = $e->getAwsErrorCode(); + $errorMessage = $e->getMessage(); + + if (strpos($errorCode, 'NoSuchBucket') !== false) { + echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorCode, 'NotFoundException') !== false) { + echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorMessage, 'encryption') !== false) { + echo "S3 Encryption Error: {$errorMessage}\n"; + } else { + echo "AWS Service Error: {$errorMessage}\n"; + echo " Error Code: {$errorCode}\n"; + } + exit(1); + } catch (Exception $e) { + echo "Unexpected error: {$e->getMessage()}\n"; + echo " File: {$e->getFile()}:{$e->getLine()}\n"; + exit(1); + } +} + +// Run the main function if this script is executed directly +if (php_sapi_name() === 'cli' && isset($GLOBALS['argv']) && basename($GLOBALS['argv'][0]) === basename(__FILE__)) { + main(); +} diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk deleted file mode 160000 index 35a52086..00000000 --- a/test-server/php-v2-transition-server/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 35a52086c5ccf7f5e62e3c17e210923e129c823b diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk new file mode 120000 index 00000000..4610ddb9 --- /dev/null +++ b/test-server/php-v2-transition-server/local-php-sdk @@ -0,0 +1 @@ +../php-v3-server/local-php-sdk \ No newline at end of file diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index 35a52086..2cb0cda2 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 35a52086c5ccf7f5e62e3c17e210923e129c823b +Subproject commit 2cb0cda27812fae3f16e719df4b22b5c08526148 From 174a70f2413f8226436725fe2d3b081df05129eb Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 13 Nov 2025 10:58:30 -0800 Subject: [PATCH 147/201] all one iv (#74) Ruby, C++, Go with tests and updates to run IV 0x01 --- .github/workflows/test.yml | 183 ++++++++++-------- .gitmodules | 15 +- test-server/Makefile | 41 +++- test-server/cpp-v2-server/Makefile | 25 ++- test-server/cpp-v2-server/aws-sdk-cpp | 1 + test-server/cpp-v2-transition-server/Makefile | 26 ++- .../cpp-v2-transition-server/aws-sdk-cpp | 1 + test-server/cpp-v3-server/Makefile | 26 ++- test-server/cpp-v3-server/aws-sdk-cpp | 1 + test-server/go-v3-server/Makefile | 18 +- test-server/go-v3-transition-server/Makefile | 18 +- .../go-v3-transition-server/local-go-s3ec | 2 +- test-server/go-v4-server/Makefile | 18 +- test-server/go-v4-server/local-go-s3ec | 2 +- test-server/java-v3-server/Makefile | 17 +- .../.duvet/config.toml | 14 +- .../java-v3-transition-server/Makefile | 21 +- .../java-v3-transition-server/specification | 1 - test-server/java-v4-server/.duvet/config.toml | 14 +- test-server/java-v4-server/Makefile | 21 +- test-server/java-v4-server/specification | 1 - test-server/net-v2-v3-server/Makefile | 34 ++-- test-server/net-v3-transition-server/Makefile | 21 +- .../s3ec-v3-transition-branch | 2 +- test-server/net-v4-server/Makefile | 15 +- .../net-v4-server/s3ec-net-v4-improved | 2 +- test-server/php-v2-server/Makefile | 17 +- test-server/php-v2-transition-server/Makefile | 17 +- .../php-v2-transition-server/local-php-sdk | 2 +- .../local-php-sdk~fireegg-test-servers | 1 + test-server/php-v3-server/Makefile | 17 +- test-server/php-v3-server/local-php-sdk | 2 +- test-server/python-v3-server/Makefile | 20 +- test-server/ruby-v2-server/Makefile | 18 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/Makefile | 18 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- test-server/specification | 2 +- 38 files changed, 434 insertions(+), 224 deletions(-) create mode 160000 test-server/cpp-v2-server/aws-sdk-cpp create mode 160000 test-server/cpp-v2-transition-server/aws-sdk-cpp create mode 160000 test-server/cpp-v3-server/aws-sdk-cpp delete mode 120000 test-server/java-v3-transition-server/specification delete mode 120000 test-server/java-v4-server/specification mode change 120000 => 160000 test-server/php-v2-transition-server/local-php-sdk create mode 120000 test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23cee4d8..e2c43191 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,70 +21,56 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - submodules: true + submodules: false # This is Ryan Emery's (seebees) PAT. # To grant this workflow access to a new private repo, # ask Ryan to edit this PAT's permissions to add access to a new private repo. token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - - name: Checkout CPP code for cpp-v2-transition - uses: actions/checkout@v5 - with: - submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev - path: test-server/cpp-v2-transition-server/aws-sdk-cpp/ - - - name: Checkout CPP code cpp-v3 - uses: actions/checkout@v5 - with: - submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev - path: test-server/cpp-v3-server/aws-sdk-cpp/ + # There are a lot of submodules here + # This initializes the checkouts in parallel (--jobs) + # rather than in series the way actions/checkout@v5 does it. - - name: Checkout .NET V2 code - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - # This is the branch for S3EC .NET V2 - ref: v3sdk-development - path: test-server/net-v2-v3-server/s3ec-net-v2/ + - name: Get CPU count + id: cpu-count + run: echo "count=$(node -p 'require("os").cpus().length')" >> $GITHUB_OUTPUT - - name: Checkout .NET V3 code - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - # This is the branch for S3EC .NET V3 - ref: s3ec-v3 - path: test-server/net-v2-v3-server/s3ec-net-v3 + - name: Setup git submodules with PAT + run: | + git config --global url."https://github.com/".insteadOf "git@github.com:" + git config --global credential.helper store + echo "https://x-token-auth:${{ secrets.PAT_FOR_PRIVATE_RUBY }}@github.com" > ~/.git-credentials - - name: Checkout .NET V3 code (Transition) - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - # This is the branch for S3EC .NET V3 transition - ref: rishav/key-commitment - path: test-server/net-v3-transition-server/s3ec-v3-transition-branch - - - name: Checkout .NET V4 (Improved) code - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - # This is the branch for S3EC .NET V4 (improved) - ref: s3ec-v4-WIP - path: test-server/net-v4-server/s3ec-net-v4-improved - - - name: Set up Python - uses: actions/setup-python@v5 + - name: Cache git submodules + uses: actions/cache@v4 with: - python-version: ${{ inputs.python-version || '3.11' }} + 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 }} + git config --global submodule.fetchJobs ${{ steps.cpu-count.outputs.count }} + git config --global remote.origin.tagOpt --no-tags + + - name: Checkout submodules with --jobs + run: | + git submodule update --init --depth 1 --jobs ${{ steps.cpu-count.outputs.count }} + + - name: Update cpp submodules recursively with --jobs + run: | + git submodule update --init --recursive \ + --depth 1 \ + --filter=blob:none \ + --jobs ${{ steps.cpu-count.outputs.count }} \ + --force \ + test-server/cpp-v2-transition-server/aws-sdk-cpp \ + test-server/cpp-v3-server/aws-sdk-cpp \ + test-server/cpp-v2-server/aws-sdk-cpp - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -116,17 +102,39 @@ jobs: with: go-version: 1.25 - # Cache uv dependencies - - name: Cache uv dependencies - uses: actions/cache@v3 - with: - path: ~/.cache/uv - key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-uv- - - - name: Install Uv - run: pip install uv + - name: Install C++ dependencies + run: | + brew install libmicrohttpd nlohmann-json ossp-uuid + + # Legacy Python tests: + # - name: Set up Python + # uses: actions/setup-python@v5 + # with: + # python-version: ${{ inputs.python-version || '3.11' }} + # + # # Cache uv dependencies + # - name: Cache uv dependencies + # uses: actions/cache@v3 + # with: + # path: ~/.cache/uv + # key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} + # restore-keys: | + # ${{ runner.os }}-uv- + + # - name: Install Uv + # run: pip install uv + + # - name: Install dependencies + # run: make install + + # - name: Run unit tests + # run: make test-unit + + # - name: Run integration tests + # run: make test-integration + # env: + # CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} + # CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} # Cache Gradle dependencies and build outputs - name: Cache Gradle packages @@ -141,32 +149,53 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Install dependencies - run: make install - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role aws-region: us-west-2 - - name: Run unit tests - run: make test-unit + - name: Build the servers + run: cd test-server && make build-all-servers FILTER=ruby,go,cpp + 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,go,cpp + 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: Run integration tests - run: make test-integration + - name: Wait for servers to start + run: cd test-server && make wait-all-servers FILTER=ruby,go,cpp env: - CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} - CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} + 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: Run test-server tests - run: cd test-server && make ci + - name: Run run-tests + run: cd test-server && make run-tests FILTER=ruby,go,cpp 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 }} GRADLE_OPTS: "-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" + - name: Upload server logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: server-logs + path: | + test-server/*/server.log + test-server/*/net-v2-server.log + test-server/*/net-v3-server.log + + - name: Stop the servers + run: cd test-server && make stop-servers FILTER=ruby,go,cpp + - name: Upload results if: always() uses: actions/upload-artifact@v4 diff --git a/.gitmodules b/.gitmodules index 415ac204..51e8e255 100644 --- a/.gitmodules +++ b/.gitmodules @@ -24,10 +24,6 @@ url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git branch = imabhichow/add-kc ; branch = s3ec/improved -[submodule "test-server/java-v4-server/specification"] - path = test-server/java-v4-server/specification - url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git - branch = fire-egg-staging [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git @@ -56,6 +52,17 @@ path = test-server/net-v3-transition-server/s3ec-v3-transition-branch url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git branch = rishav/key-commitment +[submodule "test-server/cpp-v2-transition-server/aws-sdk-cpp"] + path = test-server/cpp-v2-transition-server/aws-sdk-cpp + url = git@github.com:awslabs/aws-sdk-cpp-staging.git + branch = fire-egg-dev +[submodule "test-server/cpp-v3-server/aws-sdk-cpp"] + path = test-server/cpp-v3-server/aws-sdk-cpp + url = git@github.com:awslabs/aws-sdk-cpp-staging.git + branch = fire-egg-dev +[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 "all-examples/net/v4/s3ec-v4-local"] path = all-examples/net/v4/s3ec-v4-local url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git diff --git a/test-server/Makefile b/test-server/Makefile index dc3bdad3..9b18b857 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -6,15 +6,36 @@ all: start-all-servers wait-all-servers run-tests # CI target for GitHub Actions -ci: start-servers run-tests stop-servers +ci: + $(MAKE) build-all-servers + $(MAKE) start-all-servers + $(MAKE) wait-all-servers + $(MAKE) run-tests + $(MAKE) stop-servers SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) +BUILD_SERVER_TARGETS := $(addprefix build-, $(SERVER_DIRS)) START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS)) WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS)) -# Start all servers in parallel +# 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_SERVER_TARGETS): build-%: + @if [ -f $*/Makefile ]; then \ + echo "Building server in $*..."; \ + $(MAKE) -C $* build-server; \ + else \ + echo "❌ Error: no Makefile found in $*"; \ + exit 1; \ + fi + +# Build and start all servers start-servers: + @echo "Building all servers..." + $(MAKE) build-all-servers @echo "Starting all servers..." $(MAKE) start-all-servers @echo "Waiting for servers to start..." @@ -23,7 +44,9 @@ start-servers: $(MAKE) -C $$dir wait-for-server; \ done -start-all-servers: $(START_SERVER_TARGETS) +# Start servers sequentially (no parallel execution) +start-all-servers: + @$(MAKE) MAKEFLAGS= $(START_SERVER_TARGETS) $(START_SERVER_TARGETS): start-%: @if [ -f $*/Makefile ]; then \ @@ -32,9 +55,12 @@ $(START_SERVER_TARGETS): start-%: else \ echo "❌ Error: no Makefile found in $*"; \ exit 1; \ - fi; \ + fi; -wait-all-servers: $(WAIT_SERVER_TARGETS) +wait-all-servers: + @echo "Waiting for all servers to be ready..." + $(MAKE) $(WAIT_SERVER_TARGETS) + @echo "All servers are ready!" $(WAIT_SERVER_TARGETS): wait-%: @if [ -f $*/Makefile ]; then \ @@ -43,7 +69,7 @@ $(WAIT_SERVER_TARGETS): wait-%: else \ echo "❌ Error: no Makefile found in $*"; \ exit 1; \ - fi; \ + fi; # Run the Java tests @@ -63,7 +89,7 @@ run-tests: stop-servers: @echo "Stopping servers..." @for dir in $(SERVER_DIRS); do \ - echo "Starting server in $$dir..."; \ + echo "Stopping server in $$dir..."; \ $(MAKE) -C $$dir stop-server; \ done @echo "Servers stopped" @@ -94,6 +120,7 @@ wait-for-port: echo "❌ Error: PORT is required"; \ exit 1; \ fi + @echo "Starting to wait for $$PORT to start"; @for i in $$(seq 1 $(TIMEOUT)); do \ if nc -z localhost $$PORT; then \ echo "Ports are open, waiting for servers to initialize..."; \ diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile index 9e0f04b1..77357c37 100644 --- a/test-server/cpp-v2-server/Makefile +++ b/test-server/cpp-v2-server/Makefile @@ -1,31 +1,38 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8085 -build/s3ec-server: - brew install libmicrohttpd nlohmann-json ossp-uuid - git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git +build/s3ec-server: cd aws-sdk-cpp mkdir -p build && cd build && cmake .. -start-server: | build/s3ec-server +build-server: | build/s3ec-server + @echo "Building Cpp V2 server..." + cd build && make + +start-server: @echo "Starting Cpp V2 server..." - cd build && make && \ + cd build && \ 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" \ - ./s3ec-server & echo $$! > $(PID_FILE) + ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) @echo "Cpp V2 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f build/server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v2-server/aws-sdk-cpp b/test-server/cpp-v2-server/aws-sdk-cpp new file mode 160000 index 00000000..994384ca --- /dev/null +++ b/test-server/cpp-v2-server/aws-sdk-cpp @@ -0,0 +1 @@ +Subproject commit 994384ca8b9defe2ae60b5d3447ec5f47f7ec19f diff --git a/test-server/cpp-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile index 05803c78..16b70796 100644 --- a/test-server/cpp-v2-transition-server/Makefile +++ b/test-server/cpp-v2-transition-server/Makefile @@ -1,29 +1,37 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8097 build/s3ec-server: - brew install libmicrohttpd nlohmann-json ossp-uuid mkdir -p build && cd build && cmake .. -start-server: | build/s3ec-server - @echo "Starting Cpp V2 server..." - cd build && make && \ +build-server: | build/s3ec-server + @echo "Building Cpp transition server..." + cd build && make + +start-server: + @echo "Starting Cpp transition server..." + cd build && \ 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" \ - ./s3ec-server & echo $$! > $(PID_FILE) - @echo "Cpp V2 server starting..." + ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) + @echo "Cpp transition server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f build/server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp new file mode 160000 index 00000000..9a368aa8 --- /dev/null +++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp @@ -0,0 +1 @@ +Subproject commit 9a368aa8f6bbcd75eb1180d5d76d41936c68ed6a diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile index 86fc285e..46f0c9db 100644 --- a/test-server/cpp-v3-server/Makefile +++ b/test-server/cpp-v3-server/Makefile @@ -1,29 +1,37 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8091 build/s3ec-server: - brew install libmicrohttpd nlohmann-json ossp-uuid mkdir -p build && cd build && cmake .. -start-server: | build/s3ec-server - @echo "Starting Cpp V2 server..." - cd build && make && \ +build-server: | build/s3ec-server + @echo "Building Cpp V3 server..." + cd build && make + +start-server: + @echo "Starting Cpp V3 server..." + cd build && \ 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" \ - ./s3ec-server & echo $$! > $(PID_FILE) - @echo "Cpp V2 server starting..." + ./s3ec-server > server.log 2>&1 & echo $$! > ../$(PID_FILE) + @echo "Cpp V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f build/server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp new file mode 160000 index 00000000..9a368aa8 --- /dev/null +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -0,0 +1 @@ +Subproject commit 9a368aa8f6bbcd75eb1180d5d76d41936c68ed6a diff --git a/test-server/go-v3-server/Makefile b/test-server/go-v3-server/Makefile index fb61e578..80928dbd 100644 --- a/test-server/go-v3-server/Makefile +++ b/test-server/go-v3-server/Makefile @@ -1,25 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8082 +build-server: + @echo "Building Go V3 server..." + go mod tidy + start-server: @echo "Starting Go V3 server..." - go mod tidy 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" \ - go run . & echo $$! > $(PID_FILE) + go run . > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Go V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/go-v3-transition-server/Makefile b/test-server/go-v3-transition-server/Makefile index b03ea80b..a254acdf 100644 --- a/test-server/go-v3-transition-server/Makefile +++ b/test-server/go-v3-transition-server/Makefile @@ -1,25 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8095 +build-server: + @echo "Building Go V3 Transition server..." + go mod tidy + start-server: @echo "Starting Go V3 Transition server..." - go mod tidy 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" \ - go run . & echo $$! > $(PID_FILE) + go run . > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Go V3 Transition server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/go-v3-transition-server/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec index 7a29344c..85fd30c6 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 7a29344cc0c431fd5ac6d0a08ce4db455d75c175 +Subproject commit 85fd30c6a7ebbef3d056991c6f3673e0e9002bcf diff --git a/test-server/go-v4-server/Makefile b/test-server/go-v4-server/Makefile index cfaf32fe..6c549db2 100644 --- a/test-server/go-v4-server/Makefile +++ b/test-server/go-v4-server/Makefile @@ -1,25 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8089 +build-server: + @echo "Building Go V4 server..." + go mod tidy + start-server: @echo "Starting Go V4 server..." - go mod tidy 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" \ - go run . & echo $$! > $(PID_FILE) + go run . > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Go V4 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index 9946186d..85fd30c6 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit 9946186d6b760074750a535b663d6c84c5815308 +Subproject commit 85fd30c6a7ebbef3d056991c6f3673e0e9002bcf diff --git a/test-server/java-v3-server/Makefile b/test-server/java-v3-server/Makefile index 445be2ac..692e80b3 100644 --- a/test-server/java-v3-server/Makefile +++ b/test-server/java-v3-server/Makefile @@ -1,24 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8080 +build-server: + @echo "Building Java V3 server..." + ./gradlew --build-cache --parallel build + start-server: @echo "Starting Java V3 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" \ - ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + ./gradlew --build-cache --parallel run > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Java V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v3-transition-server/.duvet/config.toml b/test-server/java-v3-transition-server/.duvet/config.toml index d07014da..f29cd058 100644 --- a/test-server/java-v3-transition-server/.duvet/config.toml +++ b/test-server/java-v3-transition-server/.duvet/config.toml @@ -5,19 +5,19 @@ pattern = "s3ec-staging/*.java" # Include required specifications here [[specification]] -source = "specification/s3-encryption/client.md" +source = "../specification/s3-encryption/client.md" [[specification]] -source = "specification/s3-encryption/decryption.md" +source = "../specification/s3-encryption/decryption.md" [[specification]] -source = "specification/s3-encryption/encryption.md" +source = "../specification/s3-encryption/encryption.md" [[specification]] -source = "specification/s3-encryption/key-commitment.md" +source = "../specification/s3-encryption/key-commitment.md" [[specification]] -source = "specification/s3-encryption/key-derivation.md" +source = "../specification/s3-encryption/key-derivation.md" [[specification]] -source = "specification/s3-encryption/data-format/content-metadata.md" +source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] -source = "specification/s3-encryption/data-format/metadata-strategy.md" +source = "../specification/s3-encryption/data-format/metadata-strategy.md" [report.html] enabled = true diff --git a/test-server/java-v3-transition-server/Makefile b/test-server/java-v3-transition-server/Makefile index 3f0358c9..5a25a8aa 100644 --- a/test-server/java-v3-transition-server/Makefile +++ b/test-server/java-v3-transition-server/Makefile @@ -1,29 +1,36 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server build-s3ec +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8094 -build-s3ec: +build-server: @echo "Building S3EC from source..." 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 -start-server: build-s3ec +start-server: @echo "Starting Java V3 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" \ - ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + ./gradlew --build-cache --parallel run > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Java V3 Transition server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) @@ -32,4 +39,4 @@ duvet: duvet report view-report-mac: - open .duvet/reports/report.html \ No newline at end of file + open .duvet/reports/report.html diff --git a/test-server/java-v3-transition-server/specification b/test-server/java-v3-transition-server/specification deleted file mode 120000 index b173f708..00000000 --- a/test-server/java-v3-transition-server/specification +++ /dev/null @@ -1 +0,0 @@ -../specification \ No newline at end of file diff --git a/test-server/java-v4-server/.duvet/config.toml b/test-server/java-v4-server/.duvet/config.toml index d07014da..f29cd058 100644 --- a/test-server/java-v4-server/.duvet/config.toml +++ b/test-server/java-v4-server/.duvet/config.toml @@ -5,19 +5,19 @@ pattern = "s3ec-staging/*.java" # Include required specifications here [[specification]] -source = "specification/s3-encryption/client.md" +source = "../specification/s3-encryption/client.md" [[specification]] -source = "specification/s3-encryption/decryption.md" +source = "../specification/s3-encryption/decryption.md" [[specification]] -source = "specification/s3-encryption/encryption.md" +source = "../specification/s3-encryption/encryption.md" [[specification]] -source = "specification/s3-encryption/key-commitment.md" +source = "../specification/s3-encryption/key-commitment.md" [[specification]] -source = "specification/s3-encryption/key-derivation.md" +source = "../specification/s3-encryption/key-derivation.md" [[specification]] -source = "specification/s3-encryption/data-format/content-metadata.md" +source = "../specification/s3-encryption/data-format/content-metadata.md" [[specification]] -source = "specification/s3-encryption/data-format/metadata-strategy.md" +source = "../specification/s3-encryption/data-format/metadata-strategy.md" [report.html] enabled = true diff --git a/test-server/java-v4-server/Makefile b/test-server/java-v4-server/Makefile index 734a7808..418e0127 100644 --- a/test-server/java-v4-server/Makefile +++ b/test-server/java-v4-server/Makefile @@ -1,29 +1,36 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server build-s3ec +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8088 -build-s3ec: +build-server: @echo "Building S3EC from source..." 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 -start-server: build-s3ec +start-server: @echo "Starting Java V4 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" \ - ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + ./gradlew --build-cache --parallel run > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Java V4 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) @@ -32,4 +39,4 @@ duvet: duvet report view-report-mac: - open .duvet/reports/report.html \ No newline at end of file + open .duvet/reports/report.html diff --git a/test-server/java-v4-server/specification b/test-server/java-v4-server/specification deleted file mode 120000 index b173f708..00000000 --- a/test-server/java-v4-server/specification +++ /dev/null @@ -1 +0,0 @@ -../specification \ No newline at end of file diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile index e752b925..b50ae4f8 100644 --- a/test-server/net-v2-v3-server/Makefile +++ b/test-server/net-v2-v3-server/Makefile @@ -1,52 +1,58 @@ # Makefile for S3 Encryption Client .NET Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE_NET_V2 := net-v2-server.pid PID_FILE_NET_V3 := net-v3-server.pid PORT_NET_V2 := 8083 PORT_NET_V3 := 8084 +build-server: + @echo "Building .NET V2 and V3 servers..." + rm -rf obj/v2 bin/v2 obj/v3 bin/v3 + dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ + dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ + start-server: $(MAKE) start-net-v2-server; \ $(MAKE) start-net-v3-server; stop-server: + @echo "Stopping .NET V2 server on port $(PORT_NET_V2)..." + @lsof -ti:$(PORT_NET_V2) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE_NET_V2) ]; then \ - kill $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ - rm $(PID_FILE_NET_V2); \ + pkill -P $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ + rm -f $(PID_FILE_NET_V2); \ fi + @rm -f net-v2-server.log + @echo "Stopping .NET V3 server on port $(PORT_NET_V3)..." + @lsof -ti:$(PORT_NET_V3) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE_NET_V3) ]; then \ - kill $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ - rm $(PID_FILE_NET_V3); \ + pkill -P $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ + rm -f $(PID_FILE_NET_V3); \ fi + @rm -f net-v3-server.log + @echo "Servers stopped" # Start .NET V2 server in background -# This builds first into bin/v2 and runs through dll -# to avoid simultaneous dotnet run conflict start-net-v2-server: @echo "Starting .NET V2 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" \ - rm -rf obj/v2 bin/v2 && \ - dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ && \ dotnet bin/v2/NetV2V3Server.dll > net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid @echo ".NET V2 server starting..." - # Start .NET V3 server in background -# This builds first into bin/v3 and runs through dll -# to avoid simultaneous dotnet run conflict start-net-v3-server: @echo "Starting .NET V3 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" \ - rm -rf obj/v3 bin/v3 && \ - dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ && \ dotnet bin/v3/NetV2V3Server.dll > net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid @echo ".NET V3 server starting..." diff --git a/test-server/net-v3-transition-server/Makefile b/test-server/net-v3-transition-server/Makefile index ca863f28..eba78e1c 100644 --- a/test-server/net-v3-transition-server/Makefile +++ b/test-server/net-v3-transition-server/Makefile @@ -1,28 +1,37 @@ # Makefile for S3 Encryption Client .NET Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE_NET_V3_TRANSITION := net-v3-transition-server.pid PORT_NET_V3_TRANSITION := 8100 +build-server: + @echo "Building .NET V3 transition server..." + dotnet build + start-server: $(MAKE) start-net-v3-transition-server stop-server: + @echo "Stopping .NET V3 Transition server on port $(PORT_NET_V3_TRANSITION)..." + @lsof -ti:$(PORT_NET_V3_TRANSITION) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE_NET_V3_TRANSITION) ]; then \ - kill $$(cat $(PID_FILE_NET_V3_TRANSITION)) 2>/dev/null || true; \ - rm $(PID_FILE_NET_V3_TRANSITION); \ + pkill -P $$(cat $(PID_FILE_NET_V3_TRANSITION)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE_NET_V3_TRANSITION)) 2>/dev/null || true; \ + rm -f $(PID_FILE_NET_V3_TRANSITION); \ fi + @rm -f server.log + @echo "Server stopped" # Start .NET V3 transition server in background start-net-v3-transition-server: - @echo "Starting .NET V3 server..." + @echo "Starting .NET V3 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 & echo $! > net-v3-transition-server.pid - @echo ".NET V3 server starting..." + dotnet run > server.log 2>&1 & echo $$! > $(PID_FILE_NET_V3_TRANSITION) + @echo ".NET V3 transition server starting..." wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3_TRANSITION) 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 56008baf..ae9327f0 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 56008baf1ef63b084a01a30db69af32e870a655b +Subproject commit ae9327f0e21999ac263bc82ae553839230ee9117 diff --git a/test-server/net-v4-server/Makefile b/test-server/net-v4-server/Makefile index 49e4db32..e2df658a 100644 --- a/test-server/net-v4-server/Makefile +++ b/test-server/net-v4-server/Makefile @@ -5,14 +5,23 @@ PID_FILE_NET_V4 := net-V4-server.pid PORT_NET_V4 := 8090 +build-server: + @echo "Building .NET V4 improved server..." + dotnet build + start-server: $(MAKE) start-net-V4-server; stop-server: + @echo "Stopping .NET V4 Improved server on port $(PORT_NET_V4)..." + @lsof -ti:$(PORT_NET_V4) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE_NET_V4) ]; then \ - kill $$(cat $(PID_FILE_NET_V4)) 2>/dev/null || true; \ - rm $(PID_FILE_NET_V4); \ + pkill -P $$(cat $(PID_FILE_NET_V4)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE_NET_V4)) 2>/dev/null || true; \ + rm -f $(PID_FILE_NET_V4); \ fi + @rm -f server.log + @echo "Server stopped" # Start .NET V4 server in background # This builds first into bin/V4 and runs through dll @@ -23,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 & echo $! > net-v3-transition-server.pid + dotnet run --no-build & 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 691d22a5..5178af52 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 691d22a504184fd71f2dae7fd354bd669b58cc07 +Subproject commit 5178af527160bdb66cdbee4d04faa900bf8032f7 diff --git a/test-server/php-v2-server/Makefile b/test-server/php-v2-server/Makefile index adb63258..a9d04134 100644 --- a/test-server/php-v2-server/Makefile +++ b/test-server/php-v2-server/Makefile @@ -1,24 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8087 +build-server: + @echo "Building PHP V2 server..." + composer install + start-server: @echo "Starting PHP V2 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" \ - composer run start & echo $$! > $(PID_FILE) + composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V2 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/php-v2-transition-server/Makefile b/test-server/php-v2-transition-server/Makefile index 2544679d..61eb3a84 100644 --- a/test-server/php-v2-transition-server/Makefile +++ b/test-server/php-v2-transition-server/Makefile @@ -1,24 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8099 +build-server: + @echo "Building PHP V2 Transition server..." + composer install + start-server: @echo "Starting PHP 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" \ - composer run start & echo $$! > $(PID_FILE) + composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V2 Transition server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk deleted file mode 120000 index 4610ddb9..00000000 --- a/test-server/php-v2-transition-server/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -../php-v3-server/local-php-sdk \ No newline at end of file diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk new file mode 160000 index 00000000..8e22105d --- /dev/null +++ b/test-server/php-v2-transition-server/local-php-sdk @@ -0,0 +1 @@ +Subproject commit 8e22105d4e10d3410a0d3390b18bad74fe21be00 diff --git a/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers b/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers new file mode 120000 index 00000000..4610ddb9 --- /dev/null +++ b/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers @@ -0,0 +1 @@ +../php-v3-server/local-php-sdk \ No newline at end of file diff --git a/test-server/php-v3-server/Makefile b/test-server/php-v3-server/Makefile index 0ec40802..2b9661f2 100644 --- a/test-server/php-v3-server/Makefile +++ b/test-server/php-v3-server/Makefile @@ -1,24 +1,33 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8093 +build-server: + @echo "Building PHP V3 server..." + composer install + start-server: @echo "Starting PHP V3 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" \ - composer run start & echo $$! > $(PID_FILE) + composer run start > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "PHP V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index 2cb0cda2..8e22105d 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 2cb0cda27812fae3f16e719df4b22b5c08526148 +Subproject commit 8e22105d4e10d3410a0d3390b18bad74fe21be00 diff --git a/test-server/python-v3-server/Makefile b/test-server/python-v3-server/Makefile index 0468dc87..930c950c 100644 --- a/test-server/python-v3-server/Makefile +++ b/test-server/python-v3-server/Makefile @@ -1,28 +1,36 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8081 -start-server: - @echo "Starting Python V3 server..." +build-server: + @echo "Building Python V3 server..." python -m venv .venv .venv/bin/python -m ensurepip .venv/bin/python -m pip install -e . .venv/bin/python -m pip install -e ../.. + +start-server: + @echo "Starting Python V3 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" \ - .venv/bin/python src/main.py & echo $$! > $(PID_FILE) + .venv/bin/python src/main.py > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Python V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/ruby-v2-server/Makefile b/test-server/ruby-v2-server/Makefile index f4297eac..e0f938fc 100644 --- a/test-server/ruby-v2-server/Makefile +++ b/test-server/ruby-v2-server/Makefile @@ -1,29 +1,37 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8098 +build-server: + @echo "Building Ruby V2 server..." + bundle install + start-server: @if [ -f $(PID_FILE) ]; then \ echo "❌ Error: Server already running. Stop before starting."; \ exit 1; \ fi; @echo "Starting Ruby V2 server..." - bundle install 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" \ - PORT=$(PORT) bundle exec ruby app.rb & echo $$! > $(PID_FILE) + PORT=$(PORT) bundle exec ruby app.rb > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Ruby V2 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index 9bffb525..582e0241 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 9bffb525765b2aeebd6203072bf6e94d2ab53f90 +Subproject commit 582e02418ac2c5540c48dd089a7db506712e6f94 diff --git a/test-server/ruby-v3-server/Makefile b/test-server/ruby-v3-server/Makefile index ec463bad..331abac5 100644 --- a/test-server/ruby-v3-server/Makefile +++ b/test-server/ruby-v3-server/Makefile @@ -1,29 +1,37 @@ # Makefile for S3 Encryption Client Testing -.PHONY: start-server stop-server wait-for-server +.PHONY: build-server start-server stop-server wait-for-server PID_FILE := server.pid PORT := 8092 +build-server: + @echo "Building Ruby V3 server..." + bundle install + start-server: @if [ -f $(PID_FILE) ]; then \ echo "❌ Error: Server already running. Stop before starting."; \ exit 1; \ fi; @echo "Starting Ruby V3 server..." - bundle install 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" \ - PORT=$(PORT) bundle exec ruby app.rb & echo $$! > $(PID_FILE) + PORT=$(PORT) bundle exec ruby app.rb > server.log 2>&1 & echo $$! > $(PID_FILE) @echo "Ruby V3 server starting..." stop-server: + @echo "Stopping server on port $(PORT)..." + @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true @if [ -f $(PID_FILE) ]; then \ - kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm $(PID_FILE); \ + pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ + kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm -f $(PID_FILE); \ fi + @rm -f server.log + @echo "Server stopped" wait-for-server: $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 9bffb525..582e0241 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 9bffb525765b2aeebd6203072bf6e94d2ab53f90 +Subproject commit 582e02418ac2c5540c48dd089a7db506712e6f94 diff --git a/test-server/specification b/test-server/specification index 129b9c5e..1f1ae8bb 160000 --- a/test-server/specification +++ b/test-server/specification @@ -1 +1 @@ -Subproject commit 129b9c5e53a8c4f6be10a52c9d3dcdf765000d78 +Subproject commit 1f1ae8bb2b7b082b87ffbf4916a9723e531b2052 From b785c9f94c49f180d4fcdabf61057d34e22945b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:58:07 -0800 Subject: [PATCH 148/201] chore: update php to use iv of 1s (#89) * chore: update php to use iv of 1s * chore: enable php testing * fix up php dirs --- .github/workflows/test.yml | 10 +++++----- .gitmodules | 4 ---- test-server/php-v2-transition-server/local-php-sdk | 2 +- .../local-php-sdk~fireegg-test-servers | 1 - test-server/php-v3-server/local-php-sdk | 2 +- 5 files changed, 7 insertions(+), 12 deletions(-) mode change 160000 => 120000 test-server/php-v2-transition-server/local-php-sdk delete mode 120000 test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2c43191..febccd0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,27 +156,27 @@ jobs: aws-region: us-west-2 - name: Build the servers - run: cd test-server && make build-all-servers FILTER=ruby,go,cpp + run: cd test-server && make build-all-servers FILTER=ruby,go,cpp,php 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,go,cpp + run: cd test-server && make start-all-servers FILTER=ruby,go,cpp,php 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,go,cpp + run: cd test-server && make wait-all-servers FILTER=ruby,go,cpp,php 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: Run run-tests - run: cd test-server && make run-tests FILTER=ruby,go,cpp + run: cd test-server && make run-tests FILTER=ruby,go,cpp,php env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} @@ -194,7 +194,7 @@ jobs: test-server/*/net-v3-server.log - name: Stop the servers - run: cd test-server && make stop-servers FILTER=ruby,go,cpp + run: cd test-server && make stop-servers FILTER=ruby,go,cpp,php - name: Upload results if: always() diff --git a/.gitmodules b/.gitmodules index 51e8e255..1e395ed8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,10 +28,6 @@ path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git branch = fire-egg-staging -[submodule "test-server/php-v2-transition-server/local-php-sdk"] - path = test-server/php-v2-transition-server/local-php-sdk - url = git@github.com:aws/private-aws-sdk-php-staging.git - branch = s3ec/transitional [submodule "test-server/net-v2-v3-server/s3ec-net-v2"] path = test-server/net-v2-v3-server/s3ec-net-v2 url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk deleted file mode 160000 index 8e22105d..00000000 --- a/test-server/php-v2-transition-server/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8e22105d4e10d3410a0d3390b18bad74fe21be00 diff --git a/test-server/php-v2-transition-server/local-php-sdk b/test-server/php-v2-transition-server/local-php-sdk new file mode 120000 index 00000000..4610ddb9 --- /dev/null +++ b/test-server/php-v2-transition-server/local-php-sdk @@ -0,0 +1 @@ +../php-v3-server/local-php-sdk \ No newline at end of file diff --git a/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers b/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers deleted file mode 120000 index 4610ddb9..00000000 --- a/test-server/php-v2-transition-server/local-php-sdk~fireegg-test-servers +++ /dev/null @@ -1 +0,0 @@ -../php-v3-server/local-php-sdk \ No newline at end of file diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index 8e22105d..c81d43cd 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 8e22105d4e10d3410a0d3390b18bad74fe21be00 +Subproject commit c81d43cd86764642ed83b1457f0f3ae87b052d23 From d948160ef53c85135a6f7a23e242ee8d50af1e2a Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:22:47 -0500 Subject: [PATCH 149/201] bump aws-sdk version (#90) --- 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 9a368aa8..5276e9ec 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 9a368aa8f6bbcd75eb1180d5d76d41936c68ed6a +Subproject commit 5276e9ec0fbe6d296c5941ae6bbf6d401063c607 diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 9a368aa8..5276e9ec 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 9a368aa8f6bbcd75eb1180d5d76d41936c68ed6a +Subproject commit 5276e9ec0fbe6d296c5941ae6bbf6d401063c607 From a4dc46fc4e5b7285f6965e4540a929bdfca9b484 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:10:57 -0800 Subject: [PATCH 150/201] chore: enable java-v3-transition test server (#70) * chore: s3ec v3 transtion and v4 improved tests * comment cpp checkout * bump s3ec-java commits * chore: add duvet reports for s3ec-java (transition & improved) * format * git-ignore * update java submodule * fix configuration * Revert "chore: reenable c++ (#52)" This reverts commit 8c6db9eb44b082a28ac143ae9824fe272a3e2166. * remove java transiton for now * fix configuration * fix configuration * Update test configuration * Duvet * Rebase * nit - format * Change java-v4-port * duvet changes * Dotnet change * remove symlink * Fix Tests * chore: enable java-v3-transition test server * chore: enable java-v3-transition test server * update .gitmodule branch * Merge Conflicts * chore: update client configuration to allow for default. * point iv's changes commit * Apply suggestion from @rishav-karanjit Co-authored-by: Rishav karanjit * allow java * fix duvet --------- Co-authored-by: Rishav karanjit --- .github/workflows/test.yml | 10 +- .gitmodules | 3 +- .../amazon/encryption/s3/TestUtils.java | 5 +- test-server/java-v3-server/.duvet/config.toml | 2 +- .../.duvet/config.toml | 2 +- .../build.gradle.kts | 2 +- .../java-v3-transition-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 271 +++++++++--------- .../encryption/s3/GetObjectOperationImpl.java | 88 +++--- .../amazon/encryption/s3/MetadataUtils.java | 54 ++-- .../encryption/s3/PutObjectOperationImpl.java | 58 ++-- .../encryption/s3/S3ECJavaTestServer.java | 17 +- test-server/java-v4-server/.duvet/config.toml | 2 +- test-server/java-v4-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 267 +++++++++-------- .../encryption/s3/GetObjectOperationImpl.java | 94 +++--- .../amazon/encryption/s3/MetadataUtils.java | 54 ++-- .../encryption/s3/PutObjectOperationImpl.java | 65 ++--- .../encryption/s3/S3ECJavaTestServer.java | 20 +- 19 files changed, 507 insertions(+), 511 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index febccd0c..49d8d05c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,27 +156,27 @@ jobs: aws-region: us-west-2 - name: Build the servers - run: cd test-server && make build-all-servers FILTER=ruby,go,cpp,php + run: cd test-server && make build-all-servers FILTER=ruby,go,cpp,php,java 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,go,cpp,php + run: cd test-server && make start-all-servers FILTER=ruby,go,cpp,php,java 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,go,cpp,php + run: cd test-server && make wait-all-servers FILTER=ruby,go,cpp,php,java 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: Run run-tests - run: cd test-server && make run-tests FILTER=ruby,go,cpp,php + run: cd test-server && make run-tests FILTER=ruby,go,cpp,php,java env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} @@ -194,7 +194,7 @@ jobs: test-server/*/net-v3-server.log - name: Stop the servers - run: cd test-server && make stop-servers FILTER=ruby,go,cpp,php + run: cd test-server && make stop-servers FILTER=ruby,go,cpp,php,java - name: Upload results if: always() diff --git a/.gitmodules b/.gitmodules index 1e395ed8..3adca9cb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,12 +18,11 @@ [submodule "test-server/java-v3-transition-server/s3ec-staging"] path = test-server/java-v3-transition-server/s3ec-staging url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git - branch = imabhichow/s3ec-transition + branch = imabhichow/transition-read-kc [submodule "test-server/java-v4-server/s3ec-staging"] path = test-server/java-v4-server/s3ec-staging url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git branch = imabhichow/add-kc -; branch = s3ec/improved [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git 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 c20fd5a6..b60eeb04 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 @@ -134,7 +134,7 @@ public class TestUtils { public static final Set TRANSITION_VERSIONS = Set.of( - // JAVA_V3_TRANSITION, + JAVA_V3_TRANSITION, GO_V3_TRANSITION, // NET_V2_TRANSITION, NET_V3_TRANSITION, @@ -172,8 +172,7 @@ public class TestUtils { servers.put(NET_V4, new LanguageServerTarget(NET_V4, "8090")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); - // TODO: Create and add transition servers - // servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); + 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(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); diff --git a/test-server/java-v3-server/.duvet/config.toml b/test-server/java-v3-server/.duvet/config.toml index f03424b2..988fb5fa 100644 --- a/test-server/java-v3-server/.duvet/config.toml +++ b/test-server/java-v3-server/.duvet/config.toml @@ -1,7 +1,7 @@ '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" [[source]] -pattern = "s3ec-staging/*.java" +pattern = "s3ec-staging/**/*.java" # Include required specifications here [[specification]] diff --git a/test-server/java-v3-transition-server/.duvet/config.toml b/test-server/java-v3-transition-server/.duvet/config.toml index f29cd058..65605eaa 100644 --- a/test-server/java-v3-transition-server/.duvet/config.toml +++ b/test-server/java-v3-transition-server/.duvet/config.toml @@ -1,7 +1,7 @@ '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" [[source]] -pattern = "s3ec-staging/*.java" +pattern = "s3ec-staging/**/*.java" # Include required specifications here [[specification]] diff --git a/test-server/java-v3-transition-server/build.gradle.kts b/test-server/java-v3-transition-server/build.gradle.kts index 5b3a9234..7f249d65 100644 --- a/test-server/java-v3-transition-server/build.gradle.kts +++ b/test-server/java-v3-transition-server/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") // S3EC from local Maven repository (installed by mvn install) - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-TRANSITION") + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-read-kc") } // Use that application plugin to start the service via the `run` task. diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging index c8527040..6413811b 160000 --- a/test-server/java-v3-transition-server/s3ec-staging +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -1 +1 @@ -Subproject commit c852704026ebc1c46bd11b6d5ab6d9a37ec1985d +Subproject commit 6413811bb81037999b8238e02047e0e403f78c1f 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 3ef7ee60..425c0334 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,9 +1,8 @@ package software.amazon.encryption.s3; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.internal.InstructionFileConfig; -import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.materials.AesKeyring; import software.amazon.encryption.s3.materials.Keyring; import software.amazon.encryption.s3.materials.KmsKeyring; @@ -31,148 +30,150 @@ import java.util.UUID; import static software.amazon.encryption.s3.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT; -import static software.amazon.encryption.s3.model.EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; +import static software.amazon.encryption.s3.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT; +import static software.amazon.encryption.s3.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT; public class CreateClientOperationImpl implements CreateClientOperation { - private Map clientCache_; - - public CreateClientOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - - // Copied from S3EC. - private boolean onlyOneNonNull(Object... values) { - boolean haveOneNonNull = false; - for (Object o : values) { - if (o != null) { - if (haveOneNonNull) { - return false; + private final Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } } - haveOneNonNull = true; - } + return haveOneNonNull; } - return haveOneNonNull; - } - - @Override - public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { - try { - KeyMaterial key = input.getConfig().getKeyMaterial(); - if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { - throw new RuntimeException("KeyMaterial must be only one, non-null input!"); - } - Keyring keyring; - if (key.getAesKey() != null) { - byte[] keyBytes = new byte[key.getAesKey().remaining()]; - key.getAesKey().get(keyBytes); - keyring = AesKeyring.builder() - .wrappingKey(new SecretKeySpec(keyBytes, "AES")) - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .build(); - } else if (key.getRsaKey() != null) { + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { try { - byte[] keyBytes = new byte[key.getRsaKey().remaining()]; - key.getRsaKey().get(keyBytes); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); - RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( - privateKey.getModulus(), - privateKey.getPublicExponent() - ); - - // Generate public key - PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - - keyring = RsaKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyPair(PartialRsaKeyPair.builder() - .publicKey(publicKey) - .privateKey(privateKey).build()) - .build(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw GenericServerError.builder() - .message(nse.getMessage()) - .build(); + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + + // Generate public key + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(publicKey) + .privateKey(privateKey).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw GenericServerError.builder() + .message(nse.getMessage()) + .build(); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + + // V3 Transition server configuration + // Existing Builder defaults to FORBID_ENCRYPT and ALG_AES_256_GCM_IV12_TAG16_NO_KDF + S3EncryptionClient.Builder s3ClientBuilder = S3EncryptionClient.builder() + .keyring(keyring) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()); + + // Instruction File Put Configuration + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + s3ClientBuilder.instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()); + } + + // Configure commitment policy if provided + if (input.getConfig().getCommitmentPolicy() != null) { + CommitmentPolicy policy = getCommitmentPolicy(input.getConfig().getCommitmentPolicy()); + s3ClientBuilder.commitmentPolicy(policy); + } + + // Configure encryption algorithm if provided + if (input.getConfig().getEncryptionAlgorithm() != null) { + AlgorithmSuite algorithm = getAlgorithmSuite(input.getConfig().getEncryptionAlgorithm()); + s3ClientBuilder.encryptionAlgorithm(algorithm); + } + + S3Client s3Client = s3ClientBuilder.build(); + + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); } - } else if (key.getKmsKeyId() != null) { - keyring = KmsKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyId(key.getKmsKeyId()) - .build(); - } else { - throw new RuntimeException("No KeyMaterial found!"); - } - - boolean instFilePut = false; - if (input.getConfig().getInstructionFileConfig() != null) { - instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); - } - - // V3-Transitional server configuration - S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builder() - .instructionFileConfig(InstructionFileConfig.builder() - .instructionFileClient(S3Client.create()) - .enableInstructionFilePutObject(instFilePut) - .build()) - .keyring(keyring); - - // Configure commitment policy if provided ( feature) - if (input.getConfig().getCommitmentPolicy() != null) { - CommitmentPolicy policy = getCommitmentPolicy(input); - clientBuilder.commitmentPolicy(policy); - } - // V3-Transitional default: No commitment policy (null) for backward compatibility - - // Configure encryption algorithm if provided ( feature) - if (input.getConfig().getEncryptionAlgorithm() != null) { - AlgorithmSuite algorithm = getAlgorithmSuite(input); - clientBuilder.encryptionAlgorithm(algorithm); - } else { - // V3-Transitional default: Legacy algorithm for backward compatibility - clientBuilder.encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); - } - - S3Client s3Client = clientBuilder.build(); - UUID uuid = UUID.randomUUID(); - String uuidString = uuid.toString(); - clientCache_.put(uuidString, s3Client); - return CreateClientOutput.builder() - .clientId(uuidString) - .build(); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - String stackTrace = sw.toString(); - throw GenericServerError.builder() - .message(stackTrace) - .build(); } - } - - private static AlgorithmSuite getAlgorithmSuite(CreateClientInput input) { - if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { - return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; - } else if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { - return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; - } else if (input.getConfig().getEncryptionAlgorithm().equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { - return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; - } else { - throw new RuntimeException("Unknown encryption algorithm: " + input.getConfig().getEncryptionAlgorithm()); + + private static AlgorithmSuite getAlgorithmSuite(EncryptionAlgorithm input) { + if (input.equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { + return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + } else { + throw new RuntimeException("Unknown encryption algorithm: " + input); + } } - } - - private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(CreateClientInput input) { - if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { - return FORBID_ENCRYPT_ALLOW_DECRYPT; - } else if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { - return null; - } else if (input.getConfig().getCommitmentPolicy().equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { - return null; - } else { - throw new RuntimeException("Unknown commitment policy: " + input.getConfig().getCommitmentPolicy()); + + private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(software.amazon.encryption.s3.model.CommitmentPolicy input) { + if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { + return FORBID_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { + return REQUIRE_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { + return REQUIRE_ENCRYPT_REQUIRE_DECRYPT; + } else { + throw new RuntimeException("Unknown commitment policy: " + input); + } } - } } 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 e7c5493f..86749489 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 @@ -23,50 +23,52 @@ import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; public class GetObjectOperationImpl implements GetObjectOperation { - private Map clientCache_; - public GetObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - @Override - public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { - try { - S3Client s3Client = clientCache_.get(input.getClientID()); - Map ecMap = metadataListToMap(input.getMetadata()); + private Map clientCache_; - try { - ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(ecMap))); + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + 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))); - List mdAsList = metadataMapToList(resp.response().metadata()); - // Can't use asBB else it gets mad bc cant access backing array - ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); - GetObjectOutput output = GetObjectOutput.builder() - .body(bb) - .metadata(mdAsList) - .build(); - return output; - } 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(); + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } 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/MetadataUtils.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java index 036289ec..9eba6a3d 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -9,35 +9,35 @@ public class MetadataUtils { - /** - * Annoyingly, Smithy doesn't provide an interface for map types - * in HTTP headers, so we have to do the serde ourselves - */ - public static List metadataMapToList(Map md) { - List mdAsList = new ArrayList<>(md.size()); - for (Map.Entry keyValue : md.entrySet()) { - mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; } - return mdAsList; - } - public static Map metadataListToMap(List mdList) { - Map md = new HashMap<>(); - for (String entry : mdList) { - // Split on "]:[" to separate key and value - String[] parts = entry.split("]:\\["); - if (parts.length == 2) { - // Remove remaining brackets from start and end - String key = parts[0].substring(1); - String value = parts[1].substring(0, parts[1].length() - 1); - md.put(key, value); - } else { - throw GenericServerError.builder() - .message("Malformed metadata list entry: " + entry) - .build(); - } + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; } - return md; - } } diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java index 4c772673..ca76e83f 100644 --- a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -20,36 +20,36 @@ public class PutObjectOperationImpl implements PutObjectOperation { - private Map clientCache_; + private Map clientCache_; - public PutObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } - @Override - public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { - try { - final Map metadata = metadataListToMap(input.getMetadata()); - S3Client s3Client = clientCache_.get(input.getClientID()); - s3Client.putObject(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(metadata)), - RequestBody.fromByteBuffer(input.getBody()) - ); - // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway - return PutObjectOutput.builder() - .bucket(input.getBucket()) - .key(input.getKey()) - .metadata(input.getMetadata()) - .build(); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - String stackTrace = sw.toString(); - throw GenericServerError.builder() - .message(stackTrace) - .build(); + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception 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 32af5fc1..a992cabd 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 @@ -11,6 +11,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; + import software.amazon.smithy.java.server.Server; import software.amazon.encryption.s3.service.S3ECTestServer; @@ -29,14 +30,14 @@ public void run() { Map clientCache = new ConcurrentHashMap<>(); Server server = Server.builder() - .endpoints(endpoint) - .addService( - S3ECTestServer.builder() - .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) - .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) - .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) - .build()) - .build(); + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); System.out.println("Starting server..."); server.start(); try { diff --git a/test-server/java-v4-server/.duvet/config.toml b/test-server/java-v4-server/.duvet/config.toml index f29cd058..65605eaa 100644 --- a/test-server/java-v4-server/.duvet/config.toml +++ b/test-server/java-v4-server/.duvet/config.toml @@ -1,7 +1,7 @@ '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" [[source]] -pattern = "s3ec-staging/*.java" +pattern = "s3ec-staging/**/*.java" # Include required specifications here [[specification]] diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging index c101d149..db0c743e 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit c101d14946a8279387b73482a31c03c2f269c9a4 +Subproject commit db0c743eec335d16e6dceaf2b09d84becb0f74f8 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 ab85df62..cb20d5ac 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,6 +1,5 @@ package software.amazon.encryption.s3; -import software.amazon.awssdk.core.traits.Trait; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.encryption.s3.internal.InstructionFileConfig; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; @@ -35,147 +34,145 @@ import static software.amazon.encryption.s3.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT; public class CreateClientOperationImpl implements CreateClientOperation { - private Map clientCache_; - - public CreateClientOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - - // Copied from S3EC. - private boolean onlyOneNonNull(Object... values) { - boolean haveOneNonNull = false; - for (Object o : values) { - if (o != null) { - if (haveOneNonNull) { - return false; + private final Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } } - haveOneNonNull = true; - } + return haveOneNonNull; } - return haveOneNonNull; - } - - @Override - public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { - try { - KeyMaterial key = input.getConfig().getKeyMaterial(); - if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { - throw new RuntimeException("KeyMaterial must be only one, non-null input!"); - } - Keyring keyring; - if (key.getAesKey() != null) { - byte[] keyBytes = new byte[key.getAesKey().remaining()]; - key.getAesKey().get(keyBytes); - keyring = AesKeyring.builder() - .wrappingKey(new SecretKeySpec(keyBytes, "AES")) - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .build(); - } else if (key.getRsaKey() != null) { + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { try { - byte[] keyBytes = new byte[key.getRsaKey().remaining()]; - key.getRsaKey().get(keyBytes); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); - RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( - privateKey.getModulus(), - privateKey.getPublicExponent() - ); - - // Generate public key - PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - - keyring = RsaKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyPair(PartialRsaKeyPair.builder() - .publicKey(publicKey) - .privateKey(privateKey).build()) - .build(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw GenericServerError.builder() - .message(nse.getMessage()) - .build(); + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + privateKey.getModulus(), + privateKey.getPublicExponent() + ); + + // Generate public key + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(publicKey) + .privateKey(privateKey).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw GenericServerError.builder() + .message(nse.getMessage()) + .build(); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + + // V4-Improved server configuration + S3EncryptionClient.Builder s3ClientBuilder = S3EncryptionClient.builderV4() + .keyring(keyring) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()); + + // Client Creation + boolean instFilePut = false; + if (input.getConfig().getInstructionFileConfig() != null) { + instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); + s3ClientBuilder.instructionFileConfig(InstructionFileConfig.builder() + .instructionFileClient(S3Client.create()) + .enableInstructionFilePutObject(instFilePut) + .build()); + } + + // Configure commitment policy if provided + if (input.getConfig().getCommitmentPolicy() != null) { + CommitmentPolicy policy = getCommitmentPolicy(input.getConfig().getCommitmentPolicy()); + s3ClientBuilder.commitmentPolicy(policy); + } + + // Configure encryption algorithm if provided + if (input.getConfig().getEncryptionAlgorithm() != null) { + AlgorithmSuite algorithm = getAlgorithmSuite(input.getConfig().getEncryptionAlgorithm()); + s3ClientBuilder.encryptionAlgorithm(algorithm); + } + + S3Client s3Client = s3ClientBuilder.build(); + + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); } - } else if (key.getKmsKeyId() != null) { - keyring = KmsKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyId(key.getKmsKeyId()) - .build(); - } else { - throw new RuntimeException("No KeyMaterial found!"); - } - - - // Client Creation - boolean instFilePut = false; - if (input.getConfig().getInstructionFileConfig() != null) { - instFilePut = input.getConfig().getInstructionFileConfig().isEnableInstructionFilePutObject(); - } - - // Configure commitment policy if provided - software.amazon.encryption.s3.CommitmentPolicy policy = CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT; - if (input.getConfig().getCommitmentPolicy() != null) { - policy = getCommitmentPolicy(input.getConfig().getCommitmentPolicy()); - } - - // Configure encryption algorithm if provided - AlgorithmSuite algorithm = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; - if (input.getConfig().getEncryptionAlgorithm() != null) { - algorithm = getAlgorithmSuite(input.getConfig().getEncryptionAlgorithm()); - } - - // V4-Improved server configuration - S3EncryptionClient s3Client = S3EncryptionClient.builderV4() - .instructionFileConfig(InstructionFileConfig.builder() - .instructionFileClient(S3Client.create()) - .enableInstructionFilePutObject(instFilePut) - .build()) - .keyring(keyring) - .commitmentPolicy(policy) - .encryptionAlgorithm(algorithm) - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .enableLegacyUnauthenticatedModes(input.getConfig().isEnableLegacyUnauthenticatedModes()) - .build(); - - UUID uuid = UUID.randomUUID(); - String uuidString = uuid.toString(); - clientCache_.put(uuidString, s3Client); - return CreateClientOutput.builder() - .clientId(uuidString) - .build(); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - String stackTrace = sw.toString(); - throw GenericServerError.builder() - .message(stackTrace) - .build(); } - } - - private static AlgorithmSuite getAlgorithmSuite(EncryptionAlgorithm input) { - if (input.equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { - return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; - } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { - return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; - } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { - return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; - } else { - throw new RuntimeException("Unknown encryption algorithm: " + input); + + private static AlgorithmSuite getAlgorithmSuite(EncryptionAlgorithm input) { + if (input.equals(EncryptionAlgorithm.ALG_AES_256_CBC_IV16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)) { + return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + } else if (input.equals(EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)) { + return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + } else { + throw new RuntimeException("Unknown encryption algorithm: " + input); + } } - } - - private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(software.amazon.encryption.s3.model.CommitmentPolicy input) { - if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { - return FORBID_ENCRYPT_ALLOW_DECRYPT; - } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { - return REQUIRE_ENCRYPT_ALLOW_DECRYPT; - } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { - return REQUIRE_ENCRYPT_REQUIRE_DECRYPT; - } else { - throw new RuntimeException("Unknown commitment policy: " + input); + + private static software.amazon.encryption.s3.CommitmentPolicy getCommitmentPolicy(software.amazon.encryption.s3.model.CommitmentPolicy input) { + if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)) { + return FORBID_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)) { + return REQUIRE_ENCRYPT_ALLOW_DECRYPT; + } else if (input.equals(software.amazon.encryption.s3.model.CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)) { + return REQUIRE_ENCRYPT_REQUIRE_DECRYPT; + } else { + throw new RuntimeException("Unknown commitment policy: " + input); + } } - } } 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 e7c5493f..17e9a8ee 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 @@ -3,70 +3,70 @@ import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.encryption.s3.S3EncryptionClientException; -import software.amazon.smithy.java.server.RequestContext; import software.amazon.encryption.s3.model.GenericServerError; import software.amazon.encryption.s3.model.GetObjectInput; import software.amazon.encryption.s3.model.GetObjectOutput; import software.amazon.encryption.s3.model.S3EncryptionClientError; import software.amazon.encryption.s3.service.GetObjectOperation; +import software.amazon.smithy.java.server.RequestContext; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; public class GetObjectOperationImpl implements GetObjectOperation { - private Map clientCache_; - public GetObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - @Override - public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { - try { - S3Client s3Client = clientCache_.get(input.getClientID()); - Map ecMap = metadataListToMap(input.getMetadata()); + private final Map clientCache_; + + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + 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))); - List mdAsList = metadataMapToList(resp.response().metadata()); - // Can't use asBB else it gets mad bc cant access backing array - ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); - GetObjectOutput output = GetObjectOutput.builder() - .body(bb) - .metadata(mdAsList) - .build(); - return output; - } 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(); + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } 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-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java index 036289ec..9eba6a3d 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -9,35 +9,35 @@ public class MetadataUtils { - /** - * Annoyingly, Smithy doesn't provide an interface for map types - * in HTTP headers, so we have to do the serde ourselves - */ - public static List metadataMapToList(Map md) { - List mdAsList = new ArrayList<>(md.size()); - for (Map.Entry keyValue : md.entrySet()) { - mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; } - return mdAsList; - } - public static Map metadataListToMap(List mdList) { - Map md = new HashMap<>(); - for (String entry : mdList) { - // Split on "]:[" to separate key and value - String[] parts = entry.split("]:\\["); - if (parts.length == 2) { - // Remove remaining brackets from start and end - String key = parts[0].substring(1); - String value = parts[1].substring(0, parts[1].length() - 1); - md.put(key, value); - } else { - throw GenericServerError.builder() - .message("Malformed metadata list entry: " + entry) - .build(); - } + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; } - return md; - } } diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java index 4c772673..d399f13d 100644 --- a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -2,54 +2,51 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.smithy.java.server.RequestContext; import software.amazon.encryption.s3.model.GenericServerError; import software.amazon.encryption.s3.model.PutObjectInput; import software.amazon.encryption.s3.model.PutObjectOutput; import software.amazon.encryption.s3.service.PutObjectOperation; +import software.amazon.smithy.java.server.RequestContext; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; import java.util.Map; -import java.util.stream.Collectors; -import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; public class PutObjectOperationImpl implements PutObjectOperation { - private Map clientCache_; + private final Map clientCache_; - public PutObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } - @Override - public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { - try { - final Map metadata = metadataListToMap(input.getMetadata()); - S3Client s3Client = clientCache_.get(input.getClientID()); - s3Client.putObject(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(metadata)), - RequestBody.fromByteBuffer(input.getBody()) - ); - // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway - return PutObjectOutput.builder() - .bucket(input.getBucket()) - .key(input.getKey()) - .metadata(input.getMetadata()) - .build(); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - String stackTrace = sw.toString(); - throw GenericServerError.builder() - .message(stackTrace) - .build(); + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception 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-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 c3fee4f1..d394b72b 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 @@ -6,13 +6,13 @@ package software.amazon.encryption.s3; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.service.S3ECTestServer; +import software.amazon.smithy.java.server.Server; import java.net.URI; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; -import software.amazon.smithy.java.server.Server; -import software.amazon.encryption.s3.service.S3ECTestServer; public class S3ECJavaTestServer implements Runnable { static final URI endpoint = URI.create("http://localhost:8088"); @@ -29,14 +29,14 @@ public void run() { Map clientCache = new ConcurrentHashMap<>(); Server server = Server.builder() - .endpoints(endpoint) - .addService( - S3ECTestServer.builder() - .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) - .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) - .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) - .build()) - .build(); + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); System.out.println("Starting server..."); server.start(); try { From af3811ed70dd75db9688aa7d26f8a520fa7f64fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:42:52 -0800 Subject: [PATCH 151/201] chore: enable php instruction file tests (#87) --- .../it/java/software/amazon/encryption/s3/RoundTripTests.java | 3 --- .../src/it/java/software/amazon/encryption/s3/TestUtils.java | 4 ---- test-server/php-v3-server/local-php-sdk | 2 +- 3 files changed, 1 insertion(+), 8 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 1167e4db..59382006 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 @@ -548,9 +548,6 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(decLang.getLanguageName())) { throw new TestAbortedException("not testing " + encLang.getLanguageName()); } - if (INSTRUCTION_FILE_ROUNDTRIP_TEMP_UNSUPPORTED.contains(encLang.getLanguageName())) { - throw new TestAbortedException("not testing " + encLang.getLanguageName()); - } S3ECTestServerClient encClient = testServerClientFor(encLang); S3ECTestServerClient decClient = testServerClientFor(decLang); final String objectKey = appendTestSuffix(String.format("write-%s-read-%s-instruction-file", encLang.getLanguageName(), decLang.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 b60eeb04..7aa812bb 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 @@ -117,10 +117,6 @@ public class TestUtils { public static final Set INSTRUCTION_FILE_GET_UNSUPPORTED = Set.of(PYTHON_V3); - // PHP doesn't work but it should, temporarily disable - public static final Set INSTRUCTION_FILE_ROUNDTRIP_TEMP_UNSUPPORTED = - Set.of(PHP_V2_CURRENT, PHP_V2_TRANSITION, PHP_V3); - public static final Set CURRENT_VERSIONS = Set.of( JAVA_V3_CURRENT, diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index c81d43cd..d75f911e 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit c81d43cd86764642ed83b1457f0f3ae87b052d23 +Subproject commit d75f911e41df81a1224f09bb4330c4a1c6c8ed59 From ba8ce27032290d552775434ff7ddd2606bdb09f8 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Thu, 13 Nov 2025 16:49:40 -0800 Subject: [PATCH 152/201] chore: turn on .NET after IV update (#94) --- .github/workflows/test.yml | 10 +++++----- test-server/net-v2-v3-server/Makefile | 1 + .../net-v3-transition-server/s3ec-v3-transition-branch | 2 +- test-server/net-v4-server/s3ec-net-v4-improved | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49d8d05c..880ca3f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,27 +156,27 @@ jobs: aws-region: us-west-2 - name: Build the servers - run: cd test-server && make build-all-servers FILTER=ruby,go,cpp,php,java + 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,go,cpp,php,java + 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,go,cpp,php,java + 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 }} - name: Run run-tests - run: cd test-server && make run-tests FILTER=ruby,go,cpp,php,java + run: cd test-server && make run-tests env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} @@ -194,7 +194,7 @@ jobs: test-server/*/net-v3-server.log - name: Stop the servers - run: cd test-server && make stop-servers FILTER=ruby,go,cpp,php,java + run: cd test-server && make stop-servers - name: Upload results if: always() diff --git a/test-server/net-v2-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile index b50ae4f8..9881ee38 100644 --- a/test-server/net-v2-v3-server/Makefile +++ b/test-server/net-v2-v3-server/Makefile @@ -11,6 +11,7 @@ build-server: @echo "Building .NET V2 and V3 servers..." rm -rf obj/v2 bin/v2 obj/v3 bin/v3 dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ + rm -rf obj dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ start-server: 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 ae9327f0..ad825917 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 ae9327f0e21999ac263bc82ae553839230ee9117 +Subproject commit ad8259173de365a13e8b3932ee02493f599f597f diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index 5178af52..1c0a458c 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 5178af527160bdb66cdbee4d04faa900bf8032f7 +Subproject commit 1c0a458c19b351c266199c72072de746362c5326 From d758421c2b5f9ef9e8da826791f3fc04f4ca3926 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:08:26 -0500 Subject: [PATCH 153/201] bump sdk for duvet (#97) --- 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 5276e9ec..52eeeddd 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 5276e9ec0fbe6d296c5941ae6bbf6d401063c607 +Subproject commit 52eeeddd8c40c1547832781f2e48478afff6a6ad diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 5276e9ec..52eeeddd 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 5276e9ec0fbe6d296c5941ae6bbf6d401063c607 +Subproject commit 52eeeddd8c40c1547832781f2e48478afff6a6ad From a781248bfd5034b6fcffa280e93a7effb82def05 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:30:52 -0800 Subject: [PATCH 154/201] chore: java examples (#88) --- all-examples/java/v3/Makefile | 75 ++++++ all-examples/java/v3/README.md | 57 ++++ all-examples/java/v3/build.gradle.kts | 47 ++++ .../java/v3/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + all-examples/java/v3/gradlew | 249 ++++++++++++++++++ all-examples/java/v3/gradlew.bat | 92 +++++++ all-examples/java/v3/s3ec-staging | 1 + all-examples/java/v3/settings.gradle.kts | 1 + .../amazon/encryption/s3/example/Main.java | 161 +++++++++++ all-examples/java/v4/Makefile | 75 ++++++ all-examples/java/v4/README.md | 57 ++++ all-examples/java/v4/build.gradle.kts | 47 ++++ .../java/v4/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + all-examples/java/v4/gradlew | 249 ++++++++++++++++++ all-examples/java/v4/gradlew.bat | 92 +++++++ all-examples/java/v4/s3ec-staging | 1 + all-examples/java/v4/settings.gradle.kts | 1 + .../amazon/encryption/s3/example/Main.java | 160 +++++++++++ 20 files changed, 1379 insertions(+) create mode 100644 all-examples/java/v3/Makefile create mode 100644 all-examples/java/v3/README.md create mode 100644 all-examples/java/v3/build.gradle.kts create mode 100644 all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar create mode 100644 all-examples/java/v3/gradle/wrapper/gradle-wrapper.properties create mode 100755 all-examples/java/v3/gradlew create mode 100644 all-examples/java/v3/gradlew.bat create mode 120000 all-examples/java/v3/s3ec-staging create mode 100644 all-examples/java/v3/settings.gradle.kts create mode 100644 all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java create mode 100644 all-examples/java/v4/Makefile create mode 100644 all-examples/java/v4/README.md create mode 100644 all-examples/java/v4/build.gradle.kts create mode 100644 all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar create mode 100644 all-examples/java/v4/gradle/wrapper/gradle-wrapper.properties create mode 100755 all-examples/java/v4/gradlew create mode 100644 all-examples/java/v4/gradlew.bat create mode 120000 all-examples/java/v4/s3ec-staging create mode 100644 all-examples/java/v4/settings.gradle.kts create mode 100644 all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java diff --git a/all-examples/java/v3/Makefile b/all-examples/java/v3/Makefile new file mode 100644 index 00000000..81e4228d --- /dev/null +++ b/all-examples/java/v3/Makefile @@ -0,0 +1,75 @@ +# Makefile for S3 Encryption Client Java v3 Example + +# Default target +.PHONY: all install clean run help s3ec-staging + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-java-v3 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install S3 Encryption Client library from source +s3ec-staging: + @echo "[JAVA V3] Installing S3 Encryption Client library from source..." + @cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "[JAVA V3] S3 Encryption Client library installed successfully!" + +# Install dependencies using Gradle +install: s3ec-staging + @echo "[JAVA V3] Installing Java dependencies..." + @chmod +x ./gradlew + @./gradlew build + @echo "[JAVA V3] Dependencies installed successfully!" + +# Clean Gradle artifacts +clean: + @echo "[JAVA V3] Cleaning Gradle artifacts..." + @./gradlew clean + @echo "[JAVA V3] Clean completed!" + +# Run the example with default arguments +run: install + @echo "[JAVA V3] Running S3 Encryption Client v3 Java example..." + @echo "[JAVA V3] Bucket: $(BUCKET_NAME)" + @echo "[JAVA V3] Object Key: $(OBJECT_KEY)" + @echo "[JAVA V3] KMS Key ID: $(KMS_KEY_ID)" + @echo "[JAVA V3] Region: $(AWS_REGION)" + @echo "" + @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" + +# Show help +help: + @echo "S3 Encryption Client Java v3 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " s3ec-staging - Install S3 Encryption Client library from source" + @echo " install - Install Java dependencies using Gradle" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove Gradle artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Java 11+ installed on the system" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v3 library installed in local Maven repository" + @echo " (Install by running: cd s3ec-staging && mvn install)" diff --git a/all-examples/java/v3/README.md b/all-examples/java/v3/README.md new file mode 100644 index 00000000..50e67684 --- /dev/null +++ b/all-examples/java/v3/README.md @@ -0,0 +1,57 @@ +# S3 Encryption Client Java v3 Example + +This example demonstrates how to use the Amazon S3 Encryption Client v3 for Java to perform client-side encryption and decryption of objects. + +## Prerequisites + +1. **Java**: Requires Java 11 or later +2. **Gradle**: The project uses Gradle wrapper (included - `./gradlew`) +3. **Maven**: Required to install the S3 Encryption Client library from source +4. **AWS Credentials**: Configure your AWS credentials using one of the following methods: + - AWS CLI: `aws configure` + - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + - IAM roles (for EC2 instances) +5. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` +6. **S3 Bucket**: An existing S3 bucket where you have read/write permissions + +## Setup + +Install dependencies and build (this automatically installs the S3 Encryption Client library from source): +```bash +make install +``` + +Or manually: +```bash +cd s3ec-staging && mvn clean install && cd - +./gradlew build +``` + +**Note**: This example uses a local library installed in Maven local repository via the symbolic link `s3ec-staging`. + +## Usage + +### Using Make (Recommended) + +Run the example with default parameters: +```bash +make run +``` + +Run with custom parameters: +```bash +make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +``` + +### Manual Usage + +Run the example with the following command: + +```bash +./gradlew run --args=" " +``` + +### Example: + +```bash +./gradlew run --args="my-test-bucket s3ec-java-v3-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" diff --git a/all-examples/java/v3/build.gradle.kts b/all-examples/java/v3/build.gradle.kts new file mode 100644 index 00000000..f0748625 --- /dev/null +++ b/all-examples/java/v3/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + java + application +} + +group = "software.amazon.encryption.s3.example" +version = "1.0.0" + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + // AWS SDK v2 dependencies + implementation(platform("software.amazon.awssdk:bom:2.20.0")) + implementation("software.amazon.awssdk:s3") + implementation("software.amazon.awssdk:kms") + implementation("software.amazon.awssdk:auth") + + // S3 Encryption Client v3 from local Maven repository + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-read-kc") +} + +application { + mainClass.set("software.amazon.encryption.s3.example.Main") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "software.amazon.encryption.s3.example.Main" + } + + // Create a fat jar with all dependencies + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveBaseName.set("s3ec-java-v3-example") +} + +tasks.named("run") { + standardInput = System.`in` +} diff --git a/all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar b/all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/all-examples/java/v3/gradlew.bat b/all-examples/java/v3/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/all-examples/java/v3/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/all-examples/java/v3/s3ec-staging b/all-examples/java/v3/s3ec-staging new file mode 120000 index 00000000..1a52a7b9 --- /dev/null +++ b/all-examples/java/v3/s3ec-staging @@ -0,0 +1 @@ +../../../test-server/java-v3-transition-server/s3ec-staging \ No newline at end of file diff --git a/all-examples/java/v3/settings.gradle.kts b/all-examples/java/v3/settings.gradle.kts new file mode 100644 index 00000000..a4dcbbae --- /dev/null +++ b/all-examples/java/v3/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "s3ec-java-v3-example" diff --git a/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java b/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java new file mode 100644 index 00000000..b0d9c112 --- /dev/null +++ b/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java @@ -0,0 +1,161 @@ +package software.amazon.encryption.s3.example; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.encryption.s3.CommitmentPolicy; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; +import software.amazon.encryption.s3.materials.CryptographicMaterialsManager; +import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; +import software.amazon.encryption.s3.materials.KmsKeyring; + +/** + * Example demonstrating the use of Amazon S3 Encryption Client v3 for Java. + * + * This example shows how to: + * 1. Initialize the S3 Encryption Client with KMS keyring + * 2. Encrypt and upload an object to S3 + * 3. Download and decrypt the object + * 4. Verify the roundtrip encryption/decryption + */ +public class Main { + + public static void main(String[] args) { + // Check command line arguments + if (args.length != 4) { + System.out.println("Usage: ./gradlew run --args=\" \""); + System.out.println("Example: ./gradlew run --args=\"avp-21638 s3ec-java-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\""); + System.exit(1); + } + + String bucketName = args[0]; + String objectKey = args[1]; + String kmsKeyId = args[2]; + String region = args[3]; + + System.out.println("=== S3 Encryption Client v3 Example (Java) ==="); + System.out.println("Bucket: " + bucketName); + System.out.println("Object Key: " + objectKey); + System.out.println("KMS Key ID: " + kmsKeyId); + System.out.println("Region: " + region); + System.out.println(); + + // Test data for encryption + String testData = "Hello, World! This is a test message for S3 encryption client v3 in Java."; + System.out.println("Original data: " + testData); + System.out.println("Data length: " + testData.length() + " bytes"); + System.out.println(); + + try { + System.out.println("--- Initialize S3 Encryption Client v3 ---"); + + // Create standard S3 client + S3Client s3Client = S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + + // Create KMS client + KmsClient kmsClient = KmsClient.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + + // Create KMS keyring + KmsKeyring keyring = KmsKeyring.builder() + .kmsClient(kmsClient) + .wrappingKeyId(kmsKeyId) + .build(); + + // Create Cryptographic Materials Manager + CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder() + .keyring(keyring) + .build(); + + // Create S3 Encryption Client v3 + S3EncryptionClient encryptionClient = S3EncryptionClient.builder() + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .wrappedClient(s3Client) + .cryptoMaterialsManager(cmm) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .build(); + + System.out.println("Successfully initialized S3 Encryption Client v3"); + System.out.println("--- Encrypt and Upload Object to S3 ---"); + + // Add encryption context + Map encryptionContext = new HashMap<>(); + encryptionContext.put("purpose", "example"); + encryptionContext.put("version", "v3"); + encryptionContext.put("language", "java"); + + // Upload encrypted object using S3 Encryption Client + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + + encryptionClient.putObject(putRequest, RequestBody.fromString(testData)); + + System.out.println("Successfully uploaded encrypted object to S3!"); + System.out.println(" Bucket: " + bucketName); + System.out.println(" Key: " + objectKey); + System.out.println(" Encryption Context: " + encryptionContext); + System.out.println(); + + System.out.println("--- Download and Decrypt Object from S3 ---"); + + // Download and decrypt object using S3 Encryption Client + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + + String decryptedData = encryptionClient.getObjectAsBytes(getRequest) + .asString(StandardCharsets.UTF_8); + + System.out.println("Successfully downloaded and decrypted object from S3!"); + System.out.println(" Object size: " + decryptedData.length() + " bytes"); + System.out.println(" Decrypted data: " + decryptedData); + System.out.println(); + + System.out.println("--- Verify Roundtrip Success ---"); + + // Verify the roundtrip was successful + if (decryptedData.equals(testData)) { + System.out.println("SUCCESS: Roundtrip encryption/decryption completed successfully!"); + System.out.println(" Original data matches decrypted data"); + System.out.println(" Data integrity verified"); + } else { + System.out.println("ERROR: Roundtrip failed - data mismatch"); + System.out.println(" Original: " + testData); + System.out.println(" Decrypted: " + decryptedData); + System.exit(1); + } + + System.out.println(); + System.out.println("=== Example completed successfully! ==="); + + // Clean up clients + encryptionClient.close(); + s3Client.close(); + kmsClient.close(); + + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/all-examples/java/v4/Makefile b/all-examples/java/v4/Makefile new file mode 100644 index 00000000..3390c5e4 --- /dev/null +++ b/all-examples/java/v4/Makefile @@ -0,0 +1,75 @@ +# Makefile for S3 Encryption Client Java v4 Example + +# Default target +.PHONY: all install clean run help s3ec-staging + +# Default arguments for running the example +# Override these when calling make run +BUCKET_NAME ?= avp-21638 +OBJECT_KEY ?= s3ec-java-v4 +KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 +AWS_REGION ?= us-east-2 + +all: install + +# Install S3 Encryption Client library from source +s3ec-staging: + @echo "[JAVA V4] Installing S3 Encryption Client library from source..." + @cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "[JAVA V4] S3 Encryption Client library installed successfully!" + +# Install dependencies using Gradle +install: s3ec-staging + @echo "[JAVA V4] Installing Java dependencies..." + @chmod +x ./gradlew + @./gradlew build + @echo "[JAVA V4] Dependencies installed successfully!" + +# Clean Gradle artifacts +clean: + @echo "[JAVA V4] Cleaning Gradle artifacts..." + @./gradlew clean + @echo "[JAVA V4] Clean completed!" + +# Run the example with default arguments +run: install + @echo "[JAVA V4] Running S3 Encryption Client v4 Java example..." + @echo "[JAVA V4] Bucket: $(BUCKET_NAME)" + @echo "[JAVA V4] Object Key: $(OBJECT_KEY)" + @echo "[JAVA V4] KMS Key ID: $(KMS_KEY_ID)" + @echo "[JAVA V4] Region: $(AWS_REGION)" + @echo "" + @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" + +# Run with custom arguments +# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +run-custom: install + @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" + +# Show help +help: + @echo "S3 Encryption Client Java v4 Example Makefile" + @echo "" + @echo "Available targets:" + @echo " s3ec-staging - Install S3 Encryption Client library from source" + @echo " install - Install Java dependencies using Gradle" + @echo " run - Install dependencies and run the example with default parameters" + @echo " run-custom - Install dependencies and run with custom parameters" + @echo " clean - Remove Gradle artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Default parameters:" + @echo " BUCKET_NAME = $(BUCKET_NAME)" + @echo " OBJECT_KEY = $(OBJECT_KEY)" + @echo " KMS_KEY_ID = $(KMS_KEY_ID)" + @echo " AWS_REGION = $(AWS_REGION)" + @echo "" + @echo "To run with custom parameters:" + @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" + @echo "" + @echo "Prerequisites:" + @echo " - Java 11+ installed on the system" + @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" + @echo " - Valid S3 bucket and KMS key with appropriate permissions" + @echo " - S3 Encryption Client v4 library installed." + @echo " (Install by running: cd s3ec-staging && mvn install)" diff --git a/all-examples/java/v4/README.md b/all-examples/java/v4/README.md new file mode 100644 index 00000000..deedec94 --- /dev/null +++ b/all-examples/java/v4/README.md @@ -0,0 +1,57 @@ +# S3 Encryption Client Java v4 Example + +This example demonstrates how to use the Amazon S3 Encryption Client v4 for Java to perform client-side encryption and decryption of objects with **key commitment** enabled for enhanced security. + +## Prerequisites + +1. **Java**: Requires Java 11 or later +2. **Gradle**: The example project uses Gradle wrapper (included - `./gradlew`) +3. **Maven**: Required to install the S3 Encryption Client library from source +4. **AWS Credentials**: Configure your AWS credentials using one of the following methods: + - AWS CLI: `aws configure` + - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + - IAM roles (for EC2 instances) +5. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` +6. **S3 Bucket**: An existing S3 bucket where you have read/write permissions + +## Setup + +Install dependencies and build (this automatically installs the S3 Encryption Client library from source): +```bash +make install +``` + +Or manually: +```bash +cd s3ec-staging && mvn clean install && cd .. +./gradlew build +``` + +**Note**: This example uses a local library installed in Maven local repository via the symbolic link `s3ec-staging`. + +## Usage + +### Using Make (Recommended) + +Run the example with default parameters: +```bash +make run +``` + +Run with custom parameters: +```bash +make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region +``` + +### Manual Usage + +Run the example with the following command: + +```bash +./gradlew run --args=" " +``` + +### Example: + +```bash +./gradlew run --args="my-test-bucket s3ec-java-v4-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" diff --git a/all-examples/java/v4/build.gradle.kts b/all-examples/java/v4/build.gradle.kts new file mode 100644 index 00000000..56b02b97 --- /dev/null +++ b/all-examples/java/v4/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + java + application +} + +group = "software.amazon.encryption.s3.example" +version = "1.0.0" + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + // AWS SDK v2 dependencies + implementation(platform("software.amazon.awssdk:bom:2.20.0")) + implementation("software.amazon.awssdk:s3") + implementation("software.amazon.awssdk:kms") + implementation("software.amazon.awssdk:auth") + + // S3 Encryption Client v4 from local Maven repository + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-add-kc") +} + +application { + mainClass.set("software.amazon.encryption.s3.example.Main") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "software.amazon.encryption.s3.example.Main" + } + + // Create a fat jar with all dependencies + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveBaseName.set("s3ec-java-v4-example") +} + +tasks.named("run") { + standardInput = System.`in` +} diff --git a/all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar b/all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/all-examples/java/v4/gradlew.bat b/all-examples/java/v4/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/all-examples/java/v4/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/all-examples/java/v4/s3ec-staging b/all-examples/java/v4/s3ec-staging new file mode 120000 index 00000000..970f5de7 --- /dev/null +++ b/all-examples/java/v4/s3ec-staging @@ -0,0 +1 @@ +../../../test-server/java-v4-server/s3ec-staging \ No newline at end of file diff --git a/all-examples/java/v4/settings.gradle.kts b/all-examples/java/v4/settings.gradle.kts new file mode 100644 index 00000000..e20b5a12 --- /dev/null +++ b/all-examples/java/v4/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "s3ec-java-v4-example" diff --git a/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java b/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java new file mode 100644 index 00000000..4ec8fd9b --- /dev/null +++ b/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java @@ -0,0 +1,160 @@ +package software.amazon.encryption.s3.example; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.materials.CryptographicMaterialsManager; +import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; +import software.amazon.encryption.s3.materials.KmsKeyring; + +/** + * Example demonstrating the use of Amazon S3 Encryption Client v4 for Java. + * + * This example shows how to: + * 1. Initialize the S3 Encryption Client with KMS keyring and key commitment + * 2. Encrypt and upload an object to S3 with key commitment + * 3. Download and decrypt the object + * 4. Verify the roundtrip encryption/decryption + */ +public class Main { + + public static void main(String[] args) { + // Check command line arguments + if (args.length != 4) { + System.out.println("Usage: ./gradlew run --args=\" \""); + System.out.println("Example: ./gradlew run --args=\"avp-21638 s3ec-java-v4 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\""); + System.exit(1); + } + + String bucketName = args[0]; + String objectKey = args[1]; + String kmsKeyId = args[2]; + String region = args[3]; + + System.out.println("=== S3 Encryption Client v4 Example (Java) ==="); + System.out.println("Bucket: " + bucketName); + System.out.println("Object Key: " + objectKey); + System.out.println("KMS Key ID: " + kmsKeyId); + System.out.println("Region: " + region); + System.out.println(); + + // Test data for encryption + String testData = "Hello, World! This is a test message for S3 encryption client v4 in Java."; + System.out.println("Original data: " + testData); + System.out.println("Data length: " + testData.length() + " bytes"); + System.out.println(); + + try { + System.out.println("--- Initialize S3 Encryption Client v4 ---"); + + // Create standard S3 client + S3Client s3Client = S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + + // Create KMS client + KmsClient kmsClient = KmsClient.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + + // Create KMS keyring + KmsKeyring keyring = KmsKeyring.builder() + .kmsClient(kmsClient) + .wrappingKeyId(kmsKeyId) + .build(); + + // Create Cryptographic Materials Manager + CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder() + .keyring(keyring) + .build(); + + // Create S3 Encryption Client v4 with key commitment enabled (Defaults to REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4() + .wrappedClient(s3Client) + .cryptoMaterialsManager(cmm) + .enableLegacyUnauthenticatedModes(false) + .enableLegacyWrappingAlgorithms(false) + .build(); + + System.out.println("Successfully initialized S3 Encryption Client v4"); + System.out.println("Key commitment: ENABLED"); + System.out.println("--- Encrypt and Upload Object to S3 ---"); + + // Add encryption context + Map encryptionContext = new HashMap<>(); + encryptionContext.put("purpose", "example"); + encryptionContext.put("version", "v4"); + encryptionContext.put("language", "java"); + + // Upload encrypted object using S3 Encryption Client + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + + encryptionClient.putObject(putRequest, RequestBody.fromString(testData)); + + System.out.println("Successfully uploaded encrypted object to S3!"); + System.out.println(" Bucket: " + bucketName); + System.out.println(" Key: " + objectKey); + System.out.println(" Encryption Context: " + encryptionContext); + System.out.println(" Key Commitment: ENABLED"); + System.out.println(); + + System.out.println("--- Download and Decrypt Object from S3 ---"); + + // Download and decrypt object using S3 Encryption Client + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + + String decryptedData = encryptionClient.getObjectAsBytes(getRequest) + .asString(StandardCharsets.UTF_8); + + System.out.println("Successfully downloaded and decrypted object from S3!"); + System.out.println(" Object size: " + decryptedData.length() + " bytes"); + System.out.println(" Decrypted data: " + decryptedData); + System.out.println(); + + System.out.println("--- Verify Roundtrip Success ---"); + + // Verify the roundtrip was successful + if (decryptedData.equals(testData)) { + System.out.println("SUCCESS: Roundtrip encryption/decryption completed successfully!"); + System.out.println(" Original data matches decrypted data"); + System.out.println(" Data integrity verified"); + System.out.println(" Key commitment verified"); + } else { + System.out.println("ERROR: Roundtrip failed - data mismatch"); + System.out.println(" Original: " + testData); + System.out.println(" Decrypted: " + decryptedData); + System.exit(1); + } + + System.out.println(); + System.out.println("=== Example completed successfully! ==="); + + // Clean up clients + encryptionClient.close(); + s3Client.close(); + kmsClient.close(); + + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} From e5facbd8d82c9d502ea77826480cc0fb1a452708 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 14 Nov 2025 10:09:24 -0800 Subject: [PATCH 155/201] chore: Bump Go to head of main for updated Duvet (#93) --- 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 85fd30c6..f51a4402 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 85fd30c6a7ebbef3d056991c6f3673e0e9002bcf +Subproject commit f51a4402c741cd989c7984336de560e9c54baf17 diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index 85fd30c6..f51a4402 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit 85fd30c6a7ebbef3d056991c6f3673e0e9002bcf +Subproject commit f51a4402c741cd989c7984336de560e9c54baf17 From 1d259466efcaf8eef109487926eaa7e6f7f846c8 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 14 Nov 2025 11:29:36 -0800 Subject: [PATCH 156/201] chore: Use symbolic link in examples (#91) --- .github/workflows/examples.yml | 16 ---------------- .gitmodules | 8 -------- all-examples/net/v3/s3ec-v3-local | 2 +- all-examples/net/v4/s3ec-v4-local | 2 +- 4 files changed, 2 insertions(+), 26 deletions(-) mode change 160000 => 120000 all-examples/net/v3/s3ec-v3-local mode change 160000 => 120000 all-examples/net/v4/s3ec-v4-local diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index c4ae10bb..4b1cde5a 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -26,22 +26,6 @@ jobs: ref: fire-egg-dev path: all-examples/cpp/aws-sdk-cpp/ - - name: Checkout .NET V3 code - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - ref: rishav/key-commitment - path: all-examples/net/v3/s3ec-v3-local - - - name: Checkout .NET V4 code - uses: actions/checkout@v5 - with: - token: ${{ secrets.PAT_FOR_DOTNET }} - repository: aws/private-amazon-s3-encryption-client-dotnet-staging - ref: s3ec-v4-WIP - path: all-examples/net/v4/s3ec-v4-local - - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.gitmodules b/.gitmodules index 3adca9cb..a88a83f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,11 +58,3 @@ [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 "all-examples/net/v4/s3ec-v4-local"] - path = all-examples/net/v4/s3ec-v4-local - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = s3ec-v4-WIP -[submodule "all-examples/net/v3/s3ec-v3-local"] - path = all-examples/net/v3/s3ec-v3-local - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = rishav/key-commitment diff --git a/all-examples/net/v3/s3ec-v3-local b/all-examples/net/v3/s3ec-v3-local deleted file mode 160000 index ca1149d9..00000000 --- a/all-examples/net/v3/s3ec-v3-local +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ca1149d9b423591c09d35caa649b3f6846e511a6 diff --git a/all-examples/net/v3/s3ec-v3-local b/all-examples/net/v3/s3ec-v3-local new file mode 120000 index 00000000..a2d8df44 --- /dev/null +++ b/all-examples/net/v3/s3ec-v3-local @@ -0,0 +1 @@ +../../../test-server/net-v3-transition-server/s3ec-v3-transition-branch \ No newline at end of file diff --git a/all-examples/net/v4/s3ec-v4-local b/all-examples/net/v4/s3ec-v4-local deleted file mode 160000 index 691d22a5..00000000 --- a/all-examples/net/v4/s3ec-v4-local +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 691d22a504184fd71f2dae7fd354bd669b58cc07 diff --git a/all-examples/net/v4/s3ec-v4-local b/all-examples/net/v4/s3ec-v4-local new file mode 120000 index 00000000..371b1a90 --- /dev/null +++ b/all-examples/net/v4/s3ec-v4-local @@ -0,0 +1 @@ +../../../test-server/net-v4-server/s3ec-net-v4-improved \ No newline at end of file From c9c47b8a6e794846a500aebc40a1e477e54e03b8 Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Fri, 14 Nov 2025 13:12:46 -0800 Subject: [PATCH 157/201] chore: add instruction file tests for raw RSA (#95) --- .../amazon/encryption/s3/KC_GCMTests.java | 113 ++++++++++++------ .../amazon/encryption/s3/RoundTripTests.java | 74 +++++++++++- .../amazon/encryption/s3/TestUtils.java | 1 - .../Controllers/ClientController.cs | 23 ++-- .../net-v2-v3-server/Models/ClientRequest.cs | 8 ++ .../Controllers/ClientController.cs | 5 + .../Models/ClientRequest.cs | 8 ++ .../Controllers/ClientController.cs | 6 + .../net-v4-server/Models/ClientRequest.cs | 8 ++ 9 files changed, 198 insertions(+), 48 deletions(-) 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 index 99e8d37d..ee4279d6 100644 --- 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 @@ -5,48 +5,29 @@ 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.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.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 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.GetObjectInput; -import software.amazon.encryption.s3.model.GetObjectOutput; +import software.amazon.encryption.s3.model.InstructionFileConfig; 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. @@ -59,11 +40,21 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class KC_GCMTests { - private static String sharedObjectKeyBase = "test-kc-gcm-kms"; + 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 List crossLanguageObjects = new ArrayList<>(); + 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") @@ -78,7 +69,34 @@ void improved_configured_with_require_encrypt_allow_decrypt_should_encrypt_kc_gc .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + 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) @@ -94,7 +112,7 @@ void improved_configured_with_require_encrypt_require_decrypt_should_encrypt_kc_ .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(2) @@ -110,7 +128,7 @@ void improved_configured_with_the_default_should_encrypt_kc_gcm(TestUtils.Langua .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBase + language.getLanguageName()), crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Encrypt(client, S3ECId, appendTestSuffix(sharedObjectKeyBaseMetaDataMode + language.getLanguageName()), crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(10) @@ -127,7 +145,7 @@ void transition_configured_with_the_default_should_decrypt_kc_gcm(TestUtils.Lang .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(11) @@ -144,7 +162,7 @@ void transition_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_g .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(12) @@ -162,7 +180,7 @@ void improved_configured_with_forbid_encrypt_allow_decrypt_should_decrypt_kc_gcm .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(13) @@ -179,7 +197,7 @@ void improved_configured_with_require_encrypt_allow_decrypt_should_decrypt_kc_gc .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(14) @@ -196,7 +214,7 @@ void improved_configured_with_require_encrypt_require_decrypt_should_decrypt_kc_ .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + TestUtils.Decrypt(client, S3ECId, crossLanguageObjectsMetaDataMode, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); } @Order(15) @@ -213,7 +231,34 @@ void improved_configured_with_the_default_should_decrypt_kc_gcm(TestUtils.Langua .build()); String S3ECId = clientOutput.getClientId(); - TestUtils.Decrypt(client, S3ECId, crossLanguageObjects, EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + 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 59382006..6c26368d 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 @@ -30,6 +30,7 @@ import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +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; @@ -598,12 +599,77 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe assertFalse(ptInstFile.asUtf8String().isEmpty()); // Read should be enabled by default GetObjectOutput output = decClient.getObject(GetObjectInput.builder() - .clientID(decS3ECId) - .bucket(BUCKET) - .key(objectKey) - .build()); + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); assertEquals(input, new String(output.getBody().array())); } } + + @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") + @MethodSource("software.amazon.encryption.s3.TestUtils#crossLanguageClients") + public void instructionFileWriteAndReadWithRSA(LanguageServerTarget encLang, LanguageServerTarget decLang) throws Exception { + // Early validation + if (!RAW_SUPPORTED.contains(encLang.getLanguageName())) { + throw new TestAbortedException("not encrypting raw keyring with: " + encLang.getLanguageName()); + } + if (!RAW_SUPPORTED.contains(decLang.getLanguageName())) { + throw new TestAbortedException("not decrypting raw keyring with: " + decLang.getLanguageName()); + } + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyMaterial rsaKeyMaterial = KeyMaterial.builder() + .rsaKey(ByteBuffer.wrap(keyPairGen.generateKeyPair().getPrivate().getEncoded())) + .build(); + + S3ECConfig config = S3ECConfig.builder() + .instructionFileConfig(InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build()) + .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + .keyMaterial(rsaKeyMaterial) + .build(); + + // Create clients + S3ECTestServerClient encClient = testServerClientFor(encLang); + S3ECTestServerClient decClient = testServerClientFor(decLang); + + String encS3ECId = encClient.createClient(CreateClientInput.builder().config(config).build()).getClientId(); + String decS3ECId = decClient.createClient(CreateClientInput.builder().config(config).build()).getClientId(); + + final String objectKey = appendTestSuffix(String.format("rsa-insfile-write-%s-read-%s", + encLang.getLanguageName(), decLang.getLanguageName())); + final String input = "simple-test-input-rsa"; + + // Encrypt + encClient.putObject(PutObjectInput.builder() + .clientID(encS3ECId) + .bucket(BUCKET) + .key(objectKey) + .body(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))) + .build()); + + // Assert using Java plaintext client that an instruction file exists + ResponseBytes ptInstFile; + try (S3Client ptS3Client = S3Client.create()) { + ptInstFile = ptS3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey + ".instruction") + .build()); + } + 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() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + + assertEquals(input, new String(output.getBody().array())); + } } \ No newline at end of file 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 7aa812bb..d3d25d58 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,7 +6,6 @@ package software.amazon.encryption.s3; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.Socket; import java.net.URI; diff --git a/test-server/net-v2-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs index 7e626e56..e33a58e6 100644 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ b/test-server/net-v2-v3-server/Controllers/ClientController.cs @@ -18,11 +18,11 @@ 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" }); + return StatusCode(501, new GenericServerError { Message = "[NET-current] EnableDelayedAuthenticationMode not supported" }); if (request.Config.SetBufferSize.HasValue) - return StatusCode(501, new GenericServerError { Message = "SetBufferSize not supported" }); + return StatusCode(501, new GenericServerError { Message = "[NET-current] SetBufferSize not supported" }); if (request.Config.KeyMaterial.AesKey != null) - return StatusCode(501, new GenericServerError { Message = "AesKey not supported" }); + return StatusCode(501, new GenericServerError { Message = "[NET-current] AesKey not supported" }); try { @@ -36,7 +36,7 @@ public IActionResult CreateClient([FromBody] ClientRequest request) var kmsKeyId = request.Config.KeyMaterial.KmsKeyId; encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContext); logger.LogInformation( - "Created EncryptionMaterialsV2: KMS={KmsKeyId}", + "[NET-current] Created EncryptionMaterialsV2: KMS={KmsKeyId}", kmsKeyId); } else if (request.Config.KeyMaterial.RsaKey != null) @@ -49,7 +49,7 @@ public IActionResult CreateClient([FromBody] ClientRequest request) "Created EncryptionMaterialsV2: RSA"); } else { - return StatusCode(501, new GenericServerError { Message = "Unknown or missing key material!" }); + return StatusCode(501, new GenericServerError { Message = "[NET-current] Unknown or missing key material!" }); } var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; @@ -59,16 +59,21 @@ public IActionResult CreateClient([FromBody] ClientRequest request) var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; - logger.LogInformation("Created securityProfile= {securityProfile}", securityProfile.ToString()); + logger.LogInformation("[NET-current] Created securityProfile= {securityProfile}", securityProfile.ToString()); var configuration = new AmazonS3CryptoConfigurationV2(securityProfile); + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) + { + configuration.StorageMode = CryptoStorageMode.InstructionFile; + logger.LogInformation("[NET-current] 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("Created S3EC client with ID: {clientId}", clientId); + logger.LogInformation("[NET-current] Created S3EC client with ID: {clientId}", clientId); return new ContentResult { @@ -79,10 +84,10 @@ public IActionResult CreateClient([FromBody] ClientRequest request) } catch (Exception ex) { - logger.LogError(ex, "Failed to create S3EC client"); + logger.LogError(ex, "[NET-current] Failed to create S3EC client"); return StatusCode(500, new S3EncryptionClientError { - Message = $"Failed to create client: {ex.Message}" + Message = $"[NET-current] Failed to create client: {ex.Message}" }); } } diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs index 95644524..e51a9c25 100644 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ b/test-server/net-v2-v3-server/Models/ClientRequest.cs @@ -16,6 +16,7 @@ public class ClientConfig public long? SetBufferSize { get; set; } [Required] public KeyMaterial KeyMaterial { get; set; } = new(); + public InstructionFileConfig? InstructionFileConfig { get; set; } } public class KeyMaterial @@ -23,4 +24,11 @@ public class KeyMaterial public byte[]? RsaKey { get; set; } public byte[]? AesKey { get; set; } public string? KmsKeyId { get; set; } +} + +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-v3-transition-server/Controllers/ClientController.cs b/test-server/net-v3-transition-server/Controllers/ClientController.cs index 0746618e..a66fb342 100644 --- a/test-server/net-v3-transition-server/Controllers/ClientController.cs +++ b/test-server/net-v3-transition-server/Controllers/ClientController.cs @@ -67,6 +67,11 @@ public IActionResult CreateClient([FromBody] ClientRequest request) logger.LogInformation("[NET-V3-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-V3-Transitional] Created StorageMode= InstructionFile"); + } // Create S3 encryption client var encryptionClient = new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); // Add to cache and return client ID diff --git a/test-server/net-v3-transition-server/Models/ClientRequest.cs b/test-server/net-v3-transition-server/Models/ClientRequest.cs index 3c80fc59..07fe8520 100644 --- a/test-server/net-v3-transition-server/Models/ClientRequest.cs +++ b/test-server/net-v3-transition-server/Models/ClientRequest.cs @@ -21,6 +21,7 @@ public class ClientConfig public CommitmentPolicy? CommitmentPolicy { get; set; } [JsonPropertyName("encryptionAlgorithm")] public EncryptionAlgorithm? EncryptionAlgorithm { get; set; } + public InstructionFileConfig? InstructionFileConfig { get; set; } } public class KeyMaterial @@ -44,4 +45,11 @@ 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-v4-server/Controllers/ClientController.cs b/test-server/net-v4-server/Controllers/ClientController.cs index 5298d758..b9fbe3f9 100644 --- a/test-server/net-v4-server/Controllers/ClientController.cs +++ b/test-server/net-v4-server/Controllers/ClientController.cs @@ -79,6 +79,12 @@ public IActionResult CreateClient([FromBody] ClientRequest request) ? new AmazonS3CryptoConfigurationV4() : new AmazonS3CryptoConfigurationV4(securityProfile, commitmentPolicy, encryptionAlgorithm); + if (request.Config.InstructionFileConfig?.EnableInstructionFilePutObject == true) + { + configuration.StorageMode = CryptoStorageMode.InstructionFile; + logger.LogInformation("[NET-V3-Transitional] Created StorageMode= InstructionFile"); + } + // Create S3 encryption client var encryptionClient = new AmazonS3EncryptionClientV4(configuration, encryptionMaterial); // Add to cache and return client ID diff --git a/test-server/net-v4-server/Models/ClientRequest.cs b/test-server/net-v4-server/Models/ClientRequest.cs index a5eff6f7..76623b9d 100644 --- a/test-server/net-v4-server/Models/ClientRequest.cs +++ b/test-server/net-v4-server/Models/ClientRequest.cs @@ -21,6 +21,7 @@ public class ClientConfig public CommitmentPolicy? CommitmentPolicy { get; set; } [JsonPropertyName("encryptionAlgorithm")] public EncryptionAlgorithm? EncryptionAlgorithm { get; set; } + public InstructionFileConfig? InstructionFileConfig { get; set; } } public class KeyMaterial @@ -44,4 +45,11 @@ 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 From acac67411d2db5219d4b705e3ff414ec699816c6 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:47:34 -0800 Subject: [PATCH 158/201] chore: java fix - duvet (#96) * chore: s3ec v3 transtion and v4 improved tests * comment cpp checkout * bump s3ec-java commits * chore: add duvet reports for s3ec-java (transition & improved) * format * git-ignore * update java submodule * fix configuration * Revert "chore: reenable c++ (#52)" This reverts commit 8c6db9eb44b082a28ac143ae9824fe272a3e2166. * remove java transiton for now * fix configuration * fix configuration * Update test configuration * Duvet * Rebase * nit - format * Change java-v4-port * duvet changes * Dotnet change * remove symlink * Fix Tests * chore: enable java-v3-transition test server * chore: enable java-v3-transition test server * update .gitmodule branch * Merge Conflicts * chore: java examples * chore: java examples * chore: update client configuration to allow for default. * point iv's changes commit * Apply suggestion from @rishav-karanjit Co-authored-by: Rishav karanjit * allow java * fix duvet * examples * examples - remove gitignore * fix - duvet --------- Co-authored-by: Rishav karanjit --- .../java-v3-transition-server/.duvet/config.toml | 14 +++++++------- .../java-v3-transition-server/specification | 1 + test-server/java-v4-server/.duvet/config.toml | 14 +++++++------- test-server/java-v4-server/specification | 1 + 4 files changed, 16 insertions(+), 14 deletions(-) create mode 120000 test-server/java-v3-transition-server/specification create mode 120000 test-server/java-v4-server/specification diff --git a/test-server/java-v3-transition-server/.duvet/config.toml b/test-server/java-v3-transition-server/.duvet/config.toml index 65605eaa..645410cf 100644 --- a/test-server/java-v3-transition-server/.duvet/config.toml +++ b/test-server/java-v3-transition-server/.duvet/config.toml @@ -5,19 +5,19 @@ pattern = "s3ec-staging/**/*.java" # Include required specifications here [[specification]] -source = "../specification/s3-encryption/client.md" +source = "specification/s3-encryption/client.md" [[specification]] -source = "../specification/s3-encryption/decryption.md" +source = "specification/s3-encryption/decryption.md" [[specification]] -source = "../specification/s3-encryption/encryption.md" +source = "specification/s3-encryption/encryption.md" [[specification]] -source = "../specification/s3-encryption/key-commitment.md" +source = "specification/s3-encryption/key-commitment.md" [[specification]] -source = "../specification/s3-encryption/key-derivation.md" +source = "specification/s3-encryption/key-derivation.md" [[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" +source = "specification/s3-encryption/data-format/content-metadata.md" [[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" +source = "specification/s3-encryption/data-format/metadata-strategy.md" [report.html] enabled = true diff --git a/test-server/java-v3-transition-server/specification b/test-server/java-v3-transition-server/specification new file mode 120000 index 00000000..b173f708 --- /dev/null +++ b/test-server/java-v3-transition-server/specification @@ -0,0 +1 @@ +../specification \ No newline at end of file diff --git a/test-server/java-v4-server/.duvet/config.toml b/test-server/java-v4-server/.duvet/config.toml index 65605eaa..645410cf 100644 --- a/test-server/java-v4-server/.duvet/config.toml +++ b/test-server/java-v4-server/.duvet/config.toml @@ -5,19 +5,19 @@ pattern = "s3ec-staging/**/*.java" # Include required specifications here [[specification]] -source = "../specification/s3-encryption/client.md" +source = "specification/s3-encryption/client.md" [[specification]] -source = "../specification/s3-encryption/decryption.md" +source = "specification/s3-encryption/decryption.md" [[specification]] -source = "../specification/s3-encryption/encryption.md" +source = "specification/s3-encryption/encryption.md" [[specification]] -source = "../specification/s3-encryption/key-commitment.md" +source = "specification/s3-encryption/key-commitment.md" [[specification]] -source = "../specification/s3-encryption/key-derivation.md" +source = "specification/s3-encryption/key-derivation.md" [[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" +source = "specification/s3-encryption/data-format/content-metadata.md" [[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" +source = "specification/s3-encryption/data-format/metadata-strategy.md" [report.html] enabled = true diff --git a/test-server/java-v4-server/specification b/test-server/java-v4-server/specification new file mode 120000 index 00000000..b173f708 --- /dev/null +++ b/test-server/java-v4-server/specification @@ -0,0 +1 @@ +../specification \ No newline at end of file From 8da443152103815423b50f3ea71fcd1013672783 Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 14 Nov 2025 15:26:42 -0800 Subject: [PATCH 159/201] ruby spec updates (#101) --- 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 0a71f3e19c7f1f0222230ebf14db510d337d999b Mon Sep 17 00:00:00 2001 From: Rishav karanjit Date: Sat, 15 Nov 2025 10:28:07 -0800 Subject: [PATCH 160/201] chore: run php ruby decrypt for ins file (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * auto commit * php sdk bump * update test * point to stable branch --------- Co-authored-by: Jose Corella Co-authored-by: José Corella <39066999+josecorella@users.noreply.github.com> --- .../amazon/encryption/s3/RoundTripTests.java | 22 +++++++++++-------- test-server/php-v3-server/local-php-sdk | 2 +- 2 files changed, 14 insertions(+), 10 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 6c26368d..468fc708 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 @@ -549,6 +549,10 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(decLang.getLanguageName())) { throw new TestAbortedException("not testing " + encLang.getLanguageName()); } + // We skip PHP-V2-Current because it writes an instruction file that other languages may not read. + if (encLang.getLanguageName().equals("PHP-V2-Current")) { + throw new TestAbortedException("not testing " + encLang.getLanguageName()); + } S3ECTestServerClient encClient = testServerClientFor(encLang); S3ECTestServerClient decClient = testServerClientFor(decLang); final String objectKey = appendTestSuffix(String.format("write-%s-read-%s-instruction-file", encLang.getLanguageName(), decLang.getLanguageName())); @@ -596,16 +600,16 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe if (!encLang.getLanguageName().startsWith("Ruby") && !encLang.getLanguageName().startsWith("PHP")) { // Ruby and PHP do not include it :( 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() - .clientID(decS3ECId) - .bucket(BUCKET) - .key(objectKey) - .build()); - - assertEquals(input, new String(output.getBody().array())); } + assertFalse(ptInstFile.asUtf8String().isEmpty()); + // Read should be enabled by default + GetObjectOutput output = decClient.getObject(GetObjectInput.builder() + .clientID(decS3ECId) + .bucket(BUCKET) + .key(objectKey) + .build()); + + assertEquals(input, new String(output.getBody().array())); } @ParameterizedTest(name = "{displayName} for Encrypt: {0}, Decrypt: {1}") diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index d75f911e..e32c9f2b 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit d75f911e41df81a1224f09bb4330c4a1c6c8ed59 +Subproject commit e32c9f2b009a43cf88f2ab35e1e532114c8390c9 From 73081bf14f50f7edec8a0f62706c442b7cdf334e Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:18:05 -0500 Subject: [PATCH 161/201] bump cpp sdk version (#100) Update to latest C++ with better instruction file failure support --- 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 52eeeddd..87402c99 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 52eeeddd8c40c1547832781f2e48478afff6a6ad +Subproject commit 87402c99fd3c9107c6ccc6edf545fd4b05b2b551 diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 52eeeddd..87402c99 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 52eeeddd8c40c1547832781f2e48478afff6a6ad +Subproject commit 87402c99fd3c9107c6ccc6edf545fd4b05b2b551 From bcc274de4ce6f95d7ec057cdcf713d6ff626bd5e Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:00:28 -0500 Subject: [PATCH 162/201] Add migration example (#111) --- all-examples/cpp/main.cpp | 80 +++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/all-examples/cpp/main.cpp b/all-examples/cpp/main.cpp index db8627d7..30cb2b8d 100644 --- a/all-examples/cpp/main.cpp +++ b/all-examples/cpp/main.cpp @@ -1,15 +1,7 @@ -#include -#include -#include #include #include #include #include -#include - -#include -#include -#include using namespace Aws::S3Encryption; using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; @@ -23,6 +15,78 @@ static Aws::Map get_encryption_context(const char * ve }; } +static int test_migration(const char *bucket, const char *object, const char *kms_key_id, const char *region) +{ + Aws::Client::ClientConfiguration s3ClientConfig; + s3ClientConfig.region = region; + + auto materials = std::make_shared(kms_key_id, s3ClientConfig); + CryptoConfigurationV3 config(materials); + + // STEP 1: Upgrade to V3 client to prepare to read messages with commitment. + // You want to update your readers before you update your writers + config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); + auto client = std::make_shared(config, s3ClientConfig); + + auto encryption_context = get_encryption_context("V3"); + + // Put Object - writes objects WITHOUT commitment + Aws::S3::Model::PutObjectRequest put_request; + put_request.SetBucket(bucket); + put_request.SetKey(object); + + auto data = std::string("This is the sample content."); + + auto stream = std::make_shared(data); + put_request.SetBody(stream); + + // Put Object - writes objects WITHOUT commitment + auto put_outcome = client->PutObject(put_request, encryption_context); + assert(put_outcome.IsSuccess()); + + Aws::S3::Model::GetObjectRequest get_request; + get_request.SetBucket(bucket); + get_request.SetKey(object); + + // Get Object - can read objects with or without commitment + auto get_outcome = client->GetObject(get_request, encryption_context); + assert(get_outcome.IsSuccess()); + + // STEP 2: If all of the readers can read with or without commitment + // you can upgrade the commitment policy to write objects with commitment + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); + client = std::make_shared(config, s3ClientConfig); + + stream = std::make_shared(data); + put_request.SetBody(stream); + + // Put Object - writes objects WITH commitment + put_outcome = client->PutObject(put_request, encryption_context); + assert(put_outcome.IsSuccess()); + + // Get Object - can read objects with or without commitment + get_outcome = client->GetObject(get_request, encryption_context); + assert(get_outcome.IsSuccess()); + + // STEP 3: Once your system no longer has to read messages without commitment, + // you may update your client to only read messages written with key commitment + config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); + client = std::make_shared(config, s3ClientConfig); + + stream = std::make_shared(data); + put_request.SetBody(stream); + + // Put Object - writes objects WITH commitment + put_outcome = client->PutObject(put_request, encryption_context); + assert(put_outcome.IsSuccess()); + + // Get Object - can only read objects with commitment + get_outcome = client->GetObject(get_request, encryption_context); + assert(get_outcome.IsSuccess()); + + return 0; +} + static int test_v3(const char *bucket, const char *object, const char *kms_key_id, const char *region) { Aws::Client::ClientConfiguration s3ClientConfig; From fe130522359e3b3760eff9e230e55461dbde9e34 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 8 Dec 2025 14:41:47 -0800 Subject: [PATCH 163/201] Adding tests (#103) Adding: Instruction file tests ReEncrypt tests Range gets test Also, update the test servers to support these features. Also, updates to the various languages to make these tests pass. --- .github/workflows/test.yml | 19 +- test-server/Makefile | 32 +- test-server/cpp-v2-server/Makefile | 6 +- test-server/cpp-v2-server/main.cpp | 485 ++++- test-server/cpp-v2-transition-server/Makefile | 6 +- .../cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v2-transition-server/main.cpp | 553 +++++- test-server/cpp-v3-server/CMakeLists.txt | 16 +- test-server/cpp-v3-server/Makefile | 6 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/main.cpp | 592 +++++- test-server/go-v3-server/main.go | 28 +- .../go-v3-transition-server/local-go-s3ec | 2 +- test-server/go-v3-transition-server/main.go | 28 +- test-server/go-v4-server/local-go-s3ec | 2 +- test-server/go-v4-server/main.go | 28 +- test-server/java-tests/build.gradle.kts | 14 + .../s3/ExhaustiveRoundTripTests1_25.java | 11 +- .../amazon/encryption/s3/GCMTestSuite.java | 258 +++ .../amazon/encryption/s3/GCMTests.java | 203 -- .../s3/InstructionFileFailures.java | 788 ++++++++ .../amazon/encryption/s3/KC_GCMTestSuite.java | 391 ++++ .../amazon/encryption/s3/KC_GCMTests.java | 264 --- .../amazon/encryption/s3/RangedGetTests.java | 1687 +++++++++++++++++ .../amazon/encryption/s3/ReEncryptTests.java | 648 +++++++ .../amazon/encryption/s3/RoundTripTests.java | 28 +- .../amazon/encryption/s3/TestUtils.java | 400 +++- test-server/java-v3-server/Makefile | 2 +- test-server/java-v3-server/gradle.properties | 19 +- .../s3/CreateClientOperationImpl.java | 21 +- .../encryption/s3/GetObjectOperationImpl.java | 14 +- .../encryption/s3/ReEncryptOperationImpl.java | 185 ++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- .../java-v3-transition-server/Makefile | 2 +- .../gradle.properties | 19 +- .../java-v3-transition-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 21 +- .../encryption/s3/GetObjectOperationImpl.java | 24 +- .../encryption/s3/ReEncryptOperationImpl.java | 183 ++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- test-server/java-v4-server/Makefile | 2 +- test-server/java-v4-server/gradle.properties | 19 +- test-server/java-v4-server/s3ec-staging | 2 +- .../s3/CreateClientOperationImpl.java | 55 +- .../encryption/s3/GetObjectOperationImpl.java | 22 +- .../encryption/s3/ReEncryptOperationImpl.java | 183 ++ .../encryption/s3/S3ECJavaTestServer.java | 4 +- test-server/model/client.smithy | 11 +- test-server/model/object.smithy | 65 +- .../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 +- test-server/php-v2-server/Makefile | 2 +- test-server/php-v2-transition-server/Makefile | 2 +- .../src/get_object.php | 18 +- test-server/php-v3-server/Makefile | 2 +- test-server/php-v3-server/local-php-sdk | 2 +- test-server/php-v3-server/src/get_object.php | 18 +- test-server/ruby-v2-server/app.rb | 15 +- .../ruby-v2-server/lib/client_manager.rb | 44 +- test-server/ruby-v3-server/app.rb | 11 +- .../ruby-v3-server/lib/client_manager.rb | 46 +- 65 files changed, 6726 insertions(+), 855 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/InstructionFileFailures.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 create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java create mode 100644 test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java create mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.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/.github/workflows/test.yml b/.github/workflows/test.yml index 880ca3f4..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 }} @@ -59,13 +49,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 \ @@ -171,9 +160,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 diff --git a/test-server/Makefile b/test-server/Makefile index 9b18b857..94a76b3f 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 $*..."; \ - $(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 $*"; \ exit 1; \ @@ -44,18 +47,17 @@ 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 \ - 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..." @@ -64,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 @@ -82,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 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-server/Makefile b/test-server/cpp-v2-server/Makefile index 77357c37..2d0a4b55 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..." @@ -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 a2b05810..e8ffe770 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; + +// 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; @@ -27,6 +91,55 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + // 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 { + // 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) +{ + // 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, const char *key) { const char *value = @@ -52,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"]; @@ -68,17 +184,32 @@ 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); - 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"); + + // 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(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); 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 (...) { @@ -89,13 +220,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 @@ -128,6 +264,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); @@ -138,14 +277,25 @@ 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) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + std::string bucket, + std::string key, + std::string client_id, + 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, 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) { + 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\"}"); } @@ -153,46 +303,107 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; 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); - // } + + // 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); - auto outcome = it->second->GetObject(request, kmsContextMap); + + // 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) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + 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); @@ -200,85 +411,249 @@ 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); - auto outcome = it->second->PutObject(request, kmsContextMap); + // 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"); + 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") { - 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, - &request_handler, NULL, MHD_OPTION_END); + 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, 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/Makefile b/test-server/cpp-v2-transition-server/Makefile index 16b70796..0383b4d8 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..." @@ -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/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index 87402c99..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 87402c99fd3c9107c6ccc6edf545fd4b05b2b551 +Subproject commit cec1f1933be672f65627c11ff2d853e07c5b3ff4 diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp index 1fcedc3c..9e9f942d 100644 --- a/test-server/cpp-v2-transition-server/main.cpp +++ b/test-server/cpp-v2-transition-server/main.cpp @@ -1,8 +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 @@ -10,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; + +// 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; @@ -27,6 +93,55 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + // 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 { + // 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) +{ + // 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, const char *key) { const char *value = @@ -69,6 +184,9 @@ std::string get_config(json & request, const char * x) 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 commitmentPolicy = get_config(request, "commitmentPolicy"); @@ -79,7 +197,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,22 +240,56 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; } - auto materials = - std::make_shared(kms_key_id); - CryptoConfigurationV2 config(materials); + // Create CryptoConfigurationV2 based on key type + std::shared_ptr config; + + if (!aes_key_blob.empty()) { + // Base64 decode the AES key + 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.GetUnderlyingData(), + decoded.GetLength() + ); + + auto materials = std::make_shared< + Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( + key_buffer + ); + config = std::make_shared(materials); + } else if (!kms_key_id.empty()) { + auto materials = std::make_shared(kms_key_id); + 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); + config->SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); + if (legacy2) + config->SetUnAuthenticatedRangeGet(RangeGetMode::ALL); if (inst_put) - config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - auto encryption_client = std::make_shared(config); + 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(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); 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 (...) { @@ -114,13 +300,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 @@ -153,6 +344,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); @@ -163,14 +357,25 @@ 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) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + std::string bucket, + std::string key, + std::string client_id, + 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, 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) { + 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\"}"); } @@ -178,46 +383,107 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; 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); - // } + + // 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); - auto outcome = it->second->GetObject(request, kmsContextMap); + + // 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) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + 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\"}"); } 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); @@ -225,86 +491,249 @@ 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); - auto outcome = it->second->PutObject(request, kmsContextMap); + // 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-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); } } +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; // 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-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; } - if (is_push && *upload_data_size) { + + // 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 > 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") { - std::unique_ptr body(static_cast(*con_cls)); - return handle_create_client(connection, *body); + fprintf(stderr, "[CPP-V2-TRANSITION] Handling /client endpoint\n"); + std::string *body = static_cast(*con_cls); + 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) { 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-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"); + 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") { - 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-TRANSITION] 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-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 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_SELECT_INTERNALLY, port, NULL, NULL, - &request_handler, NULL, MHD_OPTION_END); + 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, 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/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..e90c8d73 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..." @@ -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/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index 87402c99..cec1f193 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 cec1f1933be672f65627c11ff2d853e07c5b3ff4 diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 1f74974c..4e7227df 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -1,8 +1,56 @@ +/* + * S3 Encryption Test Server - C++ V3 + * + * 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 #include @@ -12,12 +60,31 @@ #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; + +// 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; @@ -27,6 +94,55 @@ std::string generate_uuid() { return std::string(uuid_str); } +std::shared_ptr get_client(const std::string &client_id) +{ + // 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 { + // 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) +{ + // 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-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, const char *key) { const char *value = @@ -67,9 +183,47 @@ std::string get_config(json & request, const char * x) 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"]; + + // 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,33 +232,81 @@ 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); - 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"); - + + // Create CryptoConfigurationV3 based on key type + std::optional config; + + if (!aes_key_blob.empty()) { + // Base64 decode the AES key + 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.GetUnderlyingData(), + decoded.GetLength() + ); + + auto materials = std::make_shared< + Aws::S3Encryption::Materials::SimpleEncryptionMaterialsWithGCMAAD>( + key_buffer + ); + config.emplace(materials); + } else if (!kms_key_id.empty()) { + auto materials = std::make_shared(kms_key_id); + config.emplace(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 (legacy2) + config->SetUnAuthenticatedRangeGet(RangeGetMode::ALL); + 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") 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); + 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); + 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); + 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); + + // Create S3EncryptionClientV3 with standard configuration + 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(); - client_cache[client_id] = encryption_client; + set_client(client_id, encryption_client); json response = {{"clientId", client_id}}; return send_response(connection, 200, response.dump()); @@ -120,13 +322,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 @@ -159,6 +366,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); @@ -169,14 +379,25 @@ 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, - 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()) { + std::string bucket, + std::string key, + std::string client_id, + 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, 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) { + 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\"}"); } @@ -184,48 +405,107 @@ MHD_Result handle_get_object(struct MHD_Connection *connection, Aws::S3::Model::GetObjectRequest request; 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); - // } + + // 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); - auto outcome = it->second->GetObject(request, kmsContextMap); + + // 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(); - 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-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()); + + // 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-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, "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] [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] [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) { - auto it = client_cache.find(client_id); - if (it == client_cache.end()) { + 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()); + + 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\"}"); } 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); @@ -233,85 +513,247 @@ 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); - auto outcome = it->second->PutObject(request, kmsContextMap); + // 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-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); } } +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 + *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-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; } - if (is_push && *upload_data_size) { + + // 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); 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 + + // LOG: About to process request + fprintf(stderr, "[CPP-V3] PROCESSING: %s %s\n", method, url); - std::string url_str(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-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) { 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-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") { - return handle_get_object(connection, bucket, key, client_id, metadata); + fprintf(stderr, "[CPP-V3] Dispatching to handle_get_object\n"); + 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") { - 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-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 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 + 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] Each S3 client will use 512 max connections\n"); + 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_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, 10, + MHD_OPTION_END); if (!daemon) { fprintf(stderr, "Failed to start server on port %d\n", port); diff --git a/test-server/go-v3-server/main.go b/test-server/go-v3-server/main.go index d201ffe2..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 @@ -66,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) } @@ -141,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 @@ -172,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") @@ -194,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) @@ -264,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/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec index f51a4402..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 f51a4402c741cd989c7984336de560e9c54baf17 +Subproject commit 912914ad1c14942c3ea8638fb37f9e2c46445a84 diff --git a/test-server/go-v3-transition-server/main.go b/test-server/go-v3-transition-server/main.go index 799a9668..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 @@ -68,7 +70,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 +150,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 @@ -189,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") @@ -211,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) @@ -281,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/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index f51a4402..912914ad 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 912914ad1c14942c3ea8638fb37f9e2c46445a84 diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go index 672236ac..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 @@ -68,7 +70,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 +150,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 @@ -189,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") @@ -211,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) @@ -281,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/java-tests/build.gradle.kts b/test-server/java-tests/build.gradle.kts index 106f82ef..2d1cbdeb 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() - 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")) // For debugging 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..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,10 +177,10 @@ 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; + 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) @@ -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/GCMTestSuite.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java new file mode 100644 index 00000000..ca495f56 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/GCMTestSuite.java @@ -0,0 +1,258 @@ +/* + * 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.DisplayName; +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 + @DisplayName("GCMTestSuite - Encrypt") + 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 + @DisplayName("GCMTestSuite - Decrypt") + 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/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java new file mode 100644 index 00000000..6460fbad --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java @@ -0,0 +1,788 @@ +/* +* 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.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.DisplayName; +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.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; + +/** +* 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. +* +*/ +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 + * + * 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 + @DisplayName("InstructionFileFailures - Encrypt") + 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 accessors for decrypt tests to retrieve encrypted object keys and key materials + */ + static List getCrossLanguageObjectsKms() { + return new ArrayList<>(crossLanguageObjectsKms); + } + + static List getCrossLanguageObjectsRsa() { + return new ArrayList<>(crossLanguageObjectsRsa); + } + + static List getCrossLanguageObjectsAes() { + return new ArrayList<>(crossLanguageObjectsAes); + } + + static KeyMaterial getRsaKey() { + return RSA_KEY; + } + + static KeyMaterial getAesKey() { + return AES_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 encryptWithInstructionFilesKmsKcGcm(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 + ); + } + + @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction files") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encryptWithInstructionFilesRsaKcGcm(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 encryptWithInstructionFilesAesKcGcm(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 makeCopiesToVerifyThings() 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 + SUFFIX_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 + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION, + encryptedObject.asByteArray(), + objectMetadata, + instructionFileWithCommitmentValues + ); + + putObjectWithInstructionFile( + ptS3Client, + objectKey + SUFFIX_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))); + } + + @AfterAll + static void signalEncryptionComplete() throws Exception { + makeCopiesToVerifyThings(); + + // Signal that all encryption tests have completed + encryptPhaseComplete.countDown(); + } + } + + /** + * 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 + @DisplayName("InstructionFileFailures - Decrypt") + 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 decryptKmsOriginalAndGoodCopyObjectsSucceeds(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 + SUFFIX_GOOD_COPY) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsKms + ); + } + + @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 decryptKmsWithDuplicateCommitmentInMetadataAndInstructionFails(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 + SUFFIX_BAD_BOTH_META_AND_INSTRUCTION) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt KMS when commitment is only in instruction file") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decryptKmsWithCommitmentOnlyInInstructionFileFails(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 + SUFFIX_BAD_ONLY_INSTRUCTION) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt KMS duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decryptKmsWithDuplicateCommitmentFailsWithForbidPolicy(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 + SUFFIX_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 decryptKmsWithInstructionFileCommitmentFailsWithForbidPolicy(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 + SUFFIX_BAD_ONLY_INSTRUCTION) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + // RSA instruction file decrypt + + @ParameterizedTest(name = "{0}: Successfully decrypt RSA encrypted original and good-copy objects") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decryptRsaOriginalAndGoodCopyObjectsSucceeds(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 + SUFFIX_GOOD_COPY) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + crossLanguageObjectsRsa + ); + } + + @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 decryptRsaWithDuplicateCommitmentInMetadataAndInstructionFails(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 + SUFFIX_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 decryptRsaWithCommitmentOnlyInInstructionFileFails(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 + SUFFIX_BAD_ONLY_INSTRUCTION) + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt RSA duplicate commitment with FORBID_ENCRYPT_ALLOW_DECRYPT policy") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decryptRsaWithDuplicateCommitmentFailsWithForbidPolicy(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 + SUFFIX_BAD_BOTH_META_AND_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 decryptRsaWithInstructionFileCommitmentFailsWithForbidPolicy(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 + SUFFIX_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 decryptAesOriginalAndGoodCopyObjectsSucceeds(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 + SUFFIX_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 decryptAesWithDuplicateCommitmentInMetadataAndInstructionFails(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 + SUFFIX_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 decryptAesWithCommitmentOnlyInInstructionFileFails(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 + SUFFIX_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 decryptAesWithDuplicateCommitmentFailsWithForbidPolicy(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 + SUFFIX_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 decryptAesWithInstructionFileCommitmentFailsWithForbidPolicy(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 + 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/KC_GCMTestSuite.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java new file mode 100644 index 00000000..d256f909 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/KC_GCMTestSuite.java @@ -0,0 +1,391 @@ +/* + * 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.DisplayName; +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 + @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"; + 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_kms( + 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_rsa( + 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_kms( + 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 + @DisplayName("KC_GCMTestSuite - Decrypt") + 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_kms( + 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, 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_rsa( + 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/RangedGetTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java new file mode 100644 index 00000000..5a954e1f --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RangedGetTests.java @@ -0,0 +1,1687 @@ +/* + * 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.DisplayName; +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.InstructionFileConfig; +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) + * - 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 - 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 + * - 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 + * + * 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 + */ +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()); + + // 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 + * + * 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 + @DisplayName("RangedGetTests - Encrypt") + 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<>()); + // KC-GCM with Object Metadata Storage (all metadata in object) + private static final List kcGcmObjectsMetadata = + Collections.synchronizedList(new ArrayList<>()); + // KC-GCM with Instruction File Storage (c/d/i in metadata, rest in instruction file) + private static final List kcGcmObjectsInstruction = + Collections.synchronizedList(new ArrayList<>()); + // Corruption test lists for metadata storage mode + private static final List mutatedCObjectsMetadata = + Collections.synchronizedList(new ArrayList<>()); + 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; + 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 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 getMutatedCObjectsInstruction() { + return new ArrayList<>(mutatedCObjectsInstruction); + } + + static List getMutatedDObjectsInstruction() { + return new ArrayList<>(mutatedDObjectsInstruction); + } + + static List getMutatedIObjectsInstruction() { + return new ArrayList<>(mutatedIObjectsInstruction); + } + + static List getInvalidDLengthShortInstruction() { + return new ArrayList<>(invalidDLengthShortInstruction); + } + + static List getInvalidDLengthLongInstruction() { + return new ArrayList<>(invalidDLengthLongInstruction); + } + + 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(); + } + + 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 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); + + 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 encryptGcmForRangedGets(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 with Object Metadata Storage for ranged get testing") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$EncryptTests#improvedClientsForKCGCM") + void encryptKcGcmMetadataForRangedGets(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-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 encryptKcGcmInstructionFileForRangedGets(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 + ); + } + + /** + * 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 + * Handles both object metadata storage and instruction file storage modes + */ + static void createCorruptedCopies() throws Exception { + try (S3Client ptS3Client = S3Client.create()) { + 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) + .build()); + + byte[] objectData = encryptedObject.asByteArray(); + Map objectMetadata = encryptedObject.response().metadata(); + + // Create good copy + putObjectWithMetadata(ptS3Client, objectKey + SUFFIX_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 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 + SUFFIX_BAD_MUTATED_C + 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 + SUFFIX_BAD_MUTATED_D + bitPos; + putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); + mutatedDObjectsMetadata.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 + SUFFIX_BAD_MUTATED_I + bitPos; + putObjectWithMetadata(ptS3Client, mutatedKey, objectData, mutatedMetadata); + mutatedIObjectsMetadata.add(mutatedKey); + } + + // 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 + SUFFIX_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 + SUFFIX_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 + SUFFIX_GOOD_COPY, + objectData, + objectMetadata, + originalInstructionFileJson + ); + + // 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 + SUFFIX_BAD_COMMITMENT_IN_INSTRUCTION, + objectData, + objectMetadata, + corruptedInstructionJson + ); + + // 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 + SUFFIX_BAD_MUTATED_C + bitPos; + putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); + mutatedCObjectsInstruction.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 + SUFFIX_BAD_MUTATED_D + bitPos; + putObjectWithInstructionFile(ptS3Client, mutatedKey, objectData, mutatedMetadata, originalInstructionFileJson); + mutatedDObjectsInstruction.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 + SUFFIX_BAD_MUTATED_I + bitPos; + 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 + SUFFIX_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 + SUFFIX_BAD_INVALID_D_LENGTH_LONG; + putObjectWithInstructionFile(ptS3Client, longDKey, objectData, longDMetadata, originalInstructionFileJson); + invalidDLengthLongInstruction.add(longDKey); + } + } + } + } + + 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 + @DisplayName("RangedGetTests - RangedGet") + 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; + + @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(); + // 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(); + + // 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 rangedGetCbcStartSucceeds(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 rangedGetCbcEndSucceeds(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 rangedGetCbcMiddleSucceeds(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 rangedGetCbcWholeFileSucceeds(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 rangedGetGcmStartSucceeds(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 rangedGetGcmEndSucceeds(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 rangedGetGcmMiddleSucceeds(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 rangedGetGcmWholeFileSucceeds(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 rangedGetGcmTagOnlySucceeds(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 rangedGetKcGcmStartSucceeds(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 rangedGetKcGcmMiddleSucceeds(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 rangedGetKcGcmTagOnlySucceeds(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 rangedGetKcGcmEndSucceeds(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 rangedGetKcGcmWholeFileSucceeds(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 Instruction File Storage - Valid Object Tests + + @ParameterizedTest(name = "{0}: Successfully ranged get KC-GCM Instruction File objects - start range") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void rangedGetKcGcmInstructionStartSucceeds(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, + 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 rangedGetKcGcmInstructionMiddleSucceeds(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 rangedGetKcGcmInstructionTagOnlySucceeds(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 rangedGetKcGcmInstructionEndSucceeds(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 rangedGetKcGcmInstructionWholeFileSucceeds(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 + ); + } + } + + // 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 rangedGetKcGcmInstructionCommitmentInInstructionFails(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 instruction file storage mode objects with c/d/i duplicated into instruction file + TestUtils.RangedGet_fails( + client, + S3ECId, + kcGcmObjectsInstruction.stream() + .map(key -> key + "-bad-commitment-in-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 rangedGetKcGcmMutatedCFails(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, + 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 rangedGetKcGcmMutatedDFails(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, + 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 rangedGetKcGcmMutatedIFails(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, + 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 rangedGetKcGcmInstructionMutatedCFails(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 rangedGetKcGcmInstructionMutatedDFails(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 rangedGetKcGcmInstructionMutatedIFails(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 rangedGetKcGcmMetadataInvalidCLengthShortFails(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.getInvalidDLengthShortMetadata(); + + 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 with invalid D length (too long) in metadata") + @MethodSource("software.amazon.encryption.s3.RangedGetTests$RangedGetTestsNested#rangedGetSupportedClients") + void rangedGetKcGcmMetadataInvalidDLengthLongFails(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 rangedGetKcGcmInstructionInvalidDLengthShortFails(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 rangedGetKcGcmInstructionInvalidDLengthLongFails(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/ReEncryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java new file mode 100644 index 00000000..3053afb6 --- /dev/null +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ReEncryptTests.java @@ -0,0 +1,648 @@ +/* + * 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.DisplayName; +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 + @DisplayName("ReEncryptTests - Encrypt") + 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 encryptAesForAesToAesReencrypt(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 encryptAesForAesToRsaCustomReencrypt(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 encryptAesForAesToRsaDefaultReencrypt(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 encryptRsaForRsaToRsaReencrypt(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 encryptRsaForRsaToAesDefaultReencrypt(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 + @DisplayName("ReEncryptTests - ReEncrypt") + 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 reencryptAesToAesInstructionFile(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 reencryptRsaToRsaInstructionFile(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 reencryptAesToRsaInstructionFile(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 reencryptRsaToAesDefaultInstructionFile(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 reencryptAesToRsaDefaultInstructionFile(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 + @DisplayName("ReEncryptTests - DecryptReEncrypted") + 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 decryptReencryptedAesToAesObjects(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 decryptReencryptedRsaToRsaObjects(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 decryptReencryptedAesToRsaObjects(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 decryptReencryptedRsaToAesDefaultObjects(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 decryptReencryptedAesToRsaDefaultObjects(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/RoundTripTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java index 468fc708..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 @@ -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()); } } } @@ -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()); } @@ -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() @@ -665,7 +673,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() @@ -676,4 +684,4 @@ public void instructionFileWriteAndReadWithRSA(LanguageServerTarget encLang, Lan assertEquals(input, new String(output.getBody().array())); } -} \ No newline at end of file +} 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..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,8 @@ 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; import java.net.URI; @@ -17,10 +19,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; @@ -32,6 +38,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; @@ -95,12 +102,35 @@ 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 = + 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 = 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 = @@ -116,6 +146,19 @@ 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 + // This is a current gap. + 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, @@ -361,6 +404,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 @@ -449,22 +514,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( @@ -492,23 +593,154 @@ public static void Encrypt( public static void Decrypt( S3ECTestServerClient client, - String S3ECId, List crossLanguageObjects, + String S3ECId, + List crossLanguageObjects, EncryptionAlgorithm expectedEncryptionAlgorithm ) { - for (String objectKey : crossLanguageObjects) { - GetObjectOutput output = client.getObject(GetObjectInput.builder() - .clientID(S3ECId) - .bucket(TestUtils.BUCKET) - .key(objectKey) - .build()); - - // Then: Pass - assertEquals(objectKey, new String(output.getBody().array())); - assertEquals( - expectedEncryptionAlgorithm, - GetEncryptionAlgorithm(objectKey), - "When decrypting the EncryptionAlgorithm does not match the expected value: " + expectedEncryptionAlgorithm - ); + // 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 + ) { + 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()) { + fail("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 { + String objectKey = crossLanguageObjects.get(i); + String expectedPlaintext = expectedPlaintexts.get(i); + + GetObjectOutput output = client.getObject(GetObjectInput.builder() + .clientID(S3ECId) + .bucket(TestUtils.BUCKET) + .key(objectKey) + .metadata(metadata) // Pass materials description for C++ + .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) + )); } } @@ -517,6 +749,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 { @@ -541,4 +778,113 @@ 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 + ) { + + if (objectKeys.isEmpty()) { + throw new AssertionError("There is nothing to get"); + } + + 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 + ) { + + if (objectKeys.isEmpty()) { + throw new AssertionError("There is nothing to get"); + } + + 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/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-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..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 @@ -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; @@ -34,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. @@ -106,12 +111,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) @@ -121,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/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-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..."); 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-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..183f1984 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 183f1984ed1679e8aa4cb368aeda66f2131a2061 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..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 @@ -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; @@ -35,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. @@ -106,9 +111,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()); @@ -140,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 86749489..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 @@ -35,11 +35,25 @@ 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()); + + // 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()) { + 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/ReEncryptOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java new file mode 100644 index 00000000..7a809761 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -0,0 +1,183 @@ +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 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_; + + public ReEncryptOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @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-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..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 @@ -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)) .build()) .build(); System.out.println("Starting 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..." 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..7a1899bb 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit db0c743eec335d16e6dceaf2b09d84becb0f74f8 +Subproject commit 7a1899bb8be6f137a3031ff76f2a1bf3f278e98d 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..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 @@ -1,11 +1,15 @@ 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; 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; @@ -35,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. @@ -67,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()]; @@ -86,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()) @@ -106,8 +133,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()); @@ -139,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 17e9a8ee..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 @@ -34,10 +34,24 @@ 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()); + + // 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()) { + 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/ReEncryptOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java new file mode 100644 index 00000000..6a7cd5b6 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java @@ -0,0 +1,183 @@ +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_; + + public ReEncryptOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @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..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 @@ -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)) .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 6d793353..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 @@ -80,7 +96,16 @@ operation GetObject { @required @notProperty clientID: String - } + + @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") @@ -93,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 @@ -104,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/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..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 ad8259173de365a13e8b3932ee02493f599f597f +Subproject commit c3bf38b93c25f7169982073b1ffd1f3d00f59073 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..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 1c0a458c19b351c266199c72072de746362c5326 +Subproject commit 04f70c8b70e25c7a1a36fcd5a420c40806157c66 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-v2-transition-server/src/get_object.php b/test-server/php-v2-transition-server/src/get_object.php index 5800e850..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(); @@ -80,6 +90,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/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: diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index e32c9f2b..3acb3ad4 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 3acb3ad4d98debcfc2148290cd6fcea83962fe08 diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php index 3de7f779..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(); @@ -84,6 +94,10 @@ function handleGetObject($params) return S3EncryptionClientError($e->getMessage()); } elseif (strpos($e->getMessage(), "Message is encrypted with a non commiting algorithm but commitment policy is set to REQUIRE_ENCRYPT_REQUIRE_DECRYPT. Select a valid commitment policy to decrypt this object.") !== false) { return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "One or more reserved keys found in Instruction file when they should not be present.") !== false) { + return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Expected a V3 envelope but was unable to constuct one.") !== false) { + return S3EncryptionClientError($e->getMessage()); } else { error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server argument: " . $e->getMessage(), 500); diff --git a/test-server/ruby-v2-server/app.rb b/test-server/ruby-v2-server/app.rb index cde757a3..96e55c8a 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 @@ -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}") @@ -201,7 +208,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 diff --git a/test-server/ruby-v2-server/lib/client_manager.rb b/test-server/ruby-v2-server/lib/client_manager.rb index 717003bf..3da62b45 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 @@ -36,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/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}") diff --git a/test-server/ruby-v3-server/lib/client_manager.rb b/test-server/ruby-v3-server/lib/client_manager.rb index a6fb551f..5ee3f1ec 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' @@ -61,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 41f0a004c3bfe1adef601b7f0d9c37de30dc9c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:42:27 -0800 Subject: [PATCH 164/201] chore(PHP): run migration examples in ci (#114) --- all-examples/php/v3/main.php | 347 ++++++++++++++++++++++++++++++++++- 1 file changed, 346 insertions(+), 1 deletion(-) diff --git a/all-examples/php/v3/main.php b/all-examples/php/v3/main.php index e22519c3..949a0ce1 100755 --- a/all-examples/php/v3/main.php +++ b/all-examples/php/v3/main.php @@ -7,9 +7,11 @@ use Aws\Kms\KmsClient; use Aws\S3\Crypto\S3EncryptionClientV3; use Aws\Crypto\KmsMaterialsProviderV3; +use Aws\S3\Crypto\S3EncryptionClientV2; +use Aws\Crypto\KmsMaterialsProviderV2; use Aws\Exception\AwsException; -function main() { +function main(): void { // Check command line arguments if (count($GLOBALS['argv']) !== 5) { echo "Usage: {$GLOBALS['argv'][0]} \n"; @@ -157,7 +159,350 @@ function main() { } } +function testMigration(): void { + if (count($GLOBALS['argv']) !== 5) { + echo "Usage: {$GLOBALS['argv'][0]} \n"; + echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; + exit(1); + } + + $bucketName = $GLOBALS['argv'][1]; + $objectKey = $GLOBALS['argv'][2]; + $kmsKeyId = $GLOBALS['argv'][3]; + $region = $GLOBALS['argv'][4]; + + echo "=== S3 Encryption Client Pre-migration (V2) Example ===\n"; + echo "Bucket: {$bucketName}\n"; + echo "Object Key: {$objectKey}\n"; + echo "KMS Key ID: {$kmsKeyId}\n"; + echo "Region: {$region}\n"; + echo "\n"; + + try { + $testData = "Hello, World! This is a test message for S3 encryption client Pre-migration (V2) in PHP."; + echo "Original data: {$testData}\n"; + echo "Data length: " . strlen($testData) . " bytes\n"; + echo "\n"; + + $v2EncryptionClient = new S3EncryptionClientV2( + new S3Client([ + 'region' => $region, + 'version' => 'latest', + ]) + ); + + $materialsProviderV2 = new KmsMaterialsProviderV2( + new KmsClient([ + 'region' => $region, + 'version' => 'latest', + ]), + $kmsKeyId + ); + + $cipherOptions = [ + 'Cipher' => 'gcm', + 'KeySize' => 256, + ]; + + $v2EncryptionClient->putObject([ + '@MaterialsProvider' => $materialsProviderV2, + '@CipherOptions' => $cipherOptions, + '@KmsEncryptionContext' => ['context-key' => 'context-value'], + 'Bucket' => $bucketName, + 'Key' => $objectKey, + 'Body' => $testData, + ]); + + $getResponse = $v2EncryptionClient->getObject([ + '@KmsAllowDecryptWithAnyCmk' => true, + '@SecurityProfile' => 'V2_AND_LEGACY', + '@CommitmentPolicy' => 'FORBID_ENCRYPT_ALLOW_DECRYPT', + '@MaterialsProvider' => $materialsProviderV2, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucketName, + 'Key' => $objectKey, + ]); + + // Read the decrypted data + $decryptedData = (string) $getResponse['Body']; + + echo "Successfully downloaded and decrypted object from S3!\n"; + echo " Object size: " . strlen($decryptedData) . " bytes\n"; + echo " Decrypted data: {$decryptedData}\n"; + echo "\n"; + + echo "--- Verify Roundtrip Success ---\n"; + + // Verify the roundtrip was successful + if ($decryptedData === $testData) { + echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; + echo " Original data matches decrypted data\n"; + echo " Data integrity verified\n"; + } else { + echo "ERROR: Roundtrip failed - data mismatch\n"; + echo " Original: {$testData}\n"; + echo " Decrypted: {$decryptedData}\n"; + exit(1); + } + + // Optionally Delete the Object + // echo "--- Cleanup ---\n"; + // Clean up the test object using regular S3 client + // $s3Client->deleteObject([ + // 'Bucket' => $bucketName, + // 'Key' => $objectKey + // ]); + // echo "Test object deleted from S3\n"; + + echo "\n"; + echo "=== Example completed successfully! ===\n"; + + } catch (AwsException $e) { + $errorCode = $e->getAwsErrorCode(); + $errorMessage = $e->getMessage(); + + if (strpos($errorCode, 'NoSuchBucket') !== false) { + echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorCode, 'NotFoundException') !== false) { + echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorMessage, 'encryption') !== false) { + echo "S3 Encryption Error: {$errorMessage}\n"; + } else { + echo "AWS Service Error: {$errorMessage}\n"; + echo " Error Code: {$errorCode}\n"; + } + exit(1); + } catch (Exception $e) { + echo "Unexpected error: {$e->getMessage()}\n"; + echo " File: {$e->getFile()}:{$e->getLine()}\n"; + exit(1); + } + + echo "=== S3 Encryption Client during migration (V3 with backward compatibility) Example ===\n"; + echo "Bucket: {$bucketName}\n"; + echo "Object Key: {$objectKey}\n"; + echo "KMS Key ID: {$kmsKeyId}\n"; + echo "Region: {$region}\n"; + echo "\n"; + + try { + $testData = "Hello, World! This is a test message for S3 encryption client during migration (V3 with backward compatibility) in PHP."; + echo "Original data: {$testData}\n"; + echo "Data length: " . strlen($testData) . " bytes\n"; + echo "\n"; + + $v3EncryptionClient = new S3EncryptionClientV3( + new S3Client([ + 'region' => $region, + 'version' => 'latest', + ]) + ); + + $materialsProviderV3 = new KmsMaterialsProviderV3( + new KmsClient([ + 'region' => $region, + 'version' => 'latest', + ]), + $kmsKeyId + ); + + $cipherOptions = [ + 'Cipher' => 'gcm', + 'KeySize' => 256, + ]; + + $v3EncryptionClient->putObject([ + '@MaterialsProvider' => $materialsProviderV3, + '@CipherOptions' => $cipherOptions, + '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_ALLOW_DECRYPT', + '@KmsEncryptionContext' => ['context-key' => 'context-value'], + 'Bucket' => $bucketName, + 'Key' => $objectKey, + 'Body' => $testData, + ]); + + $getResponse = $v3EncryptionClient->getObject([ + '@KmsAllowDecryptWithAnyCmk' => true, + '@SecurityProfile' => 'V3_AND_LEGACY', + '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_ALLOW_DECRYPT', + '@MaterialsProvider' => $materialsProviderV3, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucketName, + 'Key' => $objectKey, + ]); + + // Read the decrypted data + $decryptedData = (string) $getResponse['Body']; + + echo "Successfully downloaded and decrypted object from S3!\n"; + echo " Object size: " . strlen($decryptedData) . " bytes\n"; + echo " Decrypted data: {$decryptedData}\n"; + echo "\n"; + + echo "--- Verify Roundtrip Success ---\n"; + + // Verify the roundtrip was successful + if ($decryptedData === $testData) { + echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; + echo " Original data matches decrypted data\n"; + echo " Data integrity verified\n"; + } else { + echo "ERROR: Roundtrip failed - data mismatch\n"; + echo " Original: {$testData}\n"; + echo " Decrypted: {$decryptedData}\n"; + exit(1); + } + + // Optionally Delete the Object + // echo "--- Cleanup ---\n"; + // Clean up the test object using regular S3 client + // $s3Client->deleteObject([ + // 'Bucket' => $bucketName, + // 'Key' => $objectKey + // ]); + // echo "Test object deleted from S3\n"; + + echo "\n"; + echo "=== Example completed successfully! ===\n"; + + } catch (AwsException $e) { + $errorCode = $e->getAwsErrorCode(); + $errorMessage = $e->getMessage(); + + if (strpos($errorCode, 'NoSuchBucket') !== false) { + echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorCode, 'NotFoundException') !== false) { + echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorMessage, 'encryption') !== false) { + echo "S3 Encryption Error: {$errorMessage}\n"; + } else { + echo "AWS Service Error: {$errorMessage}\n"; + echo " Error Code: {$errorCode}\n"; + } + exit(1); + } catch (Exception $e) { + echo "Unexpected error: {$e->getMessage()}\n"; + echo " File: {$e->getFile()}:{$e->getLine()}\n"; + exit(1); + } + + echo "=== S3 Encryption Client post-migration (V3 with key commitment) Example ===\n"; + echo "Bucket: {$bucketName}\n"; + echo "Object Key: {$objectKey}\n"; + echo "KMS Key ID: {$kmsKeyId}\n"; + echo "Region: {$region}\n"; + echo "\n"; + + try { + $testData = "Hello, World! This is a test message for S3 encryption client post-migration (V3 with key commitment) in PHP."; + echo "Original data: {$testData}\n"; + echo "Data length: " . strlen($testData) . " bytes\n"; + echo "\n"; + + $v3EncryptionClient = new S3EncryptionClientV3( + new S3Client([ + 'region' => $region, + 'version' => 'latest', + ]) + ); + + $materialsProviderV3 = new KmsMaterialsProviderV3( + new KmsClient([ + 'region' => $region, + 'version' => 'latest', + ]), + $kmsKeyId + ); + + $cipherOptions = [ + 'Cipher' => 'gcm', + 'KeySize' => 256, + ]; + + $v3EncryptionClient->putObject([ + '@MaterialsProvider' => $materialsProviderV3, + '@CipherOptions' => $cipherOptions, + '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_REQUIRE_DECRYPT', + '@KmsEncryptionContext' => ['context-key' => 'context-value'], + 'Bucket' => $bucketName, + 'Key' => $objectKey, + 'Body' => $testData, + ]); + + $getResponse = $v3EncryptionClient->getObject([ + '@KmsAllowDecryptWithAnyCmk' => true, + '@SecurityProfile' => 'V3', + '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_REQUIRE_DECRYPT', + '@MaterialsProvider' => $materialsProviderV3, + '@CipherOptions' => $cipherOptions, + 'Bucket' => $bucketName, + 'Key' => $objectKey, + ]); + + // Read the decrypted data + $decryptedData = (string) $getResponse['Body']; + + echo "Successfully downloaded and decrypted object from S3!\n"; + echo " Object size: " . strlen($decryptedData) . " bytes\n"; + echo " Decrypted data: {$decryptedData}\n"; + echo "\n"; + + echo "--- Verify Roundtrip Success ---\n"; + + // Verify the roundtrip was successful + if ($decryptedData === $testData) { + echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; + echo " Original data matches decrypted data\n"; + echo " Data integrity verified\n"; + } else { + echo "ERROR: Roundtrip failed - data mismatch\n"; + echo " Original: {$testData}\n"; + echo " Decrypted: {$decryptedData}\n"; + exit(1); + } + + // Optionally Delete the Object + // echo "--- Cleanup ---\n"; + // Clean up the test object using regular S3 client + // $s3Client->deleteObject([ + // 'Bucket' => $bucketName, + // 'Key' => $objectKey + // ]); + // echo "Test object deleted from S3\n"; + + echo "\n"; + echo "=== Example completed successfully! ===\n"; + + } catch (AwsException $e) { + $errorCode = $e->getAwsErrorCode(); + $errorMessage = $e->getMessage(); + + if (strpos($errorCode, 'NoSuchBucket') !== false) { + echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorCode, 'NotFoundException') !== false) { + echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; + echo " {$errorMessage}\n"; + } elseif (strpos($errorMessage, 'encryption') !== false) { + echo "S3 Encryption Error: {$errorMessage}\n"; + } else { + echo "AWS Service Error: {$errorMessage}\n"; + echo " Error Code: {$errorCode}\n"; + } + exit(1); + } catch (Exception $e) { + echo "Unexpected error: {$e->getMessage()}\n"; + echo " File: {$e->getFile()}:{$e->getLine()}\n"; + exit(1); + } +} + // Run the main function if this script is executed directly if (php_sapi_name() === 'cli' && isset($GLOBALS['argv']) && basename($GLOBALS['argv'][0]) === basename(__FILE__)) { main(); + testMigration(); } From c67949866e74eab3f4ff145860998b17a689032a Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 12 Dec 2025 13:41:04 -0800 Subject: [PATCH 165/201] Tests and fix for Ruby pen-test finding (#116) --- .../s3/InstructionFileFailures.java | 161 ++++++++++++++++++ test-server/ruby-v2-server/Gemfile.lock | 10 +- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/Gemfile.lock | 10 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 5 files changed, 173 insertions(+), 12 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 6460fbad..0096a8bf 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 @@ -89,6 +89,12 @@ class EncryptTests { Collections.synchronizedList(new ArrayList<>()); private static final List crossLanguageObjectsAes = Collections.synchronizedList(new ArrayList<>()); + + // Thread-safe lists for envelope merge tests + private static final List crossLanguageObjectsMetadataOnly = + Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsInstructionFileDeleted = + Collections.synchronizedList(new ArrayList<>()); private static KeyMaterial RSA_KEY; private static KeyMaterial AES_KEY; @@ -139,6 +145,14 @@ static KeyMaterial getKmsKeyArn() { return kmsKeyArn; } + static List getCrossLanguageObjectsMetadataOnly() { + return new ArrayList<>(crossLanguageObjectsMetadataOnly); + } + + static List getCrossLanguageObjectsInstructionFileDeleted() { + return new ArrayList<>(crossLanguageObjectsInstructionFileDeleted); + } + public static Stream improvedClientsCanPutKMSWithInstructionFile() { return improvedClientsForTest() .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) @@ -234,6 +248,55 @@ void encryptWithInstructionFilesAesKcGcm(TestUtils.LanguageServerTarget language ); } + @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM metadata-only for envelope merge test") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encryptMetadataOnlyRsaKcGcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Encrypt with metadata-only (no instruction file) + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-envelope-merge-metadata-only-" + language.getLanguageName()), + crossLanguageObjectsMetadataOnly, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Encrypt RSA KC-GCM with instruction file for deletion test") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutRawRSAWithInstructionFile") + void encryptWithInstructionFileForDeletionRsaKcGcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Encrypt with instruction file (will be deleted later) + 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 + "-envelope-merge-instruction-deleted-" + language.getLanguageName()), + crossLanguageObjectsInstructionFileDeleted, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + static void makeCopiesToVerifyThings() throws Exception { // Create a plaintext S3 client to copy objects with instruction files try (S3Client ptS3Client = S3Client.create()) { @@ -295,6 +358,19 @@ static void makeCopiesToVerifyThings() throws Exception { ); } + + // Delete instruction files for envelope merge tests + for (String objectKey : crossLanguageObjectsInstructionFileDeleted) { + String instructionFileKey = objectKey + ".instruction"; + try { + ptS3Client.deleteObject(builder -> builder + .bucket(TestUtils.BUCKET) + .key(instructionFileKey) + .build()); + } catch (Exception e) { + // Ignore if file doesn't exist + } + } } } @@ -344,6 +420,8 @@ class DecryptTests { private static List crossLanguageObjectsKms; private static List crossLanguageObjectsRsa; private static List crossLanguageObjectsAes; + private static List crossLanguageObjectsMetadataOnly; + private static List crossLanguageObjectsInstructionFileDeleted; private static KeyMaterial kmsKeyArn; private static KeyMaterial RSA_KEY; private static KeyMaterial AES_KEY; @@ -357,6 +435,8 @@ static void setup() throws InterruptedException { crossLanguageObjectsKms = EncryptTests.getCrossLanguageObjectsKms(); crossLanguageObjectsRsa = EncryptTests.getCrossLanguageObjectsRsa(); crossLanguageObjectsAes = EncryptTests.getCrossLanguageObjectsAes(); + crossLanguageObjectsMetadataOnly = EncryptTests.getCrossLanguageObjectsMetadataOnly(); + crossLanguageObjectsInstructionFileDeleted = EncryptTests.getCrossLanguageObjectsInstructionFileDeleted(); kmsKeyArn = EncryptTests.getKmsKeyArn(); RSA_KEY = EncryptTests.getRsaKey(); AES_KEY = EncryptTests.getAesKey(); @@ -784,5 +864,86 @@ void decryptAesWithInstructionFileCommitmentFailsWithForbidPolicy(TestUtils.Lang EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); } + + // Envelope merge tests + + @ParameterizedTest(name = "{0}: Successfully decrypt metadata-only object with instruction file config") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decryptMetadataOnlyObjectWithInstructionFileConfigSucceeds(TestUtils.LanguageServerTarget language) { + if (crossLanguageObjectsMetadataOnly.isEmpty()) return; + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Configure client to look for instruction file but metadata has complete envelope + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Should succeed - instruction file doesn't exist but metadata has complete envelope + TestUtils.Decrypt( + client, + S3ECId, + crossLanguageObjectsMetadataOnly, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt when metadata incomplete and instruction file deleted") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decryptWithIncompleteMetadataAndNoInstructionFileFails(TestUtils.LanguageServerTarget language) { + if (crossLanguageObjectsInstructionFileDeleted.isEmpty()) return; + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Configure client for metadata-only but metadata is incomplete + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Should fail - metadata incomplete (missing x-amz-3, x-amz-w), instruction file deleted + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsInstructionFileDeleted, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt with instruction file config when file deleted and metadata incomplete") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetRawRSAWithInstructionFile") + void decryptWithInstructionFileConfigWhenFileDeletedFails(TestUtils.LanguageServerTarget language) { + if (crossLanguageObjectsInstructionFileDeleted.isEmpty()) return; + + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Configure client to look for instruction file but it's been deleted + CreateClientOutput clientOutput = client.createClient(CreateClientInput.builder() + .config(S3ECConfig.builder() + .keyMaterial(RSA_KEY) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + // Should fail - instruction file deleted, metadata incomplete (missing x-amz-3, x-amz-w) + TestUtils.Decrypt_fails( + client, + S3ECId, + crossLanguageObjectsInstructionFileDeleted, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } } } diff --git a/test-server/ruby-v2-server/Gemfile.lock b/test-server/ruby-v2-server/Gemfile.lock index c253552a..b9f08375 100644 --- a/test-server/ruby-v2-server/Gemfile.lock +++ b/test-server/ruby-v2-server/Gemfile.lock @@ -1,15 +1,15 @@ PATH remote: local-ruby-sdk/gems/aws-sdk-kms specs: - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) PATH remote: local-ruby-sdk/gems/aws-sdk-s3 specs: - aws-sdk-s3 (1.199.1) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.206.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -19,7 +19,7 @@ GEM ast (2.4.3) aws-eventstream (1.4.0) aws-partitions (1.1180.0) - aws-sdk-core (3.236.0) + aws-sdk-core (3.239.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index d6b93925..1f32f5b9 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit d6b93925c65e0ad4b9410ade709c63ca874634c3 +Subproject commit 1f32f5b9ade757b6f2bce0650af43eefe9581d01 diff --git a/test-server/ruby-v3-server/Gemfile.lock b/test-server/ruby-v3-server/Gemfile.lock index c253552a..b9f08375 100644 --- a/test-server/ruby-v3-server/Gemfile.lock +++ b/test-server/ruby-v3-server/Gemfile.lock @@ -1,15 +1,15 @@ PATH remote: local-ruby-sdk/gems/aws-sdk-kms specs: - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) PATH remote: local-ruby-sdk/gems/aws-sdk-s3 specs: - aws-sdk-s3 (1.199.1) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.206.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -19,7 +19,7 @@ GEM ast (2.4.3) aws-eventstream (1.4.0) aws-partitions (1.1180.0) - aws-sdk-core (3.236.0) + aws-sdk-core (3.239.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index d6b93925..1f32f5b9 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit d6b93925c65e0ad4b9410ade709c63ca874634c3 +Subproject commit 1f32f5b9ade757b6f2bce0650af43eefe9581d01 From 5461ae300aa194108914ac0a031eec0c3a6f5da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corella?= <39066999+josecorella@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:54:00 -0800 Subject: [PATCH 166/201] chore: add instruction file manipulation test (#119) --- .../s3/InstructionFileFailures.java | 213 ++++++++++++++++-- .../src/get_object.php | 2 + test-server/php-v3-server/local-php-sdk | 2 +- test-server/php-v3-server/src/get_object.php | 2 + 4 files changed, 205 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 0096a8bf..18ebca47 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 @@ -5,15 +5,13 @@ package software.amazon.encryption.s3; +import static org.junit.jupiter.api.Assertions.assertEquals; 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.Map; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,11 +23,9 @@ 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; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.opentest4j.TestAbortedException; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.services.s3.S3Client; @@ -39,13 +35,7 @@ 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; +import software.amazon.encryption.s3.model.*; /** * Instruction File Failures Test Suite @@ -66,6 +56,8 @@ public class InstructionFileFailures { 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"; + private static final String SUFFIX_BAD_JSON_INSTRUCTION = "-manipulated-bad-json-instruction"; + private static final String SUFFIX_MANIPULATED_INSTRUCTION = "-manipuldate-incorrect-key-instruction"; /** * Encryption Tests - Encrypt Phase @@ -95,6 +87,10 @@ class EncryptTests { Collections.synchronizedList(new ArrayList<>()); private static final List crossLanguageObjectsInstructionFileDeleted = Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsV3InstructionFileManipulated = + Collections.synchronizedList(new ArrayList<>()); + private static final List crossLanguageObjectsV2InstructionFileManipulated = + Collections.synchronizedList(new ArrayList<>()); private static KeyMaterial RSA_KEY; private static KeyMaterial AES_KEY; @@ -153,6 +149,15 @@ static List getCrossLanguageObjectsInstructionFileDeleted() { return new ArrayList<>(crossLanguageObjectsInstructionFileDeleted); } + static List getCrossLanguageObjectsInstructionFileManipulatedV3() { + return new ArrayList<>(crossLanguageObjectsV3InstructionFileManipulated); + } + + static List getCrossLanguageObjectsInstructionFileManipulatedV2() { + return new ArrayList<>(crossLanguageObjectsV2InstructionFileManipulated); + } + + public static Stream improvedClientsCanPutKMSWithInstructionFile() { return improvedClientsForTest() .filter(target -> !INSTRUCTION_FILE_PUT_UNSUPPORTED.contains(((LanguageServerTarget) target.get()[0]).getLanguageName())) @@ -297,6 +302,62 @@ void encryptWithInstructionFileForDeletionRsaKcGcm(TestUtils.LanguageServerTarge ); } + @ParameterizedTest(name = "{0}: Encrypt KMS KC-GCM (V3) with instruction file for manipulation test") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") + void encryptWithInstructionFileV3ForManipulationKmsKcGcm(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Encrypt with instruction file, will be manipulated later on. + 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 + "-envelope-manipulation-instruction-" + language.getLanguageName()), + crossLanguageObjectsV3InstructionFileManipulated, + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Encrypt KMS (V2) with instruction file for manipulation test") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$EncryptTests#improvedClientsCanPutKMSWithInstructionFile") + void encryptWithInstructionFileV2ForManipulationKms(TestUtils.LanguageServerTarget language) { + S3ECTestServerClient client = TestUtils.testServerClientFor(language); + // Encrypt with instruction file, will be manipulated later on. + 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) + .instructionFileConfig( + InstructionFileConfig.builder() + .enableInstructionFilePutObject(true) + .build() + ) + .build()) + .build()); + String S3ECId = clientOutput.getClientId(); + + TestUtils.Encrypt( + client, + S3ECId, + appendTestSuffix(sharedObjectKeyBaseMetaDataMode + "-envelope-manipulation-instruction-" + language.getLanguageName()), + crossLanguageObjectsV2InstructionFileManipulated, + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } + + static void makeCopiesToVerifyThings() throws Exception { // Create a plaintext S3 client to copy objects with instruction files try (S3Client ptS3Client = S3Client.create()) { @@ -371,6 +432,77 @@ static void makeCopiesToVerifyThings() throws Exception { // Ignore if file doesn't exist } } + + // manipulate V3 instruction files + for (String objectKey: crossLanguageObjectsV3InstructionFileManipulated) { + // 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(); + + ObjectMapper mapper = new ObjectMapper(); + + Map invalidInstructionFileMap = new HashMap<>(); + invalidInstructionFileMap.put("invalid", "json"); + + String invalidInstructionFile = mapper.writeValueAsString(invalidInstructionFileMap); + + // Put instruction files that should fail: + putObjectWithInstructionFile( + ptS3Client, + objectKey + SUFFIX_BAD_JSON_INSTRUCTION + "-v3", + encryptedObject.asByteArray(), + objectMetadata, + invalidInstructionFile + ); + } + + // manipulate V2 instruction files + for (String objectKey: crossLanguageObjectsV2InstructionFileManipulated) { + // 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(); + + ObjectMapper mapper = new ObjectMapper(); + Map instructionFileMap = mapper.readValue(instructionFileJson, Map.class); + + instructionFileMap.put("x-amz-key-v2-tampered", instructionFileMap.get("x-amz-key-v2")); + instructionFileMap.remove("x-amz-key-v2"); + + String badKeyInstructionFile = mapper.writeValueAsString(instructionFileMap); + + // Put instruction files that should fail: + putObjectWithInstructionFile( + ptS3Client, + objectKey + SUFFIX_MANIPULATED_INSTRUCTION + "-v2", + encryptedObject.asByteArray(), + objectMetadata, + badKeyInstructionFile + ); + } } } @@ -422,6 +554,8 @@ class DecryptTests { private static List crossLanguageObjectsAes; private static List crossLanguageObjectsMetadataOnly; private static List crossLanguageObjectsInstructionFileDeleted; + private static List crossLanguageObjectsInstructionFileManipulatedV3; + private static List crossLanguageObjectsInstructionFileManipulatedV2; private static KeyMaterial kmsKeyArn; private static KeyMaterial RSA_KEY; private static KeyMaterial AES_KEY; @@ -437,6 +571,8 @@ static void setup() throws InterruptedException { crossLanguageObjectsAes = EncryptTests.getCrossLanguageObjectsAes(); crossLanguageObjectsMetadataOnly = EncryptTests.getCrossLanguageObjectsMetadataOnly(); crossLanguageObjectsInstructionFileDeleted = EncryptTests.getCrossLanguageObjectsInstructionFileDeleted(); + crossLanguageObjectsInstructionFileManipulatedV3 = EncryptTests.getCrossLanguageObjectsInstructionFileManipulatedV3(); + crossLanguageObjectsInstructionFileManipulatedV2 = EncryptTests.getCrossLanguageObjectsInstructionFileManipulatedV2(); kmsKeyArn = EncryptTests.getKmsKeyArn(); RSA_KEY = EncryptTests.getRsaKey(); AES_KEY = EncryptTests.getAesKey(); @@ -945,5 +1081,56 @@ void decryptWithInstructionFileConfigWhenFileDeletedFails(TestUtils.LanguageServ EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY ); } + + @ParameterizedTest(name = "{0}: Fail to decrypt with manipulated V3 Instruction File") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decryptWithManipulatedInstructionFileV3ImprovedClients(TestUtils.LanguageServerTarget language) { + if (TRANSITION_VERSIONS.contains(language.getLanguageName())) { + return; + } + + 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_fails( + client, + S3ECId, + crossLanguageObjectsInstructionFileManipulatedV3 + .stream() + .map(key -> key + SUFFIX_BAD_JSON_INSTRUCTION + "-v3") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + ); + } + + @ParameterizedTest(name = "{0}: Fail to decrypt with manipulated V2 Instruction File") + @MethodSource("software.amazon.encryption.s3.InstructionFileFailures$DecryptTests#clientsCanGetKMSWithInstructionFile") + void decryptWithManipulatedInstructionFileV2ImprovedClients(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, + crossLanguageObjectsInstructionFileManipulatedV2 + .stream() + .map(key -> key + SUFFIX_MANIPULATED_INSTRUCTION + "-v2") + .collect(Collectors.toList()), + EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + ); + } } } 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 847fa9e3..656a337a 100644 --- a/test-server/php-v2-transition-server/src/get_object.php +++ b/test-server/php-v2-transition-server/src/get_object.php @@ -94,6 +94,8 @@ function handleGetObject($params) return S3EncryptionClientError($e->getMessage()); } elseif (strpos($e->getMessage(), "Expected a V3 envelope but was unable to constuct one.") !== false) { return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Malformed metadata envelope.") !== 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 3acb3ad4..163fe386 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 3acb3ad4d98debcfc2148290cd6fcea83962fe08 +Subproject commit 163fe3866e7122d6cd1dbff6f121302db8d98aae diff --git a/test-server/php-v3-server/src/get_object.php b/test-server/php-v3-server/src/get_object.php index 25a88a02..fbd42f7a 100644 --- a/test-server/php-v3-server/src/get_object.php +++ b/test-server/php-v3-server/src/get_object.php @@ -98,6 +98,8 @@ function handleGetObject($params) return S3EncryptionClientError($e->getMessage()); } elseif (strpos($e->getMessage(), "Expected a V3 envelope but was unable to constuct one.") !== false) { return S3EncryptionClientError($e->getMessage()); + } elseif (strpos($e->getMessage(), "Malformed metadata envelope.") !== false) { + return S3EncryptionClientError($e->getMessage()); } else { error_log("This is the error: " . $e->getMessage()); return GenericServerError("Server argument: " . $e->getMessage(), 500); From e2fe1ae8f9acae66464ed07e498bd95fe1ed3a02 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 15:47:19 -0800 Subject: [PATCH 167/201] update .NET submodules --- .gitmodules | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitmodules b/.gitmodules index a88a83f9..4c3e19e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,17 +28,17 @@ url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git branch = fire-egg-staging [submodule "test-server/net-v2-v3-server/s3ec-net-v2"] - path = test-server/net-v2-v3-server/s3ec-net-v2 - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = v3sdk-development + path = test-server/net-v2-v3-server/s3ec-net-v2 + url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git + branch = v3sdk-development [submodule "test-server/net-v2-v3-server/s3ec-net-v3"] - path = test-server/net-v2-v3-server/s3ec-net-v3 - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = s3ec-v3 + path = test-server/net-v2-v3-server/s3ec-net-v3 + url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git + branch = v4sdk-development [submodule "test-server/net-v4-server/s3ec-net-v4-improved"] - path = test-server/net-v4-server/s3ec-net-v4-improved - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = s3ec-v4-WIP + path = test-server/net-v4-server/s3ec-net-v4-improved + url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git + branch = main [submodule "test-server/go-v3-transition-server/local-go-s3ec"] path = test-server/go-v3-transition-server/local-go-s3ec url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging From 9ad08cd7640305a056cacf3af1911581bded84bc Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 15:52:23 -0800 Subject: [PATCH 168/201] fix lint --- src/s3_encryption/__init__.py | 1 + src/s3_encryption/materials/encrypted_data_key.py | 1 + src/s3_encryption/materials/materials.py | 1 + src/s3_encryption/metadata.py | 1 + src/s3_encryption/pipelines.py | 1 + 5 files changed, 5 insertions(+) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index adfb6886..46cdbdd1 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -1,6 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """Top-level S3 Encryption Client v3 for Python package.""" + import io from attrs import define, field diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py index 28401b40..b2c2359a 100644 --- a/src/s3_encryption/materials/encrypted_data_key.py +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -5,6 +5,7 @@ This module provides the EncryptedDataKey class which represents an encrypted data key used in the S3 encryption process. """ + from attrs import define diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index 9f72ab91..3966e17c 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -6,6 +6,7 @@ which contain the cryptographic materials needed for S3 object encryption and decryption operations. """ + from typing import Any from attrs import define, field diff --git a/src/s3_encryption/metadata.py b/src/s3_encryption/metadata.py index b4378990..f42feadb 100644 --- a/src/s3_encryption/metadata.py +++ b/src/s3_encryption/metadata.py @@ -5,6 +5,7 @@ This module provides classes and utilities for managing encryption metadata for S3 objects, including serialization and deserialization of metadata. """ + import json from typing import Any diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index 6046fb3a..37093803 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -5,6 +5,7 @@ This module provides pipelines for encrypting objects before they are put into S3 and decrypting objects after they are retrieved from S3. """ + import base64 import os From efbb48b7a82fd49413341e447db73a3d4ed031aa Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 16:08:14 -0800 Subject: [PATCH 169/201] update NET submodule commits --- test-server/net-v2-v3-server/s3ec-net-v2 | 2 +- test-server/net-v2-v3-server/s3ec-net-v3 | 2 +- test-server/net-v3-transition-server/s3ec-v3-transition-branch | 2 +- test-server/net-v4-server/s3ec-net-v4-improved | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test-server/net-v2-v3-server/s3ec-net-v2 b/test-server/net-v2-v3-server/s3ec-net-v2 index ba85c07e..ed27648c 160000 --- a/test-server/net-v2-v3-server/s3ec-net-v2 +++ b/test-server/net-v2-v3-server/s3ec-net-v2 @@ -1 +1 @@ -Subproject commit ba85c07e0706bae8df242fb7bbfa7e53a264bafa +Subproject commit ed27648c36a2b290b52f94586fce107af7c51fe5 diff --git a/test-server/net-v2-v3-server/s3ec-net-v3 b/test-server/net-v2-v3-server/s3ec-net-v3 index cc942cb5..8c51dd6e 160000 --- a/test-server/net-v2-v3-server/s3ec-net-v3 +++ b/test-server/net-v2-v3-server/s3ec-net-v3 @@ -1 +1 @@ -Subproject commit cc942cb541a733a4340f46bd3e4a1d29a9cbb9a3 +Subproject commit 8c51dd6e2aa2119a2fc8213df34626fbe58fd1c9 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 c3bf38b9..273e0890 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 c3bf38b93c25f7169982073b1ffd1f3d00f59073 +Subproject commit 273e08904275d6357e3a0dad649bc945dce9b98c diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index 04f70c8b..de0fcf75 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 04f70c8b70e25c7a1a36fcd5a420c40806157c66 +Subproject commit de0fcf75c671dbaf2dfefd1fc5ed171c23b05fac From 865e31cc0c996137bf3cf2e0c0e7d48184bf4aee Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 16:31:52 -0800 Subject: [PATCH 170/201] move Ruby to public repos --- .gitmodules | 10 ++++++---- test-server/ruby-v2-server/local-ruby-sdk | 2 +- test-server/ruby-v3-server/local-ruby-sdk | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitmodules b/.gitmodules index 4c3e19e8..59dd8854 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,11 @@ [submodule "test-server/ruby-v2-server/local-ruby-sdk"] path = test-server/ruby-v2-server/local-ruby-sdk - url = git@github.com:aws/aws-sdk-ruby-staging.git + url = git@github.com:aws/aws-sdk-ruby.git + branch = version-3 [submodule "test-server/ruby-v3-server/local-ruby-sdk"] path = test-server/ruby-v3-server/local-ruby-sdk - url = git@github.com:aws/aws-sdk-ruby-staging.git + url = git@github.com:aws/aws-sdk-ruby.git + branch = version-3 [submodule "test-server/php-v2-server/local-php-sdk"] path = test-server/php-v2-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git @@ -45,8 +47,8 @@ branch = v3-strip [submodule "test-server/net-v3-transition-server/s3ec-v3-transition-branch"] path = test-server/net-v3-transition-server/s3ec-v3-transition-branch - url = https://github.com/aws/private-amazon-s3-encryption-client-dotnet-staging.git - branch = rishav/key-commitment + url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git + branch = v4sdk-development [submodule "test-server/cpp-v2-transition-server/aws-sdk-cpp"] path = test-server/cpp-v2-transition-server/aws-sdk-cpp url = git@github.com:awslabs/aws-sdk-cpp-staging.git diff --git a/test-server/ruby-v2-server/local-ruby-sdk b/test-server/ruby-v2-server/local-ruby-sdk index 1f32f5b9..93985e94 160000 --- a/test-server/ruby-v2-server/local-ruby-sdk +++ b/test-server/ruby-v2-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 1f32f5b9ade757b6f2bce0650af43eefe9581d01 +Subproject commit 93985e94bbe8345cc7d615d1cdbcd7516ac16bcd diff --git a/test-server/ruby-v3-server/local-ruby-sdk b/test-server/ruby-v3-server/local-ruby-sdk index 1f32f5b9..93985e94 160000 --- a/test-server/ruby-v3-server/local-ruby-sdk +++ b/test-server/ruby-v3-server/local-ruby-sdk @@ -1 +1 @@ -Subproject commit 1f32f5b9ade757b6f2bce0650af43eefe9581d01 +Subproject commit 93985e94bbe8345cc7d615d1cdbcd7516ac16bcd From d1a707ca9619b6209e4679e0c9890a0cfbaaadc7 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 17:07:11 -0800 Subject: [PATCH 171/201] specify bundler version to match lockfile --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 182b7f47..dd6131a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,6 +65,8 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" + bundler: "2.7.2" + bundler-cache: true - name: Set up PHP with Composer uses: shivammathur/setup-php@verbose From 63c5af17080d59b05d8303307dfb476afbb2496f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 17:21:21 -0800 Subject: [PATCH 172/201] install xcode tools --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd6131a0..04e8cbd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,13 @@ jobs: test-server/cpp-v3-server/aws-sdk-cpp \ test-server/cpp-v2-server/aws-sdk-cpp + - name: Setup build environment + run: | + # Verify Xcode CLI tools are available + xcode-select -p || xcode-select --install + # Accept Xcode license if needed + sudo xcodebuild -license accept || true + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From 14e3fb1b069a1bd9118443205c0864bb36320139 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 18:03:29 -0800 Subject: [PATCH 173/201] try ruby 3.4.7 --- .github/workflows/examples.yml | 2 +- .github/workflows/test.yml | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 4b1cde5a..8cb32ab2 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -34,7 +34,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.4" + ruby-version: "3.4.7" - name: Set up PHP with Composer uses: shivammathur/setup-php@verbose diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04e8cbd7..d3d101b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,18 +61,10 @@ jobs: test-server/cpp-v3-server/aws-sdk-cpp \ test-server/cpp-v2-server/aws-sdk-cpp - - name: Setup build environment - run: | - # Verify Xcode CLI tools are available - xcode-select -p || xcode-select --install - # Accept Xcode license if needed - sudo xcodebuild -license accept || true - - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.4" - bundler: "2.7.2" + ruby-version: "3.4.7" bundler-cache: true - name: Set up PHP with Composer From cc4d1cf91bd073d5c7c6075f2145009a7851fff9 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 18:37:21 -0800 Subject: [PATCH 174/201] supress CVE --- test-server/net-v2-v3-server/NetV2V3Server.csproj | 4 ++++ .../net-v3-transition-server/NetV3TransitionServer.csproj | 4 ++++ test-server/net-v4-server/NetV4Server.csproj | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/test-server/net-v2-v3-server/NetV2V3Server.csproj b/test-server/net-v2-v3-server/NetV2V3Server.csproj index 8d664eff..91e1dfa1 100644 --- a/test-server/net-v2-v3-server/NetV2V3Server.csproj +++ b/test-server/net-v2-v3-server/NetV2V3Server.csproj @@ -36,4 +36,8 @@ + + + + diff --git a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj index 269f555f..3a092af8 100644 --- a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj +++ b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/test-server/net-v4-server/NetV4Server.csproj b/test-server/net-v4-server/NetV4Server.csproj index 28ddba06..45ed1a1a 100644 --- a/test-server/net-v4-server/NetV4Server.csproj +++ b/test-server/net-v4-server/NetV4Server.csproj @@ -25,4 +25,8 @@ + + + + From 9b9f4c578cfc7f4020ef222081c9e57e02f8dfb7 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 21 Jan 2026 18:59:52 -0800 Subject: [PATCH 175/201] try again to disable nuget audit errors --- .../net-v3-transition-server/NetV3TransitionServer.csproj | 1 + test-server/net-v4-server/NetV4Server.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj index 3a092af8..e9ca3798 100644 --- a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj +++ b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj @@ -2,6 +2,7 @@ net8.0 + moderate enable enable false diff --git a/test-server/net-v4-server/NetV4Server.csproj b/test-server/net-v4-server/NetV4Server.csproj index 45ed1a1a..71e818e5 100644 --- a/test-server/net-v4-server/NetV4Server.csproj +++ b/test-server/net-v4-server/NetV4Server.csproj @@ -6,6 +6,7 @@ enable false NetV2V3Server + moderate From 88e832061a3ede9b694063e72106757ec6477c67 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 11:54:04 -0800 Subject: [PATCH 176/201] fixup NET --- test-server/net-v2-v3-server/NetV2V3Server.csproj | 4 ---- .../net-v3-transition-server/NetV3TransitionServer.csproj | 5 ----- .../net-v3-transition-server/s3ec-v3-transition-branch | 2 +- test-server/net-v4-server/NetV4Server.csproj | 5 ----- test-server/net-v4-server/s3ec-net-v4-improved | 2 +- 5 files changed, 2 insertions(+), 16 deletions(-) diff --git a/test-server/net-v2-v3-server/NetV2V3Server.csproj b/test-server/net-v2-v3-server/NetV2V3Server.csproj index 91e1dfa1..8d664eff 100644 --- a/test-server/net-v2-v3-server/NetV2V3Server.csproj +++ b/test-server/net-v2-v3-server/NetV2V3Server.csproj @@ -36,8 +36,4 @@ - - - - diff --git a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj index e9ca3798..269f555f 100644 --- a/test-server/net-v3-transition-server/NetV3TransitionServer.csproj +++ b/test-server/net-v3-transition-server/NetV3TransitionServer.csproj @@ -2,7 +2,6 @@ net8.0 - moderate enable enable false @@ -25,8 +24,4 @@ - - - - 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 273e0890..7a552940 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 273e08904275d6357e3a0dad649bc945dce9b98c +Subproject commit 7a55294068bb3bb7f96226efd6d9edcd1057184b diff --git a/test-server/net-v4-server/NetV4Server.csproj b/test-server/net-v4-server/NetV4Server.csproj index 71e818e5..28ddba06 100644 --- a/test-server/net-v4-server/NetV4Server.csproj +++ b/test-server/net-v4-server/NetV4Server.csproj @@ -6,7 +6,6 @@ enable false NetV2V3Server - moderate @@ -26,8 +25,4 @@ - - - - diff --git a/test-server/net-v4-server/s3ec-net-v4-improved b/test-server/net-v4-server/s3ec-net-v4-improved index de0fcf75..9b628b06 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 de0fcf75c671dbaf2dfefd1fc5ed171c23b05fac +Subproject commit 9b628b06e5c1bf12696c752afb2631c38cae11f9 From b9b2470fcc8a9da26fbf40d66ed1cd34f6d7c46a Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 12:19:19 -0800 Subject: [PATCH 177/201] NET again --- test-server/net-v2-v3-server/s3ec-net-v3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/net-v2-v3-server/s3ec-net-v3 b/test-server/net-v2-v3-server/s3ec-net-v3 index 8c51dd6e..7a552940 160000 --- a/test-server/net-v2-v3-server/s3ec-net-v3 +++ b/test-server/net-v2-v3-server/s3ec-net-v3 @@ -1 +1 @@ -Subproject commit 8c51dd6e2aa2119a2fc8213df34626fbe58fd1c9 +Subproject commit 7a55294068bb3bb7f96226efd6d9edcd1057184b From d396df770f08db6807cc452f13286ec787067ed0 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 14:45:31 -0800 Subject: [PATCH 178/201] move PHP, CPP over to public repos --- .gitmodules | 17 +++++++++-------- test-server/cpp-v2-server/aws-sdk-cpp | 2 +- .../cpp-v2-transition-server/aws-sdk-cpp | 2 +- test-server/cpp-v3-server/aws-sdk-cpp | 2 +- test-server/php-v2-server/local-php-sdk | 2 +- test-server/php-v3-server/local-php-sdk | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitmodules b/.gitmodules index 59dd8854..b3f0127c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,12 +8,12 @@ branch = version-3 [submodule "test-server/php-v2-server/local-php-sdk"] path = test-server/php-v2-server/local-php-sdk - url = git@github.com:aws/private-aws-sdk-php-staging.git + url = git@github.com:aws/aws-sdk-php.git branch = master [submodule "test-server/php-v3-server/local-php-sdk"] path = test-server/php-v3-server/local-php-sdk - url = git@github.com:aws/private-aws-sdk-php-staging.git - branch = s3ec/improved + url = git@github.com:aws/aws-sdk-php.git + branch = master [submodule "test-server/go-v4-server/local-go-s3ec"] path = test-server/go-v4-server/local-go-s3ec url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging @@ -51,12 +51,13 @@ branch = v4sdk-development [submodule "test-server/cpp-v2-transition-server/aws-sdk-cpp"] path = test-server/cpp-v2-transition-server/aws-sdk-cpp - url = git@github.com:awslabs/aws-sdk-cpp-staging.git - branch = fire-egg-dev + url = git@github.com:aws/aws-sdk-cpp.git + branch = main [submodule "test-server/cpp-v3-server/aws-sdk-cpp"] path = test-server/cpp-v3-server/aws-sdk-cpp - url = git@github.com:awslabs/aws-sdk-cpp-staging.git - branch = fire-egg-dev + url = git@github.com:aws/aws-sdk-cpp.git + branch = main [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 + url = git@github.com:aws/aws-sdk-cpp.git + branch = main diff --git a/test-server/cpp-v2-server/aws-sdk-cpp b/test-server/cpp-v2-server/aws-sdk-cpp index 994384ca..9110b0ff 160000 --- a/test-server/cpp-v2-server/aws-sdk-cpp +++ b/test-server/cpp-v2-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 994384ca8b9defe2ae60b5d3447ec5f47f7ec19f +Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c diff --git a/test-server/cpp-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp index cec1f193..9110b0ff 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 cec1f1933be672f65627c11ff2d853e07c5b3ff4 +Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c diff --git a/test-server/cpp-v3-server/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp index cec1f193..9110b0ff 160000 --- a/test-server/cpp-v3-server/aws-sdk-cpp +++ b/test-server/cpp-v3-server/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit cec1f1933be672f65627c11ff2d853e07c5b3ff4 +Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c diff --git a/test-server/php-v2-server/local-php-sdk b/test-server/php-v2-server/local-php-sdk index ab8aee74..f53d8fc6 160000 --- a/test-server/php-v2-server/local-php-sdk +++ b/test-server/php-v2-server/local-php-sdk @@ -1 +1 @@ -Subproject commit ab8aee74db1141da07c9c979cf313418fddae256 +Subproject commit f53d8fc6cdbc1e64e7d14e72d1e315d05003b2b4 diff --git a/test-server/php-v3-server/local-php-sdk b/test-server/php-v3-server/local-php-sdk index 163fe386..f53d8fc6 160000 --- a/test-server/php-v3-server/local-php-sdk +++ b/test-server/php-v3-server/local-php-sdk @@ -1 +1 @@ -Subproject commit 163fe3866e7122d6cd1dbff6f121302db8d98aae +Subproject commit f53d8fc6cdbc1e64e7d14e72d1e315d05003b2b4 From 3c5b79dd1e28d9b2e7681e33e92e859549774978 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 15:18:50 -0800 Subject: [PATCH 179/201] make C++ examples use public repo --- .github/workflows/examples.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 8cb32ab2..2a8c6aca 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -22,8 +22,8 @@ jobs: with: submodules: recursive token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev + repository: aws/aws-sdk-cpp + ref: main path: all-examples/cpp/aws-sdk-cpp/ - name: Set up Python From 5c5767a985c404a6d147943ac8f4d8496285eb1b Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 15:35:23 -0800 Subject: [PATCH 180/201] no token for C++ --- .github/workflows/examples.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 2a8c6aca..b442be5a 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -21,7 +21,6 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} repository: aws/aws-sdk-cpp ref: main path: all-examples/cpp/aws-sdk-cpp/ From d5a4233dc0a29ae0d9a2fbc979b5939c0137c14f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 26 Jan 2026 15:59:29 -0800 Subject: [PATCH 181/201] fix Duvet --- .github/workflows/duvet.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index 529be19d..8b277ec0 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -23,9 +23,8 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive - token: ${{ secrets.PAT_FOR_CPP }} - repository: awslabs/aws-sdk-cpp-staging - ref: fire-egg-dev + repository: aws/aws-sdk-cpp + ref: main path: test-server/cpp-v3-server/aws-sdk-cpp/ - name: Setup Rust toolchain From 7bcf5eb8bfd757a86241eb1c55d1642e9f210952 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 27 Jan 2026 14:13:26 -0800 Subject: [PATCH 182/201] disable CURRENT tests --- .../amazon/encryption/s3/TestUtils.java | 159 +++++++++--------- 1 file changed, 79 insertions(+), 80 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 f2065115..f06f5cd2 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 @@ -59,32 +59,32 @@ public class TestUtils { // vN-Transition: Proposed feature release version. Supports reading messages encrypted with key commitment. // vN+1: Proposed breaking release version. Supports reading/writing messages encrypted with key commitment. - public static final String JAVA_V3_CURRENT = "Java-V3-Current"; +// public static final String JAVA_V3_CURRENT = "Java-V3-Current"; public static final String JAVA_V3_TRANSITION = "Java-V3-Transition"; public static final String JAVA_V4 = "Java-V4"; // No Python S3EC versions are released. Only test V3 as the "vN+1" version. public static final String PYTHON_V3 = "Python-V3"; - public static final String GO_V3_CURRENT = "Go-V3-Current"; +// public static final String GO_V3_CURRENT = "Go-V3-Current"; public static final String GO_V3_TRANSITION = "Go-V3-Transition"; public static final String GO_V4 = "Go-V4"; - public static final String NET_V2_CURRENT = "NET-V2-Current"; - public static final String NET_V3_CURRENT = "NET-V3-Current"; +// public static final String NET_V2_CURRENT = "NET-V2-Current"; +// public static final String NET_V3_CURRENT = "NET-V3-Current"; public static final String NET_V2_TRANSITION = "NET-V2-Transition"; public static final String NET_V3_TRANSITION = "NET-V3-Transition"; public static final String NET_V4 = "NET-V4"; - public static final String CPP_V2_CURRENT = "CPP-V2-Current"; +// public static final String CPP_V2_CURRENT = "CPP-V2-Current"; public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; public static final String CPP_V3 = "CPP-V3"; - public static final String RUBY_V2_CURRENT = "Ruby-V2-Current"; +// public static final String RUBY_V2_CURRENT = "Ruby-V2-Current"; public static final String RUBY_V2_TRANSITION = "Ruby-V2-Transition"; public static final String RUBY_V3 = "Ruby-V3"; - public static final String PHP_V2_CURRENT = "PHP-V2-Current"; +// public static final String PHP_V2_CURRENT = "PHP-V2-Current"; public static final String PHP_V2_TRANSITION = "PHP-V2-Transition"; public static final String PHP_V3 = "PHP-V3"; @@ -97,33 +97,31 @@ 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(PHP_V2_TRANSITION, PHP_V3, 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_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_V3_TRANSITION, NET_V4); +// 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); + Set.of(JAVA_V3_TRANSITION, JAVA_V4); +// 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 + JAVA_V3_TRANSITION, JAVA_V4, CPP_V2_TRANSITION, CPP_V3 +// 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 = - 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 - ); + Set.of(JAVA_V3_TRANSITION, JAVA_V4, NET_V3_TRANSITION, NET_V4, RUBY_V2_TRANSITION, RUBY_V3, CPP_V2_TRANSITION, CPP_V3); +// 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 - ); + Set.of(JAVA_V3_TRANSITION, JAVA_V4, NET_V3_TRANSITION, NET_V4, RUBY_V2_TRANSITION, RUBY_V3); +// 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 = @@ -134,13 +132,14 @@ 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_TRANSITION, NET_V3_TRANSITION, NET_V4); +// Set.of(NET_V2_CURRENT, NET_V2_TRANSITION, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); // Go does not write with instruction files public static final Set INSTRUCTION_FILE_PUT_UNSUPPORTED = - Set.of(GO_V3_CURRENT, GO_V3_TRANSITION, GO_V4, PYTHON_V3 + Set.of(GO_V3_TRANSITION, GO_V4, PYTHON_V3); +// Set.of(GO_V3_CURRENT, GO_V3_TRANSITION, GO_V4, PYTHON_V3, CPP_V2_CURRENT); // Apparently C++ V2 Current does not work, even though it should - , CPP_V2_CURRENT); // Not implemented yet in Python. public static final Set INSTRUCTION_FILE_GET_UNSUPPORTED = @@ -159,17 +158,17 @@ public class TestUtils { PHP_V3 ); - public static final Set CURRENT_VERSIONS = - Set.of( - JAVA_V3_CURRENT, - GO_V3_CURRENT, - NET_V2_CURRENT, - NET_V3_CURRENT, - CPP_V2_CURRENT, - RUBY_V2_CURRENT, - PHP_V2_CURRENT - ); - +// public static final Set CURRENT_VERSIONS = +// Set.of( +// JAVA_V3_CURRENT, +// GO_V3_CURRENT, +// NET_V2_CURRENT, +// NET_V3_CURRENT, +// CPP_V2_CURRENT, +// RUBY_V2_CURRENT, +// PHP_V2_CURRENT +// ); +// public static final Set TRANSITION_VERSIONS = Set.of( JAVA_V3_TRANSITION, @@ -196,16 +195,16 @@ public class TestUtils { static { final Map servers = new LinkedHashMap<>(); - servers.put(JAVA_V3_CURRENT, new LanguageServerTarget(JAVA_V3_CURRENT, "8080")); +// servers.put(JAVA_V3_CURRENT, new LanguageServerTarget(JAVA_V3_CURRENT, "8080")); servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); - servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); - servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); - servers.put(NET_V3_CURRENT, new LanguageServerTarget(NET_V3_CURRENT, "8084")); - servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); +// servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); +// servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); +// servers.put(NET_V3_CURRENT, new LanguageServerTarget(NET_V3_CURRENT, "8084")); +// servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); - servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); +// servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); servers.put(NET_V4, new LanguageServerTarget(NET_V4, "8090")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); @@ -377,14 +376,14 @@ public static Stream clientsForTest() { .map(Arguments::of); } - /** - * Get stream of arguments for current version clients for testing. - */ - public static Stream currentClientsForTest() { - return serverMap.values().stream() - .filter(target -> CURRENT_VERSIONS.contains(target.getLanguageName())) - .map(Arguments::of); - } +// /** +// * Get stream of arguments for current version clients for testing. +// */ +// public static Stream currentClientsForTest() { +// return serverMap.values().stream() +// .filter(target -> CURRENT_VERSIONS.contains(target.getLanguageName())) +// .map(Arguments::of); +// } /** * Get stream of arguments for transition version clients for testing. @@ -454,37 +453,37 @@ public static Stream encryptTransitionDecryptImproved() { ))); } - public static Stream encryptImprovedDecryptCurrent() { - return improvedClientsForTest() - .flatMap(encrypt -> currentClientsForTest() - .flatMap(decrypt -> Stream.of( - Arguments.of(encrypt.get()[0], decrypt.get()[0]) - ))); - } - - public static Stream encryptCurrentDecryptImproved() { - return currentClientsForTest() - .flatMap(encrypt -> improvedClientsForTest() - .flatMap(decrypt -> Stream.of( - Arguments.of(encrypt.get()[0], decrypt.get()[0]) - ))); - } - - public static Stream encryptTransitionDecryptCurrent() { - return transitionClientsForTest() - .flatMap(encrypt -> currentClientsForTest() - .flatMap(decrypt -> Stream.of( - Arguments.of(encrypt.get()[0], decrypt.get()[0]) - ))); - } - - public static Stream encryptCurrentDecryptTransition() { - return currentClientsForTest() - .flatMap(encrypt -> transitionClientsForTest() - .flatMap(decrypt -> Stream.of( - Arguments.of(encrypt.get()[0], decrypt.get()[0]) - ))); - } +// public static Stream encryptImprovedDecryptCurrent() { +// return improvedClientsForTest() +// .flatMap(encrypt -> currentClientsForTest() +// .flatMap(decrypt -> Stream.of( +// Arguments.of(encrypt.get()[0], decrypt.get()[0]) +// ))); +// } + +// public static Stream encryptCurrentDecryptImproved() { +// return currentClientsForTest() +// .flatMap(encrypt -> improvedClientsForTest() +// .flatMap(decrypt -> Stream.of( +// Arguments.of(encrypt.get()[0], decrypt.get()[0]) +// ))); +// } + +// public static Stream encryptTransitionDecryptCurrent() { +// return transitionClientsForTest() +// .flatMap(encrypt -> currentClientsForTest() +// .flatMap(decrypt -> Stream.of( +// Arguments.of(encrypt.get()[0], decrypt.get()[0]) +// ))); +// } + +// public static Stream encryptCurrentDecryptTransition() { +// return currentClientsForTest() +// .flatMap(encrypt -> transitionClientsForTest() +// .flatMap(decrypt -> Stream.of( +// Arguments.of(encrypt.get()[0], decrypt.get()[0]) +// ))); +// } /** * Provides a stream of arguments for parameterized tests that test cross-language compatibility From a11430c84b40f25850e07bc2bc131dd11cae73ee Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 27 Jan 2026 14:52:58 -0800 Subject: [PATCH 183/201] fix compile --- .../software/amazon/encryption/s3/RoundTripTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 cf45006e..4821aec1 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 @@ -213,7 +213,7 @@ public void crossLanguageTestKmsWithSubsetEncCtxFails(LanguageServerTarget encLa .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { 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"), "Actual error: " + e.getMessage()); @@ -277,7 +277,7 @@ public void crossLanguageTestKmsWithIncorrectEncCtxFails(LanguageServerTarget en .build()); fail("Expected exception!"); } catch (S3EncryptionClientError e) { - if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_CURRENT) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { + if (decLang.getLanguageName().equals(RUBY_V3) || decLang.getLanguageName().equals(RUBY_V2_TRANSITION)) { 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"), "Actual error: " + e.getMessage()); @@ -423,12 +423,12 @@ 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) - || language.getLanguageName().equals(CPP_V2_CURRENT) || language.getLanguageName().equals(CPP_V2_TRANSITION) || language.getLanguageName().equals(CPP_V3)) { + if (language.getLanguageName().equals(NET_V3_TRANSITION) || language.getLanguageName().equals(NET_V4) + || 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)) { + } else if (language.getLanguageName().equals(RUBY_V3) || 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()); From 9626325a6c6817404b8ab9da86b9431757fd9974 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 28 Jan 2026 15:52:55 -0800 Subject: [PATCH 184/201] delete cpp-v2-server --- .github/workflows/test.yml | 3 +- .gitmodules | 4 - test-server/cpp-v2-server/.duvet/.gitignore | 3 - test-server/cpp-v2-server/CMakeLists.txt | 39 - test-server/cpp-v2-server/Makefile | 38 - test-server/cpp-v2-server/README.md | 37 - test-server/cpp-v2-server/aws-sdk-cpp | 1 - test-server/cpp-v2-server/main.cpp | 668 ------------------ .../amazon/encryption/s3/TestUtils.java | 2 - 9 files changed, 1 insertion(+), 794 deletions(-) delete mode 100644 test-server/cpp-v2-server/.duvet/.gitignore delete mode 100644 test-server/cpp-v2-server/CMakeLists.txt delete mode 100644 test-server/cpp-v2-server/Makefile delete mode 100644 test-server/cpp-v2-server/README.md delete mode 160000 test-server/cpp-v2-server/aws-sdk-cpp delete mode 100644 test-server/cpp-v2-server/main.cpp diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3d101b5..b934bc20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,8 +58,7 @@ jobs: --jobs ${{ steps.cpu-count.outputs.count }} \ --force \ test-server/cpp-v2-transition-server/aws-sdk-cpp \ - test-server/cpp-v3-server/aws-sdk-cpp \ - test-server/cpp-v2-server/aws-sdk-cpp + test-server/cpp-v3-server/aws-sdk-cpp - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.gitmodules b/.gitmodules index b3f0127c..cc14cae1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -57,7 +57,3 @@ path = test-server/cpp-v3-server/aws-sdk-cpp url = git@github.com:aws/aws-sdk-cpp.git branch = main -[submodule "test-server/cpp-v2-server/aws-sdk-cpp"] - path = test-server/cpp-v2-server/aws-sdk-cpp - url = git@github.com:aws/aws-sdk-cpp.git - branch = main diff --git a/test-server/cpp-v2-server/.duvet/.gitignore b/test-server/cpp-v2-server/.duvet/.gitignore deleted file mode 100644 index 93956e36..00000000 --- a/test-server/cpp-v2-server/.duvet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -reports/ -requirements/ -specification/ \ No newline at end of file diff --git a/test-server/cpp-v2-server/CMakeLists.txt b/test-server/cpp-v2-server/CMakeLists.txt deleted file mode 100644 index b282dbc4..00000000 --- a/test-server/cpp-v2-server/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(s3ec-cpp-v2-server) - -set(CMAKE_CXX_STANDARD 17) - -# Configure AWS SDK build options -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") - -# Add AWS SDK as subdirectory -add_subdirectory(aws-sdk-cpp) - -find_package(PkgConfig REQUIRED) -pkg_check_modules(LIBMICROHTTPD REQUIRED libmicrohttpd) - -find_package(nlohmann_json REQUIRED) - -add_executable(s3ec-server main.cpp) - -target_include_directories(s3ec-server PRIVATE - ${LIBMICROHTTPD_INCLUDE_DIRS} - /opt/homebrew/include -) - -target_link_directories(s3ec-server PRIVATE - ${LIBMICROHTTPD_LIBRARY_DIRS} - /opt/homebrew/lib -) - -target_link_libraries(s3ec-server - ${LIBMICROHTTPD_LIBRARIES} - aws-cpp-sdk-core - aws-cpp-sdk-kms - aws-cpp-sdk-s3 - aws-cpp-sdk-s3-encryption - nlohmann_json::nlohmann_json - uuid -) \ No newline at end of file diff --git a/test-server/cpp-v2-server/Makefile b/test-server/cpp-v2-server/Makefile deleted file mode 100644 index 2d0a4b55..00000000 --- a/test-server/cpp-v2-server/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Makefile for S3 Encryption Client Testing - -.PHONY: build-server start-server stop-server wait-for-server - -PID_FILE := server.pid -PORT := 8085 - -build/s3ec-server: - cd aws-sdk-cpp - mkdir -p build && cd build && cmake .. - -build-server: | build/s3ec-server - @echo "Building Cpp V2 server..." - cd build && $(MAKE) - -start-server: - @echo "Starting Cpp V2 server..." - cd build && \ - 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" \ - ./s3ec-server > ../server.log 2>&1 & echo $$! > ../$(PID_FILE) - @echo "Cpp V2 server starting..." - -stop-server: - @echo "Stopping server on port $(PORT)..." - @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE) ]; then \ - pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm -f $(PID_FILE); \ - fi - @rm -f server.log - @echo "Server stopped" - -wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/cpp-v2-server/README.md b/test-server/cpp-v2-server/README.md deleted file mode 100644 index 8e77feda..00000000 --- a/test-server/cpp-v2-server/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# C++ S3 Encryption Test Server - -Minimal C++ implementation of the S3 Encryption test server. - -## Dependencies - -- libmicrohttpd -- AWS SDK for C++ -- nlohmann/json -- uuid - -On MacOS you can -```bash -brew install libmicrohttpd nlohmann-json ossp-uuid -``` - -## Build - -```bash -mkdir build && cd build -cmake .. -make -``` - -## Run - -```bash -./s3ec-server -``` - -Server runs on localhost:8085 - -## API Endpoints - -- `POST /client` - Create S3 encryption client -- `GET /object/{bucket}/{key}` - Get encrypted object -- `PUT /object/{bucket}/{key}` - Put encrypted object \ No newline at end of file diff --git a/test-server/cpp-v2-server/aws-sdk-cpp b/test-server/cpp-v2-server/aws-sdk-cpp deleted file mode 160000 index 9110b0ff..00000000 --- a/test-server/cpp-v2-server/aws-sdk-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c diff --git a/test-server/cpp-v2-server/main.cpp b/test-server/cpp-v2-server/main.cpp deleted file mode 100644 index e8ffe770..00000000 --- a/test-server/cpp-v2-server/main.cpp +++ /dev/null @@ -1,668 +0,0 @@ -/* - * 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 -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using json = nlohmann::json; -using namespace Aws::S3Encryption; -using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; - -// 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; - uuid_generate(uuid); - char uuid_str[37]; - uuid_unparse(uuid, uuid_str); - return std::string(uuid_str); -} - -std::shared_ptr get_client(const std::string &client_id) -{ - // 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 { - // 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) -{ - // 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, - const char *key) { - const char *value = - MHD_lookup_connection_value(connection, MHD_HEADER_KIND, key); - return value ? std::string(value) : ""; -} - -MHD_Result send_response(struct MHD_Connection *connection, int status_code, - const std::string &content) { - struct MHD_Response *response = MHD_create_response_from_buffer( - content.length(), (void *)content.data(), MHD_RESPMEM_MUST_COPY); - MHD_Result ret = MHD_queue_response(connection, status_code, response); - MHD_destroy_response(response); - return ret; -} - -std::string make_error(const std::string &message, int status_code) { - return "{\"__type\": " - "\"software.amazon.encryption.s3#S3EncryptionClientError\", " - "\"message\": \"" + - message + "\"}"; -} - -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"]; - bool legacy1 = request["config"]["enableLegacyWrappingAlgorithms"]; - bool legacy2 = request["config"]["enableLegacyUnauthenticatedModes"]; - bool inst_put = false; - if (request["config"].contains("instructionFileConfig") && - request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) { - inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"]; - } - - auto materials = - std::make_shared(kms_key_id); - 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); - - // 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"); - - // 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(); - set_client(client_id, encryption_client); - - 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 (...) { - return send_response(connection, 500, "{\"error\":\"Unknown error\"}"); - } -} - -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 - size_t key_start = current.find('[', pos); - if (key_start == std::string::npos) - break; - - // Find closing bracket for key - size_t key_end = current.find(']', key_start); - if (key_end == std::string::npos) - break; - - // Find colon separator - size_t colon = current.find(':', key_end); - if (colon == std::string::npos) - break; - - // Find opening bracket for value - size_t value_start = current.find('[', colon); - if (value_start == std::string::npos) - break; - - // Find closing bracket for value - size_t value_end = current.find(']', value_start); - if (value_end == std::string::npos) - break; - - // Extract key and value - std::string key = current.substr(key_start + 1, key_end - key_start - 1); - 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); - - // Move to next pair (look for comma or next opening bracket) - pos = value_end + 1; - size_t comma = current.find(',', pos); - if (comma != std::string::npos) { - 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, - std::string bucket, - std::string key, - std::string client_id, - 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, 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) { - 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\"}"); - } - - try { - 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); - - // 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::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) { - 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, - 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); - - Aws::S3::Model::PutObjectRequest request; - request.SetBucket(bucket); - request.SetKey(key); - - // 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) { - 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; - } - - // 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; - } - - // 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") { - 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") { - fprintf(stderr, "[CPP-V2] Dispatching to handle_get_object\n"); - 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") { - 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 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_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, 10, - MHD_OPTION_END); - - if (!daemon) { - fprintf(stderr, "[CPP-V2] Failed to start server on port %d\n", port); - Aws::ShutdownAPI(options); - return 1; - } - - fprintf(stderr, "Server running on port %d\n", port); - sleep(10000); - - MHD_stop_daemon(daemon); - Aws::ShutdownAPI(options); - fprintf(stderr, "Ending server\n"); - return 0; -} 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 f06f5cd2..510d57b6 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 @@ -76,7 +76,6 @@ public class TestUtils { public static final String NET_V3_TRANSITION = "NET-V3-Transition"; public static final String NET_V4 = "NET-V4"; -// public static final String CPP_V2_CURRENT = "CPP-V2-Current"; public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; public static final String CPP_V3 = "CPP-V3"; @@ -200,7 +199,6 @@ public class TestUtils { // servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); // servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); // servers.put(NET_V3_CURRENT, new LanguageServerTarget(NET_V3_CURRENT, "8084")); -// servers.put(CPP_V2_CURRENT, new LanguageServerTarget(CPP_V2_CURRENT, "8085")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); From 51e707168e5e3726e5920bd583add79a713f1994 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 11:49:44 -0800 Subject: [PATCH 185/201] delete Java, Go, PHP current servers --- .github/workflows/examples.yml | 3 +- .github/workflows/test.yml | 8 +- .gitmodules | 4 - test-server/README.md | 7 - test-server/go-v3-server/Makefile | 39 -- test-server/go-v3-server/README.md | 23 -- test-server/go-v3-server/go.mod | 31 -- test-server/go-v3-server/go.sum | 46 --- test-server/go-v3-server/main.go | 370 ------------------ .../amazon/encryption/s3/RoundTripTests.java | 4 - .../amazon/encryption/s3/TestUtils.java | 3 - test-server/java-v3-server/.duvet/.gitignore | 3 - test-server/java-v3-server/.duvet/config.toml | 21 - test-server/java-v3-server/.gitignore | 1 - test-server/java-v3-server/Makefile | 39 -- test-server/java-v3-server/README.md | 23 -- test-server/java-v3-server/build.gradle.kts | 56 --- test-server/java-v3-server/gradle.properties | 24 -- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - test-server/java-v3-server/gradlew | 249 ------------ test-server/java-v3-server/gradlew.bat | 92 ----- test-server/java-v3-server/license.txt | 4 - .../java-v3-server/settings.gradle.kts | 19 - test-server/java-v3-server/smithy-build.json | 11 - test-server/java-v3-server/specification | 1 - .../s3/CreateClientOperationImpl.java | 155 -------- .../encryption/s3/GetObjectOperationImpl.java | 78 ---- .../amazon/encryption/s3/MetadataUtils.java | 43 -- .../encryption/s3/PutObjectOperationImpl.java | 55 --- .../encryption/s3/ReEncryptOperationImpl.java | 185 --------- .../encryption/s3/S3ECJavaTestServer.java | 55 --- test-server/php-v2-server/.duvet/.gitignore | 3 - test-server/php-v2-server/.duvet/config.toml | 24 -- test-server/php-v2-server/.gitignore | 4 - test-server/php-v2-server/Makefile | 39 -- test-server/php-v2-server/README.md | 69 ---- test-server/php-v2-server/composer.json | 36 -- test-server/php-v2-server/local-php-sdk | 1 - test-server/php-v2-server/src/client.php | 74 ---- test-server/php-v2-server/src/errors.php | 42 -- test-server/php-v2-server/src/get_object.php | 85 ---- test-server/php-v2-server/src/index.php | 295 -------------- test-server/php-v2-server/src/put_object.php | 79 ---- 44 files changed, 2 insertions(+), 2408 deletions(-) delete mode 100644 test-server/go-v3-server/Makefile delete mode 100644 test-server/go-v3-server/README.md delete mode 100644 test-server/go-v3-server/go.mod delete mode 100644 test-server/go-v3-server/go.sum delete mode 100644 test-server/go-v3-server/main.go delete mode 100644 test-server/java-v3-server/.duvet/.gitignore delete mode 100644 test-server/java-v3-server/.duvet/config.toml delete mode 100644 test-server/java-v3-server/.gitignore delete mode 100644 test-server/java-v3-server/Makefile delete mode 100644 test-server/java-v3-server/README.md delete mode 100644 test-server/java-v3-server/build.gradle.kts delete mode 100644 test-server/java-v3-server/gradle.properties delete mode 100644 test-server/java-v3-server/gradle/wrapper/gradle-wrapper.jar delete mode 100644 test-server/java-v3-server/gradle/wrapper/gradle-wrapper.properties delete mode 100755 test-server/java-v3-server/gradlew delete mode 100644 test-server/java-v3-server/gradlew.bat delete mode 100644 test-server/java-v3-server/license.txt delete mode 100644 test-server/java-v3-server/settings.gradle.kts delete mode 100644 test-server/java-v3-server/smithy-build.json delete mode 120000 test-server/java-v3-server/specification delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java delete mode 100644 test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java delete mode 100644 test-server/php-v2-server/.duvet/.gitignore delete mode 100644 test-server/php-v2-server/.duvet/config.toml delete mode 100644 test-server/php-v2-server/.gitignore delete mode 100644 test-server/php-v2-server/Makefile delete mode 100644 test-server/php-v2-server/README.md delete mode 100644 test-server/php-v2-server/composer.json delete mode 160000 test-server/php-v2-server/local-php-sdk delete mode 100644 test-server/php-v2-server/src/client.php delete mode 100644 test-server/php-v2-server/src/errors.php delete mode 100644 test-server/php-v2-server/src/get_object.php delete mode 100644 test-server/php-v2-server/src/index.php delete mode 100644 test-server/php-v2-server/src/put_object.php diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index b442be5a..1ef01de4 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -64,9 +64,8 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - test-server/java-v3-server/.gradle test-server/java-tests/.gradle - key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-v3-server/**/*.gradle*', 'test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*', 'test-server/java-v3-server/**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*') }} restore-keys: | ${{ runner.os }}-gradle- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b934bc20..e5b5b8a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,11 +71,6 @@ jobs: with: php-version: "8.1" - - name: Install PHP V2 dependencies - working-directory: ./test-server/php-v2-server - shell: bash - run: composer install - - name: Install PHP V2 Transition dependencies working-directory: ./test-server/php-v2-transition-server shell: bash @@ -132,9 +127,8 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - test-server/java-v3-server/.gradle test-server/java-tests/.gradle - key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-v3-server/**/*.gradle*', 'test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*', 'test-server/java-v3-server/**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*') }} restore-keys: | ${{ runner.os }}-gradle- diff --git a/.gitmodules b/.gitmodules index cc14cae1..6f0577f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,10 +6,6 @@ path = test-server/ruby-v3-server/local-ruby-sdk url = git@github.com:aws/aws-sdk-ruby.git branch = version-3 -[submodule "test-server/php-v2-server/local-php-sdk"] - path = test-server/php-v2-server/local-php-sdk - url = git@github.com:aws/aws-sdk-php.git - branch = master [submodule "test-server/php-v3-server/local-php-sdk"] path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/aws-sdk-php.git diff --git a/test-server/README.md b/test-server/README.md index 818e8ded..48187fc3 100644 --- a/test-server/README.md +++ b/test-server/README.md @@ -31,12 +31,6 @@ make start-servers # Start only the Python S3EC V3 server make start-python-v3-server -# Start only the Java S3EC V3 server -make start-java-v3-server - -# Start only the Go S3EC V3 server -make start-go-v3-server - # Run Java tests make run-tests @@ -83,7 +77,6 @@ You can adjust the source pattern or comment style as needed. Examples: - `ruby-v2-server/.duvet/config.toml` -- `php-v2-server/.duvet/config.toml` There are Makefile targets, but you can just run `make duvet` or `duvet report` inside a server directory to run the report. diff --git a/test-server/go-v3-server/Makefile b/test-server/go-v3-server/Makefile deleted file mode 100644 index 80928dbd..00000000 --- a/test-server/go-v3-server/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Makefile for S3 Encryption Client Testing - -.PHONY: build-server start-server stop-server wait-for-server - -PID_FILE := server.pid -PORT := 8082 - -build-server: - @echo "Building Go V3 server..." - go mod tidy - -start-server: - @echo "Starting Go V3 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" \ - go run . > server.log 2>&1 & echo $$! > $(PID_FILE) - @echo "Go V3 server starting..." - -stop-server: - @echo "Stopping server on port $(PORT)..." - @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE) ]; then \ - pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm -f $(PID_FILE); \ - fi - @rm -f server.log - @echo "Server stopped" - -wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT) - -duvet: - duvet report - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/go-v3-server/README.md b/test-server/go-v3-server/README.md deleted file mode 100644 index cf1692b6..00000000 --- a/test-server/go-v3-server/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# S3EC Go V3 Test Server - -This is the Go implementation of the S3ECTestServer framework for S3EC Go V3. It provides a server implementation for testing Go S3 Encryption Client V3 functionality. - -## Overview - -The S3EC Go test server implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: - -- Creating S3 Encryption Clients -- Putting objects with encryption -- Getting and decrypting objects - -## Usage - -To run the server: - -```console -go run . -``` - -This will start the server running on port `8082`. - -The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/go-v3-server/go.mod b/test-server/go-v3-server/go.mod deleted file mode 100644 index 014a64da..00000000 --- a/test-server/go-v3-server/go.mod +++ /dev/null @@ -1,31 +0,0 @@ -module github.com/aws/amazon-s3-encryption-client-python/test-server/go-server - -go 1.21 - -require ( - github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 - github.com/aws/aws-sdk-go-v2 v1.24.0 - github.com/aws/aws-sdk-go-v2/config v1.26.1 - github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 - github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 - github.com/google/uuid v1.5.0 - github.com/gorilla/mux v1.8.1 -) - -require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect - github.com/aws/smithy-go v1.19.0 // indirect -) diff --git a/test-server/go-v3-server/go.sum b/test-server/go-v3-server/go.sum deleted file mode 100644 index 4fc073e0..00000000 --- a/test-server/go-v3-server/go.sum +++ /dev/null @@ -1,46 +0,0 @@ -github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 h1:P4dOTmTkEb8Dj/LuAoA4bqRZZrDq4DqZQI88vdMaj18= -github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0/go.mod h1:olnwkBTbWjaJCaGOHohvJu98q40GiJZuDHLXj751mII= -github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= -github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= -github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/test-server/go-v3-server/main.go b/test-server/go-v3-server/main.go deleted file mode 100644 index 0384c5ff..00000000 --- a/test-server/go-v3-server/main.go +++ /dev/null @@ -1,370 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "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" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/google/uuid" - "github.com/gorilla/mux" -) - -// Server represents the Go test server -type Server struct { - clientCache map[string]*client.S3EncryptionClientV3 - kmsClient *kms.Client - mu sync.RWMutex -} - -// CreateClientInput represents the input for creating a client -type CreateClientInput struct { - Config S3ECConfig `json:"config"` -} - -// CreateClientOutput represents the output for creating a client -type CreateClientOutput struct { - ClientID string `json:"clientId"` -} - -// S3ECConfig represents the S3 encryption client configuration -type S3ECConfig struct { - EnableLegacyUnauthenticatedModes bool `json:"enableLegacyUnauthenticatedModes"` - EnableDelayedAuthenticationMode bool `json:"enableDelayedAuthenticationMode"` - EnableLegacyWrappingAlgorithms bool `json:"enableLegacyWrappingAlgorithms"` - SetBufferSize int64 `json:"setBufferSize"` - KeyMaterial KeyMaterial `json:"keyMaterial"` -} - -// KeyMaterial represents the key material for encryption -type KeyMaterial struct { - RSAKey []byte `json:"rsaKey"` - AESKey []byte `json:"aesKey"` - KMSKeyID string `json:"kmsKeyId"` -} - -// PutObjectOutput represents the output for put object operation -type PutObjectOutput struct { - Bucket string `json:"bucket"` - Key string `json:"key"` - Metadata []string `json:"metadata"` -} - -// ErrorResponse represents an error response -type ErrorResponse struct { - Type string `json:"__type"` - Message string `json:"message"` -} - -// NewServer creates a new server instance -func NewServer() (*Server, error) { - 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) - } - - return &Server{ - clientCache: make(map[string]*client.S3EncryptionClientV3), - kmsClient: kms.NewFromConfig(cfg), - }, nil -} - -// createGenericServerError creates a generic server error response -func (s *Server) createGenericServerError(w http.ResponseWriter, message string, statusCode int) { - // Echo error to console - log.Printf("[Go V3] GenericServerError: %s (Status: %d)", message, statusCode) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(ErrorResponse{ - Type: "software.amazon.encryption.s3#GenericServerError", - Message: message, - }) -} - -// createS3EncryptionClientError creates an S3 encryption client error response -func (s *Server) createS3EncryptionClientError(w http.ResponseWriter, message string, statusCode int) { - // Echo error to console - log.Printf("[Go V3] S3EncryptionClientError: %s (Status: %d)", message, statusCode) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(ErrorResponse{ - Type: "software.amazon.encryption.s3#S3EncryptionClientError", - Message: message, - }) -} - -// metadataStringToMap converts metadata string to map -func metadataStringToMap(mdString string) (map[string]string, error) { - md := make(map[string]string) - if mdString == "" { - return md, nil - } - - mdList := strings.Split(mdString, ",") - for _, entry := range mdList { - // Split on "]:[" to separate key and value - parts := strings.Split(entry, "]:[") - if len(parts) == 2 { - // Remove remaining brackets from start and end - key := parts[0][1:] // Remove first character - value := parts[1][:len(parts[1])-1] // Remove last character - md[key] = value - } else { - return nil, fmt.Errorf("malformed metadata list entry: %s", entry) - } - } - return md, nil -} - -// createClient handles POST /client -func (s *Server) createClient(w http.ResponseWriter, r *http.Request) { - // Read body - body, err := io.ReadAll(r.Body) - if err != nil { - s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) - return - } - - var input CreateClientInput - if err := json.Unmarshal(body, &input); err != nil { - s.createGenericServerError(w, "Invalid JSON in request body", http.StatusBadRequest) - return - } - - 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 - } - - // Create KMS keyring - kmsClient := kms.NewFromConfig(cfg) - keyring := materials.NewKmsKeyring(kmsClient, input.Config.KeyMaterial.KMSKeyID, func(options *materials.KeyringOptions) { - options.EnableLegacyWrappingAlgorithms = input.Config.EnableLegacyWrappingAlgorithms - }) - cmm, err := materials.NewCryptographicMaterialsManager(keyring) - - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create CMM: %v", err), http.StatusInternalServerError) - return - } - - // Create S3 encryption client - var s3EncryptionClient *client.S3EncryptionClientV3 - s3PlaintextClient := s3.NewFromConfig(cfg) - s3EncryptionClient, err = client.New(s3PlaintextClient, cmm) - - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to create S3EC: %v", err), http.StatusInternalServerError) - return - } - - // Generate client ID - clientID := uuid.New().String() - - // 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") - json.NewEncoder(w).Encode(CreateClientOutput{ - ClientID: clientID, - }) -} - -// putObject handles PUT /object/{bucket}/{key} -func (s *Server) putObject(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - bucket := vars["bucket"] - key := vars["key"] - - clientID := r.Header.Get("ClientID") - if clientID == "" { - s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) - return - } - - // 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) - return - } - - // Read body - body, err := io.ReadAll(r.Body) - if err != nil { - s.createGenericServerError(w, "Failed to read request body", http.StatusBadRequest) - return - } - - // Get metadata from header - metadataHeader := r.Header.Get("Content-Metadata") - encCtx, err := metadataStringToMap(metadataHeader) - - // Create context with encryption context - ctx := context.Background() - encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) - return - } - - // Create put object input - putInput := &s3.PutObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(key), - Body: strings.NewReader(string(body)), - } - - // Add metadata if present - if len(encCtx) > 0 { - putInput.Metadata = encCtx - } - - // Make the put object request using the encryption client - _, err = client.PutObject(encryptionContext, putInput) - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to put object: %v", err), http.StatusInternalServerError) - return - } - - log.Printf("[Go V3] PutObject SUCCESS: Bucket=%s, Key=%s", bucket, key) - - // Return response - w.Header().Set("Content-Type", "application/json") - response := PutObjectOutput{ - Bucket: bucket, - Key: key, - Metadata: []string{}, // Return empty metadata list as per the model - } - json.NewEncoder(w).Encode(response) -} - -// getObject handles GET /object/{bucket}/{key} -func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - bucket := vars["bucket"] - key := vars["key"] - - clientID := r.Header.Get("ClientID") - if clientID == "" { - s.createGenericServerError(w, "ClientID header is required", http.StatusBadRequest) - return - } - - // 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) - return - } - - // Get metadata from header - metadataHeader := r.Header.Get("Content-Metadata") - encCtx, err := metadataStringToMap(metadataHeader) - - // Create context with encryption context - // Note: S3EC Go V3 does not validate encryption context on decrypt, so the value provided here - // will not be validated against the encryption context stored on the object. - ctx := context.Background() - encryptionContext := context.WithValue(ctx, "EncryptionContext", encCtx) - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to parse metadata: %v", err), http.StatusBadRequest) - return - } - - // Create get object input - getInput := &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(key), - } - - // Make the get object request using the encryption client - result, err := client.GetObject(encryptionContext, getInput) - if err != nil { - errMsg := err.Error() - // Shim the S3EC error message to the error message expected by the test server. - // We don't want to change the S3EC error message but the test server expects a specific error message; - // This is the appropriate place to rewrite the error message. - if strings.Contains(errMsg, "to decrypt x-amz-cek-alg value `kms` you must enable legacyWrappingAlgorithms on the keyring") { - s.createS3EncryptionClientError(w, "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms", http.StatusInternalServerError) - return - } - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to get object: %v", err), http.StatusInternalServerError) - return - } - defer result.Body.Close() - - // Read the body - body, err := io.ReadAll(result.Body) - if err != nil { - s.createS3EncryptionClientError(w, fmt.Sprintf("Failed to read object body: %v", err), http.StatusInternalServerError) - return - } - - // Convert metadata to string format - var metadataList []string - if result.Metadata != nil { - for k, v := range result.Metadata { - metadataList = append(metadataList, fmt.Sprintf("%s=%s", k, v)) - } - } - - metadataStr := strings.Join(metadataList, ",") - - log.Printf("[Go V3] GetObject SUCCESS: Bucket=%s, Key=%s", bucket, key) - - // Set response headers - w.Header().Set("Content-Metadata", metadataStr) - - // Return the body as response - w.Write(body) -} - -func main() { - server, err := NewServer() - if err != nil { - log.Fatalf("[Go V3] Failed to create Go V3 server: %v", err) - } - - r := mux.NewRouter() - - // Register routes - r.HandleFunc("/client", server.createClient).Methods("POST") - r.HandleFunc("/object/{bucket}/{key}", server.putObject).Methods("PUT") - r.HandleFunc("/object/{bucket}/{key}", server.getObject).Methods("GET") - - fmt.Println("[Go V3] Starting Go V3 server on :8082...") - log.Fatal(http.ListenAndServe(":8082", r)) -} 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 4821aec1..e6cfae84 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 @@ -549,10 +549,6 @@ public void instructionFileWriteAndRead(LanguageServerTarget encLang, LanguageSe if (KMS_INSTRUCTION_FILE_UNSUPPORTED.contains(decLang.getLanguageName())) { throw new TestAbortedException("not testing " + encLang.getLanguageName()); } - // We skip PHP-V2-Current because it writes an instruction file that other languages may not read. - if (encLang.getLanguageName().equals("PHP-V2-Current")) { - throw new TestAbortedException("not testing " + encLang.getLanguageName()); - } S3ECTestServerClient encClient = testServerClientFor(encLang); S3ECTestServerClient decClient = testServerClientFor(decLang); final String objectKey = appendTestSuffix(String.format("write-%s-read-%s-instruction-file", encLang.getLanguageName(), decLang.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 510d57b6..4084c2a1 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 @@ -59,14 +59,12 @@ public class TestUtils { // vN-Transition: Proposed feature release version. Supports reading messages encrypted with key commitment. // vN+1: Proposed breaking release version. Supports reading/writing messages encrypted with key commitment. -// public static final String JAVA_V3_CURRENT = "Java-V3-Current"; public static final String JAVA_V3_TRANSITION = "Java-V3-Transition"; public static final String JAVA_V4 = "Java-V4"; // No Python S3EC versions are released. Only test V3 as the "vN+1" version. public static final String PYTHON_V3 = "Python-V3"; -// public static final String GO_V3_CURRENT = "Go-V3-Current"; public static final String GO_V3_TRANSITION = "Go-V3-Transition"; public static final String GO_V4 = "Go-V4"; @@ -83,7 +81,6 @@ public class TestUtils { public static final String RUBY_V2_TRANSITION = "Ruby-V2-Transition"; public static final String RUBY_V3 = "Ruby-V3"; -// public static final String PHP_V2_CURRENT = "PHP-V2-Current"; public static final String PHP_V2_TRANSITION = "PHP-V2-Transition"; public static final String PHP_V3 = "PHP-V3"; diff --git a/test-server/java-v3-server/.duvet/.gitignore b/test-server/java-v3-server/.duvet/.gitignore deleted file mode 100644 index 93956e36..00000000 --- a/test-server/java-v3-server/.duvet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -reports/ -requirements/ -specification/ \ No newline at end of file diff --git a/test-server/java-v3-server/.duvet/config.toml b/test-server/java-v3-server/.duvet/config.toml deleted file mode 100644 index 988fb5fa..00000000 --- a/test-server/java-v3-server/.duvet/config.toml +++ /dev/null @@ -1,21 +0,0 @@ -'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" - -[[source]] -pattern = "s3ec-staging/**/*.java" - -# Include required specifications here -[[specification]] -source = "specification/s3-encryption/data-format/content-metadata.md" -[[specification]] -source = "specification/s3-encryption/data-format/metadata-strategy.md" -[[specification]] -source = "specification/s3-encryption/encryption.md" -[[specification]] -source = "specification/s3-encryption/key-derivation.md" - -[report.html] -enabled = true - -# Enable snapshots to prevent requirement coverage regressions -[report.snapshot] -enabled = false diff --git a/test-server/java-v3-server/.gitignore b/test-server/java-v3-server/.gitignore deleted file mode 100644 index e660fd93..00000000 --- a/test-server/java-v3-server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/test-server/java-v3-server/Makefile b/test-server/java-v3-server/Makefile deleted file mode 100644 index 59dcdff5..00000000 --- a/test-server/java-v3-server/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Makefile for S3 Encryption Client Testing - -.PHONY: build-server start-server stop-server wait-for-server - -PID_FILE := server.pid -PORT := 8080 - -build-server: - @echo "Building Java V3 server..." - ./gradlew --build-cache --parallel --no-daemon build - -start-server: - @echo "Starting Java V3 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" \ - ./gradlew --build-cache --parallel run > server.log 2>&1 & echo $$! > $(PID_FILE) - @echo "Java V3 server starting..." - -stop-server: - @echo "Stopping server on port $(PORT)..." - @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE) ]; then \ - pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm -f $(PID_FILE); \ - fi - @rm -f server.log - @echo "Server stopped" - -wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT) - -duvet: - duvet report - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/java-v3-server/README.md b/test-server/java-v3-server/README.md deleted file mode 100644 index e00eb496..00000000 --- a/test-server/java-v3-server/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# S3EC Java V3 Test Server - -This is the Java implementation of the S3ECTestServer framework for S3EC Java V3. It provides a server implementation for testing Java S3 Encryption Client V3 functionality. - -## Overview - -The S3ECJavaTestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: - -- Creating S3 Encryption Clients -- Putting objects with encryption -- Getting and decrypting objects - -## Usage - -To run the server: - -```console -gradle run -``` - -This will start the server running on port `8080`. - -The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v3-server/build.gradle.kts b/test-server/java-v3-server/build.gradle.kts deleted file mode 100644 index baae4947..00000000 --- a/test-server/java-v3-server/build.gradle.kts +++ /dev/null @@ -1,56 +0,0 @@ -plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") - application -} - -dependencies { - val smithyJavaVersion: String by project - - smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion") - - implementation("software.amazon.smithy:smithy-rules-engine:1.59.0") - implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") - implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") - - compileOnly("software.amazon.awssdk:aws-sdk-java:2.31.66") - // This MUST stay at 3.5.0 - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.5.0") -} - -// Use that application plugin to start the service via the `run` task. -application { - mainClass = "software.amazon.encryption.s3.S3ECJavaTestServer" -} - -// Add generated Java files to the main sourceSet -afterEvaluate { - val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen") - sourceSets { - main { - java { - srcDir(serverPath) - } - } - } -} - -tasks { - compileJava { - dependsOn(smithyBuild) - } -} - -// Helps Intellij IDE's discover smithy models -sourceSets { - main { - java { - srcDir("../model") - } - } -} - -repositories { - mavenLocal() - mavenCentral() -} diff --git a/test-server/java-v3-server/gradle.properties b/test-server/java-v3-server/gradle.properties deleted file mode 100644 index 483cd315..00000000 --- a/test-server/java-v3-server/gradle.properties +++ /dev/null @@ -1,24 +0,0 @@ -# Smithy versions -smithyJavaVersion=[0,1] -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 - -# Configure on demand to reduce startup time -org.gradle.configureondemand=true diff --git a/test-server/java-v3-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v3-server/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e6441136f3d4ba8a0da8d277868979cfbc8ad796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/test-server/java-v3-server/gradlew.bat b/test-server/java-v3-server/gradlew.bat deleted file mode 100644 index 7101f8e4..00000000 --- a/test-server/java-v3-server/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/test-server/java-v3-server/license.txt b/test-server/java-v3-server/license.txt deleted file mode 100644 index 2dd564b3..00000000 --- a/test-server/java-v3-server/license.txt +++ /dev/null @@ -1,4 +0,0 @@ -/* - * Example file license header. - * File header line two - */ \ No newline at end of file diff --git a/test-server/java-v3-server/settings.gradle.kts b/test-server/java-v3-server/settings.gradle.kts deleted file mode 100644 index e7c41714..00000000 --- a/test-server/java-v3-server/settings.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Basic usage of generated server stubs. - */ - -pluginManagement { - val smithyGradleVersion: String by settings - - plugins { - id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) - } - - repositories { - mavenLocal() - mavenCentral() - gradlePluginPortal() - } -} - -rootProject.name = "S3ECJavaTestServer" diff --git a/test-server/java-v3-server/smithy-build.json b/test-server/java-v3-server/smithy-build.json deleted file mode 100644 index a0fcb8e5..00000000 --- a/test-server/java-v3-server/smithy-build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0", - "plugins": { - "java-server-codegen": { - "service": "software.amazon.encryption.s3#S3ECTestServer", - "namespace": "software.amazon.encryption.s3", - "headerFile": "license.txt" - } - }, - "sources": ["../model"] -} diff --git a/test-server/java-v3-server/specification b/test-server/java-v3-server/specification deleted file mode 120000 index b173f708..00000000 --- a/test-server/java-v3-server/specification +++ /dev/null @@ -1 +0,0 @@ -../specification \ No newline at end of file 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 deleted file mode 100644 index 6a3da066..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java +++ /dev/null @@ -1,155 +0,0 @@ -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; -import software.amazon.encryption.s3.internal.InstructionFileConfig; -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.PartialRsaKeyPair; -import software.amazon.encryption.s3.materials.RsaKeyring; -import software.amazon.smithy.java.core.schema.Schema; -import software.amazon.smithy.java.server.RequestContext; -import software.amazon.encryption.s3.model.CreateClientInput; -import software.amazon.encryption.s3.model.CreateClientOutput; -import software.amazon.encryption.s3.model.GenericServerError; -import software.amazon.encryption.s3.model.KeyMaterial; -import software.amazon.encryption.s3.service.CreateClientOperation; - -import javax.crypto.spec.SecretKeySpec; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.interfaces.RSAPrivateCrtKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.RSAPublicKeySpec; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -public class CreateClientOperationImpl implements CreateClientOperation { - private Map clientCache_; - private Map keyringCache_; - - public CreateClientOperationImpl(Map clientCache, Map keyringCache) { - clientCache_ = clientCache; - keyringCache_ = keyringCache; - } - - // Copied from S3EC. - private boolean onlyOneNonNull(Object... values) { - boolean haveOneNonNull = false; - for (Object o : values) { - if (o != null) { - if (haveOneNonNull) { - return false; - } - - haveOneNonNull = true; - } - } - - return haveOneNonNull; - } - - @Override - public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { - try { - // Key Material / Keyring Creation - KeyMaterial key = input.getConfig().getKeyMaterial(); - if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { - throw new RuntimeException("KeyMaterial must be only one, non-null input!"); - } - Keyring keyring; - if (key.getAesKey() != null) { - byte[] keyBytes = new byte[key.getAesKey().remaining()]; - key.getAesKey().get(keyBytes); - keyring = AesKeyring.builder() - .wrappingKey(new SecretKeySpec(keyBytes, "AES")) - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .build(); - } else if (key.getRsaKey() != null) { - try { - byte[] keyBytes = new byte[key.getRsaKey().remaining()]; - key.getRsaKey().get(keyBytes); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec); - RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( - privateKey.getModulus(), - privateKey.getPublicExponent() - ); - - // Generate public key - PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - - keyring = RsaKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyPair(PartialRsaKeyPair.builder() - .publicKey(publicKey) - .privateKey(privateKey).build()) - .build(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { - throw GenericServerError.builder() - .message(nse.getMessage()) - .build(); - } - } else if (key.getKmsKeyId() != null) { - keyring = KmsKeyring.builder() - .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) - .wrappingKeyId(key.getKmsKeyId()) - .build(); - } else { - 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) - .build()) - .keyring(keyring) - .build(); - UUID uuid = UUID.randomUUID(); - String uuidString = uuid.toString(); - clientCache_.put(uuidString, s3Client); - keyringCache_.put(uuidString, keyring); - return CreateClientOutput.builder() - .clientId(uuidString) - .build(); - } catch (Exception 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-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 deleted file mode 100644 index fbccd458..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java +++ /dev/null @@ -1,78 +0,0 @@ -package software.amazon.encryption.s3; - -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.encryption.s3.S3EncryptionClientException; -import software.amazon.smithy.java.server.RequestContext; -import software.amazon.encryption.s3.model.GenericServerError; -import software.amazon.encryption.s3.model.GetObjectInput; -import software.amazon.encryption.s3.model.GetObjectOutput; -import software.amazon.encryption.s3.model.S3EncryptionClientError; -import software.amazon.encryption.s3.service.GetObjectOperation; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; -import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; -import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; - -public class GetObjectOperationImpl implements GetObjectOperation { - private Map clientCache_; - public GetObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - @Override - public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { - try { - 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)); - - // 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 - ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); - GetObjectOutput output = GetObjectOutput.builder() - .body(bb) - .metadata(mdAsList) - .build(); - return output; - } 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-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java deleted file mode 100644 index 036289ec..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -package software.amazon.encryption.s3; - -import software.amazon.encryption.s3.model.GenericServerError; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class MetadataUtils { - - /** - * Annoyingly, Smithy doesn't provide an interface for map types - * in HTTP headers, so we have to do the serde ourselves - */ - public static List metadataMapToList(Map md) { - List mdAsList = new ArrayList<>(md.size()); - for (Map.Entry keyValue : md.entrySet()) { - mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); - } - return mdAsList; - } - - public static Map metadataListToMap(List mdList) { - Map md = new HashMap<>(); - for (String entry : mdList) { - // Split on "]:[" to separate key and value - String[] parts = entry.split("]:\\["); - if (parts.length == 2) { - // Remove remaining brackets from start and end - String key = parts[0].substring(1); - String value = parts[1].substring(0, parts[1].length() - 1); - md.put(key, value); - } else { - throw GenericServerError.builder() - .message("Malformed metadata list entry: " + entry) - .build(); - } - } - return md; - } - -} diff --git a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java deleted file mode 100644 index 4c772673..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -package software.amazon.encryption.s3; - -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.smithy.java.server.RequestContext; -import software.amazon.encryption.s3.model.GenericServerError; -import software.amazon.encryption.s3.model.PutObjectInput; -import software.amazon.encryption.s3.model.PutObjectOutput; -import software.amazon.encryption.s3.service.PutObjectOperation; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Map; -import java.util.stream.Collectors; - -import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; -import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; - -public class PutObjectOperationImpl implements PutObjectOperation { - - private Map clientCache_; - - public PutObjectOperationImpl(Map clientCache) { - clientCache_ = clientCache; - } - - @Override - public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { - try { - final Map metadata = metadataListToMap(input.getMetadata()); - S3Client s3Client = clientCache_.get(input.getClientID()); - s3Client.putObject(builder -> builder - .bucket(input.getBucket()) - .key(input.getKey()) - .overrideConfiguration(withAdditionalConfiguration(metadata)), - RequestBody.fromByteBuffer(input.getBody()) - ); - // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway - return PutObjectOutput.builder() - .bucket(input.getBucket()) - .key(input.getKey()) - .metadata(input.getMetadata()) - .build(); - } catch (Exception 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-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 deleted file mode 100644 index dd376429..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/ReEncryptOperationImpl.java +++ /dev/null @@ -1,185 +0,0 @@ -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 deleted file mode 100644 index be53f20c..00000000 --- a/test-server/java-v3-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.encryption.s3; - -import software.amazon.awssdk.services.s3.S3Client; - -import java.net.URI; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import software.amazon.smithy.java.server.Server; -import software.amazon.encryption.s3.service.S3ECTestServer; - -public class S3ECJavaTestServer implements Runnable { - static final URI endpoint = URI.create("http://localhost:8080"); - - public static void main(String[] args) { - new S3ECJavaTestServer().run(); - } - - @Override - public void run() { - // All the S3EC instances live here. - // 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, keyringCache)) - .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) - .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) - .addReEncryptOperation(new ReEncryptOperationImpl(clientCache, keyringCache)) - .build()) - .build(); - System.out.println("Starting server..."); - server.start(); - try { - Thread.currentThread().join(); - } catch (InterruptedException e) { - System.out.println("Stopping server..."); - try { - server.shutdown().get(); - } catch (InterruptedException | ExecutionException ex) { - throw new RuntimeException(ex); - } - } - } -} diff --git a/test-server/php-v2-server/.duvet/.gitignore b/test-server/php-v2-server/.duvet/.gitignore deleted file mode 100644 index 93956e36..00000000 --- a/test-server/php-v2-server/.duvet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -reports/ -requirements/ -specification/ \ No newline at end of file diff --git a/test-server/php-v2-server/.duvet/config.toml b/test-server/php-v2-server/.duvet/config.toml deleted file mode 100644 index 64b00927..00000000 --- a/test-server/php-v2-server/.duvet/config.toml +++ /dev/null @@ -1,24 +0,0 @@ -'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" - -[[source]] -pattern = "local-php-sdk/src/S3/**/*.php" - -[[source]] -pattern = "local-php-sdk/src/Crypto/**/*.php" - -# Include required specifications here -[[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" -[[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" -[[specification]] -source = "../specification/s3-encryption/encryption.md" -[[specification]] -source = "../specification/s3-encryption/key-derivation.md" - -[report.html] -enabled = true - -# Enable snapshots to prevent requirement coverage regressions -[report.snapshot] -enabled = false diff --git a/test-server/php-v2-server/.gitignore b/test-server/php-v2-server/.gitignore deleted file mode 100644 index 07108589..00000000 --- a/test-server/php-v2-server/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -vendor/* -cookies.txt -server.pid -composer.lock \ No newline at end of file diff --git a/test-server/php-v2-server/Makefile b/test-server/php-v2-server/Makefile deleted file mode 100644 index 719ea238..00000000 --- a/test-server/php-v2-server/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Makefile for S3 Encryption Client Testing - -.PHONY: build-server start-server stop-server wait-for-server - -PID_FILE := server.pid -PORT := 8087 - -build-server: - @echo "Building PHP V2 server..." - composer install - -start-server: - @echo "Starting PHP V2 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" \ - composer run start --timeout=0 > server.log 2>&1 & echo $$! > $(PID_FILE) - @echo "PHP V2 server starting..." - -stop-server: - @echo "Stopping server on port $(PORT)..." - @lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE) ]; then \ - pkill -P $$(cat $(PID_FILE)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE)) 2>/dev/null || true; \ - rm -f $(PID_FILE); \ - fi - @rm -f server.log - @echo "Server stopped" - -wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT) - -duvet: - duvet report - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/php-v2-server/README.md b/test-server/php-v2-server/README.md deleted file mode 100644 index c4ba49fe..00000000 --- a/test-server/php-v2-server/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# S3EC PHP v2 Test Server - -This is the PHP V2 implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality. - -## Overview - -The S3ECPhpV2TestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: - -- Creating S3 Encryption Clients with session-based caching -- Putting objects with encryption -- Getting and decrypting objects - -## Starting the Server - -### Method 1: Using Composer (Recommended) -```bash -composer run start -``` - -The server will start on port `8087`. - -## Available Endpoints - -### Server Status -- **GET /** - Returns server status and available endpoints - -### Client Management -- **POST /client** - Creates an S3EncryptionClient and caches it with session persistence -- **GET /cache** - Shows current session state and cached clients (for debugging) - -### Object Operations -- **GET /object/{bucket}/{key}** - Handle GET requests using the S3EncryptionClient -- **PUT /object/{bucket}/{key}** - Handle PUT requests using the S3EncryptionClient - -## Testing with curl - -### Important: Session Cookie Management - -To properly test the server and maintain session persistence, you **must** use cookies with curl: - -#### First Request (creates session cookie): -```bash -curl -X POST http://localhost:8087/client \ - -H "Content-Type: application/json" \ - -c cookies.txt \ - -v -``` - -#### Subsequent Requests (reuses session cookie): -```bash -curl -X POST http://localhost:8087/client \ - -H "Content-Type: application/json" \ - -b cookies.txt \ - -c cookies.txt \ - -v -``` - -#### Check Cache Status: -```bash -curl http://localhost:8087/cache \ - -b cookies.txt -``` - -#### Helpful Notes -- **Session Storage**: Client configurations are stored in `$_SESSION['s3ecCache']` -- **Object Recreation**: AWS SDK objects are recreated from stored configuration (they cannot be serialized) -AWS SDK obbjects cannot be serialized due to internal resources and closures. -- **Helper Function**: `getCachedClient($clientId)` retrieves and recreates clients from cache -- **Debugging**: Enhanced logging and `/cache` endpoint for troubleshooting diff --git a/test-server/php-v2-server/composer.json b/test-server/php-v2-server/composer.json deleted file mode 100644 index d5177951..00000000 --- a/test-server/php-v2-server/composer.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "aws/s3ec-php-v2-test-server", - "description": "PHP v2 implementation of the S3EC Test Server framework", - "type": "project", - "license": "Apache-2.0", - "repositories": [ - { - "type": "path", - "url": "./local-php-sdk", - "options": { - "symlink": true - } - } - ], - "require": { - "php": ">=7.4", - "aws/aws-sdk-php": "@dev", - "ramsey/uuid": "^4.9" - }, - "autoload": { - "psr-4": { - "S3EC\\PhpV2Server\\": "src/" - } - }, - "scripts": { - "start": [ - "php -S 0.0.0.0:8087 src/index.php" - ] - }, - "config": { - "optimize-autoloader": true, - "platform": { - "php": "8.1" - } - } -} \ No newline at end of file diff --git a/test-server/php-v2-server/local-php-sdk b/test-server/php-v2-server/local-php-sdk deleted file mode 160000 index f53d8fc6..00000000 --- a/test-server/php-v2-server/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f53d8fc6cdbc1e64e7d14e72d1e315d05003b2b4 diff --git a/test-server/php-v2-server/src/client.php b/test-server/php-v2-server/src/client.php deleted file mode 100644 index 9c39d540..00000000 --- a/test-server/php-v2-server/src/client.php +++ /dev/null @@ -1,74 +0,0 @@ -toString(); - $kmsKeyId = $keyMaterial["kmsKeyId"] ?? null; - $instFileConfig = $configData['instructionFileConfig'] ?? null; - $instFilePut = false; - if ($instFileConfig != null) { - $instFilePut = $instFileConfig['enableInstructionFilePutObject'] ?? false; - } - - if ($configData == []) { - return GenericServerError("Invalid config in request body", 400); - } - if (($keyMaterial || $kmsKeyId) === null) { - return GenericServerError("Invalid keyMaterial in config", 400); - } - - // Store client configuration instead of objects (AWS objects can't be serialized) - $_SESSION['s3ecCache'][$clientId] = [ - 's3Config' => [ - 'region' => 'us-west-2', - 'version' => 'latest', - 'http' => [ - 'debug' => false, - 'verify' => true, - 'curl' => [ - CURLOPT_VERBOSE => false, - CURLOPT_NOPROGRESS => true - ] - ] - ], - 'kmsConfig' => [ - 'region' => 'us-west-2', - 'version' => 'latest', - 'http' => [ - 'debug' => false, - 'verify' => true, - 'curl' => [ - CURLOPT_VERBOSE => false, - CURLOPT_NOPROGRESS => true - ] - ] - ], - 'kmsKeyId' => $kmsKeyId, - 'legacy' => $legacyAlgorithms, - 'instFilePut' => $instFilePut, - 'created' => time() - ]; - - // Auto-update cookies.txt with current session ID so tests can access cached clients - writeSessionIdToCookiesFile(session_id()); - - header("Content-Type: application/json"); - return json_encode([ - 'clientId' => $clientId, - ]); -} diff --git a/test-server/php-v2-server/src/errors.php b/test-server/php-v2-server/src/errors.php deleted file mode 100644 index 2b59861d..00000000 --- a/test-server/php-v2-server/src/errors.php +++ /dev/null @@ -1,42 +0,0 @@ - 'GenericServerError', - 'message' => $message - ]; - - return json_encode($errorResponse); -} - -/** - * Used for modeled errors, e.g. errors thrown by the S3EC - * Tests SHOULD expect this error in negative tests. - * - * @param string $message The error message to include in the response - * @return string JSON-encoded error response - */ -function S3EncryptionClientError($message) -{ - http_response_code(500); - header('Content-Type: application/json'); - - $errorResponse = [ - "__type" => "software.amazon.encryption.s3#S3EncryptionClientError", - 'message' => $message - ]; - - return json_encode($errorResponse); -} diff --git a/test-server/php-v2-server/src/get_object.php b/test-server/php-v2-server/src/get_object.php deleted file mode 100644 index 61bacb5b..00000000 --- a/test-server/php-v2-server/src/get_object.php +++ /dev/null @@ -1,85 +0,0 @@ -getObject([ - '@SecurityProfile' => $legacy, - '@MaterialsProvider' => $materialProvider, - '@KmsEncryptionContext' => $encryptionContext, - 'Bucket' => $bucket, - 'Key' => $key, - ]); - - // Capture and discard any unwanted output from AWS SDK - $unwantedOutput = ob_get_clean(); - if (!empty($unwantedOutput)) { - error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes"); - } - - $body = $result['Body']->getContents(); - $formattedMetadata = formatMetadataForResponse($result["Metadata"]); - - // Now set headers safely - header("Content-Metadata: " . $formattedMetadata); - header("Content-Type: application/octet-stream"); - header("Content-Length: " . strlen($body)); - return $body; - } catch (InvalidArgumentException $e) { - // Clean up output buffer if still active - if (ob_get_level()) { - ob_end_clean(); - } - return GenericServerError("Invalid argument: " . $e->getMessage(), 400); - } catch (Exception $e) { - // Clean up output buffer if still active - if (ob_get_level()) { - ob_end_clean(); - } - if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) { - return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms"); - } else { - return GenericServerError("Server argument: " . $e->getMessage(), 500); - } - } -} diff --git a/test-server/php-v2-server/src/index.php b/test-server/php-v2-server/src/index.php deleted file mode 100644 index 167834e0..00000000 --- a/test-server/php-v2-server/src/index.php +++ /dev/null @@ -1,295 +0,0 @@ -= 7 && $parts[5] === 'PHPSESSID') { - error_log("Found session ID in cookies.txt: " . $parts[6]); - return $parts[6]; // Return the session ID value - } - } - - error_log("No PHPSESSID found in cookies.txt file"); - return null; -} - -// Function to write session ID to cookies.txt file -function writeSessionIdToCookiesFile($sessionId) -{ - $cookiesFile = __DIR__ . '/../cookies.txt'; - - // Create Netscape cookie format entry - $cookieLine = "localhost\tFALSE\t/\tFALSE\t0\tPHPSESSID\t$sessionId"; - - // Write header and cookie entry - $content = "# Netscape HTTP Cookie File\n"; - $content .= "# https://curl.se/docs/http-cookies.html\n"; - $content .= "# This file was generated by libcurl! Edit at your own risk.\n\n"; - $content .= $cookieLine . "\n"; - - $result = file_put_contents($cookiesFile, $content); - - if ($result === false) { - error_log("Failed to write session ID to cookies.txt file: $cookiesFile"); - return false; - } - - error_log("Successfully wrote session ID to cookies.txt: $sessionId"); - return true; -} - -set_time_limit(600); -// Start session to persist cache across requests -// First try to use session ID from cookies.txt if available -$sessionId = getSessionIdFromCookiesFile(); -if ($sessionId) { - session_id($sessionId); -} -session_start(); - -// Initialize session cache if it doesn't exist -if (!isset($_SESSION['s3ecCache'])) { - $_SESSION['s3ecCache'] = []; -} - -// Simple router class -class SimpleRouter -{ - private $routes = []; - - public function addRoute($method, $path, $handler) - { - $this->routes[] = [ - 'method' => strtoupper($method), - 'path' => $path, - 'handler' => $handler - ]; - } - - public function handleRequest() - { - $method = $_SERVER['REQUEST_METHOD']; - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); - - foreach ($this->routes as $route) { - if ($route['method'] === $method) { - $params = $this->matchPathWithParams($route['path'], $path); - if ($params !== false) { - return call_user_func($route['handler'], $params); - } - } - } - - // Default 404 response - http_response_code(404); - return json_encode(['error' => 'Not Found']); - } - - private function matchPathWithParams($routePath, $requestPath) - { - // Handle exact matches first (for routes without parameters) - if ($routePath === $requestPath) { - return []; - } - - // Convert route path like '/object/{bucket}/{key}' to regex - $pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath); - $pattern = '/^' . str_replace('/', '\/', $pattern) . '$/'; - - if (preg_match($pattern, $requestPath, $matches)) { - array_shift($matches); // Remove full match - - // Extract parameter names - preg_match_all('/\{([^}]+)\}/', $routePath, $paramNames); - $params = []; - - foreach ($paramNames[1] as $index => $paramName) { - $params[$paramName] = $matches[$index] ?? null; - } - - return $params; - } - - return false; - } -} - -// Helper function to get cached client by ID -function getCachedClient($clientId) -{ - if (!isset($_SESSION['s3ecCache'][$clientId])) { - return null; - } - - $config = $_SESSION['s3ecCache'][$clientId]; - - // Recreate the AWS clients from stored configuration - $s3Client = new S3Client($config['s3Config']); - $encryptionClient = new S3EncryptionClientV2($s3Client); - - $kmsClient = new KmsClient($config['kmsConfig']); - $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $config['kmsKeyId']); - - return [ - 's3Client' => $s3Client, - 'encryptionClient' => $encryptionClient, - 'materialsProvider' => $materialsProvider, - 'config' => $config - ]; -} - -function createDefaultClientTuple(): array -{ - $s3Client = new S3Client([ - 'region' => 'us-west-2', - 'version' => 'latest', - 'http' => [ - 'debug' => false, - 'verify' => true, - 'curl' => [ - CURLOPT_VERBOSE => false, - CURLOPT_NOPROGRESS => true - ] - ] - ]); - $encryptionClient = new S3EncryptionClientV2($s3Client); - - $kmsClient = new KmsClient([ - 'region' => 'us-west-2', - 'version' => 'latest', - 'http' => [ - 'debug' => false, - 'verify' => true, - 'curl' => [ - CURLOPT_VERBOSE => false, - CURLOPT_NOPROGRESS => true - ] - ] - ]); - $materialsProvider = new KmsMaterialsProviderV2($kmsClient, 'arn:aws:kms:us-west-2:370957321024:alias/S3EC-Test-Server-Github-KMS-Key'); - - return [ - 'encryptionClient' => $encryptionClient, - 'materialsProvider' => $materialsProvider - ]; -} - -function metadataStringToMap($metadata): array -{ - $md = []; - - if (empty($metadata)) { - return $md; - } - - $mdList = explode(',', $metadata); - - foreach ($mdList as $entry) { - $parts = explode(']:[', $entry); - - if (count($parts) === 2) { - $key = substr($parts[0], 1); - $value = substr($parts[1], 0, -1); - $md[$key] = $value; - } else { - throw new InvalidArgumentException("Malformed metadata list entry: " . $entry); - } - } - - return $md; -} -function formatMetadataForResponse($metadata) -{ - $metadataList = []; - // Handle different metadata input types - if (is_array($metadata)) { - // If it's an associative array (like Python dict) - foreach ($metadata as $key => $value) { - $metadataList[] = $key . '=' . $value; - } - } elseif (is_string($metadata) && !empty($metadata)) { - // If it's already a string, assume it's in the correct format - return $metadata; - } - - // Convert array to comma-separated string - return implode(',', $metadataList); -} - -// Initialize router -$router = new SimpleRouter(); - -// Add basic routes -$router->addRoute('GET', '/', function () { - return json_encode([ - 'service' => 'S3EC PHP v2 Test Server', - 'status' => 'running', - 'port' => 8087, - 'endpoints' => [ - 'GET /' => 'Server status', - 'POST /client' => 'Create an S3EncryptionClient and cache it.', - 'GET /object/{bucket}/{key}' => 'Handle GET requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a GetObject request to S3.', - 'PUT /object/{bucket}/{key}' => 'Handle PUT requests to /object/{bucket}/{key} by using the S3EncryptionClient to make a PutObject request to S3.', - ] - ]); -}); - -$router->addRoute('GET', '/cache', function () { - return json_encode([ - 'sessionId' => session_id(), - 'sessionStatus' => session_status(), - 'totalCachedClients' => count($_SESSION['s3ecCache'] ?? []), - 'allClientIds' => array_keys($_SESSION['s3ecCache'] ?? []), - 'cacheDetails' => $_SESSION['s3ecCache'] ?? [] - ]); -}); - -$router->addRoute('GET', '/object/{bucket}/{key}', function ($params) { - return handleGetObject($params); -}); - -$router->addRoute('PUT', '/object/{bucket}/{key}', function ($params) { - return handlePutObject($params); -}); - -$router->addRoute('POST', '/client', function () { - return handleCreateClient(); -}); - -// Handle the request and output response -$result = $router->handleRequest(); -if ($result !== false) { - echo $result; -} diff --git a/test-server/php-v2-server/src/put_object.php b/test-server/php-v2-server/src/put_object.php deleted file mode 100644 index c6c592fe..00000000 --- a/test-server/php-v2-server/src/put_object.php +++ /dev/null @@ -1,79 +0,0 @@ - 'gcm', - 'KeySize' => 256, - ]; - $legacyConfig = $s3ecClientTuple["legacy"] ?? false; - $legacy = null; - if ($legacyConfig === false) { - $legacy = "V2"; - } else { - $legacy = "V2_AND_LEGACY"; - } - $strategy = $s3ecClientTuple["config"]["instFilePut"] ? - new InstructionFileMetadataStrategy($s3Client) : - new HeadersMetadataStrategy(); - try { - $result = $s3ec->putObject([ - '@SecurityProfile' => $legacy, - '@MaterialsProvider' => $materialProvider, - '@KmsEncryptionContext' => $encryptionContext, - '@MetadataStrategy' => $strategy, - '@CipherOptions' => $cipherOptions, - 'Bucket' => $bucket, - 'Key' => $key, - 'Body' => $rawBody, - ]); - - header("Content-Type: application/json"); - return json_encode([ - "bucket" => $bucket, - "key" => $key, - // php for some reason blows java's heap if we pass the metadata - // "metadata" => $encryptionContext - ]); - - } catch (InvalidArgumentException $e) { - return S3EncryptionClientError("Invalid arguement: " . $e->getMessage()); - } catch (Exception $e) { - return GenericServerError("Server error: " . $e->getMessage()); - } -} From bc446111fab58a6077812eb4f8e02a047bba3945 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 13:35:53 -0800 Subject: [PATCH 186/201] remove NET V2 --- .github/workflows/test.yml | 2 - .gitmodules | 8 -- .../amazon/encryption/s3/TestUtils.java | 9 -- .../net-v2-v3-server/.duvet/.gitignore | 3 - .../net-v2-v3-server/.duvet/config.toml | 21 ---- test-server/net-v2-v3-server/.gitignore | 44 -------- .../Controllers/ClientController.cs | 106 ------------------ .../Controllers/ObjectController.cs | 105 ----------------- test-server/net-v2-v3-server/Makefile | 68 ----------- .../net-v2-v3-server/Models/ClientRequest.cs | 34 ------ .../net-v2-v3-server/Models/ClientResponse.cs | 8 -- .../net-v2-v3-server/Models/ErrorModels.cs | 17 --- .../net-v2-v3-server/NetV2V3Server.csproj | 39 ------- test-server/net-v2-v3-server/Program.cs | 21 ---- test-server/net-v2-v3-server/README.md | 72 ------------ .../Services/ClientCacheService.cs | 28 ----- test-server/net-v2-v3-server/s3ec-net-v2 | 1 - test-server/net-v2-v3-server/s3ec-net-v3 | 1 - .../net-v3-transition-server/README.md | 4 +- test-server/net-v4-server/README.md | 6 +- 20 files changed, 5 insertions(+), 592 deletions(-) delete mode 100644 test-server/net-v2-v3-server/.duvet/.gitignore delete mode 100644 test-server/net-v2-v3-server/.duvet/config.toml delete mode 100644 test-server/net-v2-v3-server/.gitignore delete mode 100644 test-server/net-v2-v3-server/Controllers/ClientController.cs delete mode 100644 test-server/net-v2-v3-server/Controllers/ObjectController.cs delete mode 100644 test-server/net-v2-v3-server/Makefile delete mode 100644 test-server/net-v2-v3-server/Models/ClientRequest.cs delete mode 100644 test-server/net-v2-v3-server/Models/ClientResponse.cs delete mode 100644 test-server/net-v2-v3-server/Models/ErrorModels.cs delete mode 100644 test-server/net-v2-v3-server/NetV2V3Server.csproj delete mode 100644 test-server/net-v2-v3-server/Program.cs delete mode 100644 test-server/net-v2-v3-server/README.md delete mode 100644 test-server/net-v2-v3-server/Services/ClientCacheService.cs delete mode 160000 test-server/net-v2-v3-server/s3ec-net-v2 delete mode 160000 test-server/net-v2-v3-server/s3ec-net-v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e5b5b8a2..5ea728af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -171,8 +171,6 @@ jobs: name: server-logs path: | test-server/*/server.log - test-server/*/net-v2-server.log - test-server/*/net-v3-server.log - name: Stop the servers run: cd test-server && make stop-servers diff --git a/.gitmodules b/.gitmodules index 6f0577f6..df9a207f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,14 +25,6 @@ path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git branch = fire-egg-staging -[submodule "test-server/net-v2-v3-server/s3ec-net-v2"] - path = test-server/net-v2-v3-server/s3ec-net-v2 - url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git - branch = v3sdk-development -[submodule "test-server/net-v2-v3-server/s3ec-net-v3"] - path = test-server/net-v2-v3-server/s3ec-net-v3 - url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git - branch = v4sdk-development [submodule "test-server/net-v4-server/s3ec-net-v4-improved"] path = test-server/net-v4-server/s3ec-net-v4-improved url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git 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 4084c2a1..a9334eed 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 @@ -68,8 +68,6 @@ public class TestUtils { public static final String GO_V3_TRANSITION = "Go-V3-Transition"; public static final String GO_V4 = "Go-V4"; -// public static final String NET_V2_CURRENT = "NET-V2-Current"; -// public static final String NET_V3_CURRENT = "NET-V3-Current"; public static final String NET_V2_TRANSITION = "NET-V2-Transition"; public static final String NET_V3_TRANSITION = "NET-V3-Transition"; public static final String NET_V4 = "NET-V4"; @@ -191,22 +189,15 @@ public class TestUtils { static { final Map servers = new LinkedHashMap<>(); -// servers.put(JAVA_V3_CURRENT, new LanguageServerTarget(JAVA_V3_CURRENT, "8080")); servers.put(PYTHON_V3, new LanguageServerTarget(PYTHON_V3, "8081")); -// servers.put(GO_V3_CURRENT, new LanguageServerTarget(GO_V3_CURRENT, "8082")); -// servers.put(NET_V2_CURRENT, new LanguageServerTarget(NET_V2_CURRENT, "8083")); -// servers.put(NET_V3_CURRENT, new LanguageServerTarget(NET_V3_CURRENT, "8084")); servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); servers.put(CPP_V3, new LanguageServerTarget(CPP_V3, "8091")); - // servers.put(RUBY_V2_CURRENT, new LanguageServerTarget(RUBY_V2_CURRENT, "8086")); -// servers.put(PHP_V2_CURRENT, new LanguageServerTarget(PHP_V2_CURRENT, "8087")); servers.put(GO_V4, new LanguageServerTarget(GO_V4, "8089")); servers.put(NET_V4, new LanguageServerTarget(NET_V4, "8090")); servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); 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(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-v3-server/.duvet/.gitignore b/test-server/net-v2-v3-server/.duvet/.gitignore deleted file mode 100644 index 93956e36..00000000 --- a/test-server/net-v2-v3-server/.duvet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -reports/ -requirements/ -specification/ \ No newline at end of file diff --git a/test-server/net-v2-v3-server/.duvet/config.toml b/test-server/net-v2-v3-server/.duvet/config.toml deleted file mode 100644 index 04d2e812..00000000 --- a/test-server/net-v2-v3-server/.duvet/config.toml +++ /dev/null @@ -1,21 +0,0 @@ -'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" - -[[source]] -pattern = "**/*.cs" - -# Include required specifications here -[[specification]] -source = "../specification/s3-encryption/data-format/content-metadata.md" -[[specification]] -source = "../specification/s3-encryption/data-format/metadata-strategy.md" -[[specification]] -source = "../specification/s3-encryption/encryption.md" -[[specification]] -source = "../specification/s3-encryption/key-derivation.md" - -[report.html] -enabled = true - -# Enable snapshots to prevent requirement coverage regressions -[report.snapshot] -enabled = false diff --git a/test-server/net-v2-v3-server/.gitignore b/test-server/net-v2-v3-server/.gitignore deleted file mode 100644 index 4c20cbc8..00000000 --- a/test-server/net-v2-v3-server/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# 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-v3-server/Controllers/ClientController.cs b/test-server/net-v2-v3-server/Controllers/ClientController.cs deleted file mode 100644 index 437233a8..00000000 --- a/test-server/net-v2-v3-server/Controllers/ClientController.cs +++ /dev/null @@ -1,106 +0,0 @@ -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 NetV2V3Server.Models; -using NetV2V3Server.Services; - -namespace NetV2V3Server.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 = "[NET-current] EnableDelayedAuthenticationMode not supported" }); - if (request.Config.SetBufferSize.HasValue) - return StatusCode(501, new GenericServerError { Message = "[NET-current] 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-current] Created EncryptionMaterialsV2: KMS={KmsKeyId}", - kmsKeyId); - } - 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 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!" }); - } - - var enableLegacyUnauthenticatedModes = request.Config.EnableLegacyUnauthenticatedModes; - var enableLegacyWrappingAlgorithms = request.Config.EnableLegacyWrappingAlgorithms; - - // SecurityProfile V2AndLegacy can decrypt from legacy S3EC but V2 cannot - var enableLegacyMode = enableLegacyUnauthenticatedModes || enableLegacyWrappingAlgorithms; - var securityProfile = enableLegacyMode ? SecurityProfile.V2AndLegacy : SecurityProfile.V2; - - 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; - logger.LogInformation("[NET-current] 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-current] 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-current] Failed to create S3EC client"); - return StatusCode(500, new S3EncryptionClientError - { - Message = $"[NET-current] Failed to create client: {ex.Message}" - }); - } - } -} diff --git a/test-server/net-v2-v3-server/Controllers/ObjectController.cs b/test-server/net-v2-v3-server/Controllers/ObjectController.cs deleted file mode 100644 index bf1842ae..00000000 --- a/test-server/net-v2-v3-server/Controllers/ObjectController.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Text.Json; -using Amazon.S3.Model; -using Microsoft.AspNetCore.Mvc; -using NetV2V3Server.Models; -using NetV2V3Server.Services; - -namespace NetV2V3Server.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 = "ClientID header is required" }); - - var client = clientCacheService.GetClient(clientId); - if (client == null) - return NotFound(new GenericServerError { Message = $"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( - "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, "Failed to put object from S3 for bucket={bucket}, key={key}", bucket, key); - return StatusCode(500, new S3EncryptionClientError { Message = $"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 = "ClientID header is required" }); - - var client = clientCacheService.GetClient(clientId); - if (client == null) - return NotFound(new GenericServerError { Message = $"No client found for ClientID: {clientId}" }); - - try - { - var getRequest = new GetObjectRequest - { - BucketName = bucket, - Key = key - }; - var response = await client.GetObjectAsync(getRequest); - logger.LogInformation("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, "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-v3-server/Makefile b/test-server/net-v2-v3-server/Makefile deleted file mode 100644 index 9881ee38..00000000 --- a/test-server/net-v2-v3-server/Makefile +++ /dev/null @@ -1,68 +0,0 @@ -# Makefile for S3 Encryption Client .NET Testing - -.PHONY: build-server start-server stop-server wait-for-server - -PID_FILE_NET_V2 := net-v2-server.pid -PID_FILE_NET_V3 := net-v3-server.pid -PORT_NET_V2 := 8083 -PORT_NET_V3 := 8084 - -build-server: - @echo "Building .NET V2 and V3 servers..." - rm -rf obj/v2 bin/v2 obj/v3 bin/v3 - dotnet build -p:S3EncryptionVersion=v2 -o bin/v2 -p:BaseIntermediateOutputPath=obj/v2/ - rm -rf obj - dotnet build -p:S3EncryptionVersion=v3 -o bin/v3 -p:BaseIntermediateOutputPath=obj/v3/ - -start-server: - $(MAKE) start-net-v2-server; \ - $(MAKE) start-net-v3-server; - -stop-server: - @echo "Stopping .NET V2 server on port $(PORT_NET_V2)..." - @lsof -ti:$(PORT_NET_V2) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE_NET_V2) ]; then \ - pkill -P $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE_NET_V2)) 2>/dev/null || true; \ - rm -f $(PID_FILE_NET_V2); \ - fi - @rm -f net-v2-server.log - @echo "Stopping .NET V3 server on port $(PORT_NET_V3)..." - @lsof -ti:$(PORT_NET_V3) | xargs kill -9 2>/dev/null || true - @if [ -f $(PID_FILE_NET_V3) ]; then \ - pkill -P $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ - kill -9 $$(cat $(PID_FILE_NET_V3)) 2>/dev/null || true; \ - rm -f $(PID_FILE_NET_V3); \ - fi - @rm -f net-v3-server.log - @echo "Servers stopped" - -# Start .NET V2 server in background -start-net-v2-server: - @echo "Starting .NET V2 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 bin/v2/NetV2V3Server.dll > net-v2-server.log 2>&1 & echo $$! > net-v2-server.pid - @echo ".NET V2 server starting..." - -# Start .NET V3 server in background -start-net-v3-server: - @echo "Starting .NET V3 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 bin/v3/NetV2V3Server.dll > net-v3-server.log 2>&1 & echo $$! > net-v3-server.pid - @echo ".NET V3 server starting..." - -wait-for-server: - $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V2) - $(MAKE) -C .. wait-for-port PORT=$(PORT_NET_V3) - -duvet: - duvet report - -view-report-mac: - open .duvet/reports/report.html diff --git a/test-server/net-v2-v3-server/Models/ClientRequest.cs b/test-server/net-v2-v3-server/Models/ClientRequest.cs deleted file mode 100644 index e51a9c25..00000000 --- a/test-server/net-v2-v3-server/Models/ClientRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace NetV2V3Server.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(); - public InstructionFileConfig? InstructionFileConfig { get; set; } -} - -public class KeyMaterial -{ - public byte[]? RsaKey { get; set; } - public byte[]? AesKey { get; set; } - public string? KmsKeyId { get; set; } -} - -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-v3-server/Models/ClientResponse.cs b/test-server/net-v2-v3-server/Models/ClientResponse.cs deleted file mode 100644 index 1e029ef7..00000000 --- a/test-server/net-v2-v3-server/Models/ClientResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NetV2V3Server.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-v3-server/Models/ErrorModels.cs b/test-server/net-v2-v3-server/Models/ErrorModels.cs deleted file mode 100644 index af1646e7..00000000 --- a/test-server/net-v2-v3-server/Models/ErrorModels.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NetV2V3Server.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-v3-server/NetV2V3Server.csproj b/test-server/net-v2-v3-server/NetV2V3Server.csproj deleted file mode 100644 index 8d664eff..00000000 --- a/test-server/net-v2-v3-server/NetV2V3Server.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net8.0 - enable - enable - false - - - - false - - - - S3EC_V2 - - - - S3EC_V3 - - - - - - - - - - - - - - - - - - - - diff --git a/test-server/net-v2-v3-server/Program.cs b/test-server/net-v2-v3-server/Program.cs deleted file mode 100644 index 1b77de5d..00000000 --- a/test-server/net-v2-v3-server/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using NetV2V3Server.Services; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddControllers(); -builder.Services.AddSingleton(); - -#if S3EC_V2 -const int port = 8083; -#else -const int port = 8084; -#endif - -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-v3-server/README.md b/test-server/net-v2-v3-server/README.md deleted file mode 100644 index 9463d8b5..00000000 --- a/test-server/net-v2-v3-server/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Net-V2-V3-Server - -A .NET test server for Amazon S3 encryption client .NET v2 and v3. - -## Project Structure - -``` -net-v2-v3-server/ -├── Controllers/ # API controllers -├── Models/ # Data models -├── Services/ # Business logic services -├── Program.cs # Application entry point -├── NetV2V3Server.csproj # Project file -└── README.md # This file -``` - -## Running the Server - -For S3 Encryption Client v2 (runs on port 8083): - -```bash -dotnet run -p:S3EncryptionVersion=v2 -``` - -For S3 Encryption Client v3 (runs on port 8084): - -```bash -dotnet run -p:S3EncryptionVersion=v3 -``` - -## 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"}}}' \ - http://localhost:8083/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-v3-server/Services/ClientCacheService.cs b/test-server/net-v2-v3-server/Services/ClientCacheService.cs deleted file mode 100644 index d8239c9b..00000000 --- a/test-server/net-v2-v3-server/Services/ClientCacheService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Amazon.Extensions.S3.Encryption; -using System.Collections.Concurrent; - -namespace NetV2V3Server.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-v3-server/s3ec-net-v2 b/test-server/net-v2-v3-server/s3ec-net-v2 deleted file mode 160000 index ed27648c..00000000 --- a/test-server/net-v2-v3-server/s3ec-net-v2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ed27648c36a2b290b52f94586fce107af7c51fe5 diff --git a/test-server/net-v2-v3-server/s3ec-net-v3 b/test-server/net-v2-v3-server/s3ec-net-v3 deleted file mode 160000 index 7a552940..00000000 --- a/test-server/net-v2-v3-server/s3ec-net-v3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7a55294068bb3bb7f96226efd6d9edcd1057184b diff --git a/test-server/net-v3-transition-server/README.md b/test-server/net-v3-transition-server/README.md index 7634a4e7..ea925c73 100644 --- a/test-server/net-v3-transition-server/README.md +++ b/test-server/net-v3-transition-server/README.md @@ -1,11 +1,11 @@ -# Net-V2-V3-Server +# Net-V3-Transition-Server A .NET test server for Amazon S3 encryption client .NET v3 transition. ## Project Structure ``` -net-v2-v3-server/ +net-v3-transition-server/ ├── Controllers/ # API controllers ├── Models/ # Data models ├── Services/ # Business logic services diff --git a/test-server/net-v4-server/README.md b/test-server/net-v4-server/README.md index dd5a6753..487d8471 100644 --- a/test-server/net-v4-server/README.md +++ b/test-server/net-v4-server/README.md @@ -1,11 +1,11 @@ -# Net-V2-V3-Server +# Net-V4-Server -A .NET test server for Amazon S3 encryption client .NET v2 and v3. +A .NET test server for Amazon S3 encryption client .NET v4. ## Project Structure ``` -net-v2-v3-server/ +net-v4-server/ ├── Controllers/ # API controllers ├── Models/ # Data models ├── Services/ # Business logic services From 56cba03551c3cf88163b488ca47ea95e43b254b2 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 13:36:32 -0800 Subject: [PATCH 187/201] delete examples --- .github/workflows/examples.yml | 88 --- all-examples/Makefile | 110 ---- all-examples/README.md | 74 --- all-examples/cpp/CMakeLists.txt | 23 - all-examples/cpp/Makefile | 60 --- all-examples/cpp/README.md | 15 - all-examples/cpp/main.cpp | 245 --------- all-examples/go/v3/Makefile | 69 --- all-examples/go/v3/README.md | 55 -- all-examples/go/v3/go.mod | 32 -- all-examples/go/v3/go.sum | 40 -- all-examples/go/v3/local-go-s3ec | 1 - all-examples/go/v3/main.go | 171 ------ all-examples/go/v4/Makefile | 69 --- all-examples/go/v4/README.md | 55 -- all-examples/go/v4/go.mod | 32 -- all-examples/go/v4/go.sum | 40 -- all-examples/go/v4/local-go-s3ec | 1 - all-examples/go/v4/main.go | 171 ------ all-examples/java/v3/Makefile | 75 --- all-examples/java/v3/README.md | 57 -- all-examples/java/v3/build.gradle.kts | 47 -- .../java/v3/gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - all-examples/java/v3/gradlew | 249 --------- all-examples/java/v3/gradlew.bat | 92 ---- all-examples/java/v3/s3ec-staging | 1 - all-examples/java/v3/settings.gradle.kts | 1 - .../amazon/encryption/s3/example/Main.java | 161 ------ all-examples/java/v4/Makefile | 75 --- all-examples/java/v4/README.md | 57 -- all-examples/java/v4/build.gradle.kts | 47 -- .../java/v4/gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - all-examples/java/v4/gradlew | 249 --------- all-examples/java/v4/gradlew.bat | 92 ---- all-examples/java/v4/s3ec-staging | 1 - all-examples/java/v4/settings.gradle.kts | 1 - .../amazon/encryption/s3/example/Main.java | 160 ------ all-examples/net/.gitignore | 19 - all-examples/net/v3/Makefile | 66 --- all-examples/net/v3/Program.cs | 76 --- all-examples/net/v3/s3ec-v3-local | 1 - all-examples/net/v3/v3.csproj | 19 - all-examples/net/v4/Makefile | 66 --- all-examples/net/v4/Program.cs | 76 --- all-examples/net/v4/s3ec-v4-local | 1 - all-examples/net/v4/v4.csproj | 19 - all-examples/php/v2/.gitignore | 4 - all-examples/php/v2/Makefile | 71 --- all-examples/php/v2/composer.json | 33 -- all-examples/php/v2/local-php-sdk | 1 - all-examples/php/v2/main.php | 161 ------ all-examples/php/v3/.gitignore | 4 - all-examples/php/v3/Makefile | 71 --- all-examples/php/v3/composer.json | 33 -- all-examples/php/v3/local-php-sdk | 1 - all-examples/php/v3/main.php | 508 ------------------ all-examples/ruby/v2/Gemfile | 12 - all-examples/ruby/v2/Gemfile.lock | 82 --- all-examples/ruby/v2/Makefile | 70 --- all-examples/ruby/v2/local-ruby-sdk | 1 - all-examples/ruby/v2/main.rb | 150 ------ all-examples/ruby/v3/Gemfile | 12 - all-examples/ruby/v3/Makefile | 70 --- all-examples/ruby/v3/local-ruby-sdk | 1 - all-examples/ruby/v3/main.rb | 148 ----- 67 files changed, 4506 deletions(-) delete mode 100644 .github/workflows/examples.yml delete mode 100644 all-examples/Makefile delete mode 100644 all-examples/README.md delete mode 100644 all-examples/cpp/CMakeLists.txt delete mode 100644 all-examples/cpp/Makefile delete mode 100644 all-examples/cpp/README.md delete mode 100644 all-examples/cpp/main.cpp delete mode 100644 all-examples/go/v3/Makefile delete mode 100644 all-examples/go/v3/README.md delete mode 100644 all-examples/go/v3/go.mod delete mode 100644 all-examples/go/v3/go.sum delete mode 120000 all-examples/go/v3/local-go-s3ec delete mode 100644 all-examples/go/v3/main.go delete mode 100644 all-examples/go/v4/Makefile delete mode 100644 all-examples/go/v4/README.md delete mode 100644 all-examples/go/v4/go.mod delete mode 100644 all-examples/go/v4/go.sum delete mode 120000 all-examples/go/v4/local-go-s3ec delete mode 100644 all-examples/go/v4/main.go delete mode 100644 all-examples/java/v3/Makefile delete mode 100644 all-examples/java/v3/README.md delete mode 100644 all-examples/java/v3/build.gradle.kts delete mode 100644 all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar delete mode 100644 all-examples/java/v3/gradle/wrapper/gradle-wrapper.properties delete mode 100755 all-examples/java/v3/gradlew delete mode 100644 all-examples/java/v3/gradlew.bat delete mode 120000 all-examples/java/v3/s3ec-staging delete mode 100644 all-examples/java/v3/settings.gradle.kts delete mode 100644 all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java delete mode 100644 all-examples/java/v4/Makefile delete mode 100644 all-examples/java/v4/README.md delete mode 100644 all-examples/java/v4/build.gradle.kts delete mode 100644 all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar delete mode 100644 all-examples/java/v4/gradle/wrapper/gradle-wrapper.properties delete mode 100755 all-examples/java/v4/gradlew delete mode 100644 all-examples/java/v4/gradlew.bat delete mode 120000 all-examples/java/v4/s3ec-staging delete mode 100644 all-examples/java/v4/settings.gradle.kts delete mode 100644 all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java delete mode 100644 all-examples/net/.gitignore delete mode 100644 all-examples/net/v3/Makefile delete mode 100644 all-examples/net/v3/Program.cs delete mode 120000 all-examples/net/v3/s3ec-v3-local delete mode 100644 all-examples/net/v3/v3.csproj delete mode 100644 all-examples/net/v4/Makefile delete mode 100644 all-examples/net/v4/Program.cs delete mode 120000 all-examples/net/v4/s3ec-v4-local delete mode 100644 all-examples/net/v4/v4.csproj delete mode 100644 all-examples/php/v2/.gitignore delete mode 100644 all-examples/php/v2/Makefile delete mode 100644 all-examples/php/v2/composer.json delete mode 120000 all-examples/php/v2/local-php-sdk delete mode 100755 all-examples/php/v2/main.php delete mode 100644 all-examples/php/v3/.gitignore delete mode 100644 all-examples/php/v3/Makefile delete mode 100644 all-examples/php/v3/composer.json delete mode 120000 all-examples/php/v3/local-php-sdk delete mode 100755 all-examples/php/v3/main.php delete mode 100644 all-examples/ruby/v2/Gemfile delete mode 100644 all-examples/ruby/v2/Gemfile.lock delete mode 100644 all-examples/ruby/v2/Makefile delete mode 120000 all-examples/ruby/v2/local-ruby-sdk delete mode 100644 all-examples/ruby/v2/main.rb delete mode 100644 all-examples/ruby/v3/Gemfile delete mode 100644 all-examples/ruby/v3/Makefile delete mode 120000 all-examples/ruby/v3/local-ruby-sdk delete mode 100644 all-examples/ruby/v3/main.rb diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml deleted file mode 100644 index 1ef01de4..00000000 --- a/.github/workflows/examples.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Run Examples - -on: - workflow_call: - -jobs: - run-examples: - runs-on: macos-14-large - permissions: - id-token: write - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - submodules: true - token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - - - name: Checkout CPP code cpp-examples - uses: actions/checkout@v5 - with: - submodules: recursive - repository: aws/aws-sdk-cpp - ref: main - path: all-examples/cpp/aws-sdk-cpp/ - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.4.7" - - - name: Set up PHP with Composer - uses: shivammathur/setup-php@verbose - with: - php-version: "8.1" - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.24 - - # Cache uv dependencies - - name: Cache uv dependencies - uses: actions/cache@v3 - with: - path: ~/.cache/uv - key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-uv- - - - name: Install Uv - run: pip install uv - - # Cache Gradle dependencies and build outputs - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - test-server/java-tests/.gradle - key: ${{ runner.os }}-gradle-${{ hashFiles('test-server/java-tests/**/gradle-wrapper.properties', 'test-server/java-tests/**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role - aws-region: us-west-2 - - - name: Install dependencies for all examples - working-directory: ./all-examples - run: make install - - - name: Run all examples - working-directory: ./all-examples - run: make run - env: - AWS_REGION: us-west-2 - BUCKET_NAME: ${{ vars.TEST_SERVER_S3_BUCKET }} - KMS_KEY_ID: ${{ vars.TEST_SERVER_KMS_KEY_ARN }} diff --git a/all-examples/Makefile b/all-examples/Makefile deleted file mode 100644 index eeb0949b..00000000 --- a/all-examples/Makefile +++ /dev/null @@ -1,110 +0,0 @@ -# Makefile for S3 Encryption Client Examples -# Runs make commands across all language/version directories - -# Default target -.PHONY: all install clean run help list-examples - -# Find all directories with Makefiles -EXAMPLE_DIRS := $(shell find . -name Makefile -not -path "./Makefile" -not -path "./cpp/aws-sdk-cpp/**" -not -path "./cpp/build/**" | xargs dirname | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) - -all: install - -# Install dependencies for all examples -install: - @echo "Installing dependencies for all examples..." - @failed=0; \ - for dir in $(EXAMPLE_DIRS); do \ - echo ""; \ - echo "=== Installing dependencies in $$dir ==="; \ - if (cd $$dir && $(MAKE) install); then \ - echo "✓ Successfully installed dependencies in $$dir"; \ - else \ - echo "✗ Failed to install dependencies in $$dir"; \ - failed=$$((failed + 1)); \ - fi; \ - done; \ - echo ""; \ - if [ $$failed -eq 0 ]; then \ - echo "All dependencies installed successfully!"; \ - else \ - echo "$$failed example(s) failed to install dependencies"; \ - exit 1; \ - fi - -# Clean all examples -clean: - @echo "Cleaning all examples..." - @failed=0; \ - for dir in $(EXAMPLE_DIRS); do \ - echo ""; \ - echo "=== Cleaning $$dir ==="; \ - if (cd $$dir && $(MAKE) clean); then \ - echo "✓ Successfully cleaned $$dir"; \ - else \ - echo "✗ Failed to clean $$dir"; \ - failed=$$((failed + 1)); \ - fi; \ - done; \ - echo ""; \ - if [ $$failed -eq 0 ]; then \ - echo "All examples cleaned successfully!"; \ - else \ - echo "$$failed example(s) failed to clean"; \ - exit 1; \ - fi - -# Run all examples with default parameters -run: - @echo "Running all examples with default parameters..." - @failed=0; \ - for dir in $(EXAMPLE_DIRS); do \ - echo ""; \ - echo "=== Running example in $$dir ==="; \ - if (cd $$dir && $(MAKE) run); then \ - echo "✓ Successfully ran example in $$dir"; \ - else \ - echo "✗ Failed to run example in $$dir"; \ - failed=$$((failed + 1)); \ - fi; \ - done; \ - echo ""; \ - if [ $$failed -eq 0 ]; then \ - echo "All examples completed successfully!"; \ - else \ - echo "$$failed example(s) failed to run"; \ - exit 1; \ - fi - -# List all available examples -list-examples: - @echo "Available S3 Encryption Client examples:" - @for dir in $(EXAMPLE_DIRS); do \ - echo " $$dir"; \ - done - -# Show help -help: - @echo "S3 Encryption Client Examples Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install dependencies for all examples" - @echo " run - Run all examples with default parameters" - @echo " clean - Clean all examples" - @echo " list-examples - List all available example directories" - @echo " help - Show this help message" - @echo "" - @echo "Filtering examples:" - @echo " Use FILTER to run commands on specific examples:" - @echo " make install FILTER=go # Only Go examples" - @echo " make run FILTER=v4 # Only v4 examples" - @echo " make clean FILTER=go/v3,ruby # Go v3 and Ruby examples" - @echo "" - @echo "Individual example usage:" - @echo " To work with a specific example, cd into its directory and use its Makefile:" - @echo " cd go/v4 && make run" - @echo " cd ruby/v2 && make install" - @echo "" - @echo "Available examples:" - @for dir in $(EXAMPLE_DIRS); do \ - echo " $$dir"; \ - done diff --git a/all-examples/README.md b/all-examples/README.md deleted file mode 100644 index 8472de78..00000000 --- a/all-examples/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# S3 Encryption Client Examples - -This directory contains example projects for the Amazon S3 Encryption Client across different programming languages and major versions. - -## Directory Structure - -Each language has subdirectories for different major versions of the S3 Encryption Client: - -- `cpp/` - C++ examples - - `v2/` - S3EC C++ v2 example (transitional) - - `v3/` - S3EC C++ v3 example (improved) -- `net/` - .NET examples - - `v3/` - S3EC .NET v3 example (transitional) - - `v4/` - S3EC .NET v4 example (improved) -- `go/` - Go examples - - `v3/` - S3EC Go v3 example (transitional) - - `v4/` - S3EC Go v4 example (improved) -- `java/` - Java examples - - `v3/` - S3EC Java v3 example (transitional) - - `v4/` - S3EC Java v4 example (improved) -- `php/` - PHP examples - - `v2/` - S3EC PHP v2 example (transitional) - - `v3/` - S3EC PHP v3 example (improved) -- `ruby/` - Ruby examples - - `v2/` - S3EC Ruby v2 example (transitional) - - `v3/` - S3EC Ruby v3 example (improved) - -## Setup Instructions - -### Prerequisites - -1. **Git Submodules**: Some examples depend on staging versions of the S3EC libraries that are included as git submodules. Initialize and update submodules: - - ```bash - git submodule update --init --recursive - ``` - -2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: - - AWS CLI: `aws configure` - - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - - IAM roles (for EC2 instances) - -3. **KMS Key**: Use "arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01" by default, or create a KMS key in your AWS account and note the key ID for use in examples. - -### Language-Specific Setup - -Each language directory contains specific setup instructions in its README file. Generally: - -- **Java**: Requires JDK 11+ and Gradle -- **Go**: Requires Go 1.21+ -- **.NET**: Requires .NET 8.0+ -- **PHP**: Requires PHP 7.4+ and Composer -- **Ruby**: Requires Ruby 3.0+ and Bundler -- **C++**: Requires CMake 3.16+ and C++17 compiler - -## Usage - -Each example directory contains: - -- Build configuration files (e.g., `build.gradle.kts`, `go.mod`, `composer.json`) -- Source code demonstrating basic S3EC usage -- README with specific setup and run instructions - -## Dependencies - -Examples use different dependency sources based on version: - -- **Released versions**: Use public package repositories (Maven Central, npm, etc.) -- **Staging versions**: Use git submodules pointing to staging repositories -- **Local versions**: Reference locally built libraries - -## Support - -For issues with specific examples, refer to the individual README files in each language/version directory. diff --git a/all-examples/cpp/CMakeLists.txt b/all-examples/cpp/CMakeLists.txt deleted file mode 100644 index 906951b8..00000000 --- a/all-examples/cpp/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(s3ec-test) - -set(CMAKE_CXX_STANDARD 14) - -# Configure AWS SDK build options -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") - -# Add AWS SDK as subdirectory -add_subdirectory(aws-sdk-cpp) - -find_package(PkgConfig REQUIRED) - -add_executable(s3ec-test main.cpp) - -target_link_libraries(s3ec-test - aws-cpp-sdk-core - aws-cpp-sdk-kms - aws-cpp-sdk-s3 - aws-cpp-sdk-s3-encryption -) diff --git a/all-examples/cpp/Makefile b/all-examples/cpp/Makefile deleted file mode 100644 index 64550528..00000000 --- a/all-examples/cpp/Makefile +++ /dev/null @@ -1,60 +0,0 @@ -# Makefile for S3 Encryption Client C++ Example - -.PHONY: all install clean run help - -# Default arguments for running the example -# Override these when calling make run -VERSION ?= V3 -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-cpp-test -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: run - -install: build/s3ec-test - -aws-sdk-cpp: - git clone --recurse-submodules -b fire-egg-dev https://github.com/awslabs/aws-sdk-cpp-staging.git aws-sdk-cpp - -build/s3ec-test: aws-sdk-cpp - mkdir -p build && cd build && cmake .. && make - -clean: - rm -rf build - -# Run the example with default arguments -run: build/s3ec-test - @echo "Running S3 Encryption Client C++ example..." - @echo "Version: $(VERSION)" - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - ./build/s3ec-test $(VERSION) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client C++ Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install Go dependencies using Go modules" - @echo " run - Install dependencies and run the example" - @echo " clean - Remove C++ artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " VERSION = $(VERSION) (must be V2 or V3)" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run VERSION=your-version BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Read access to https://github.com/awslabs/aws-sdk-cpp-staging.git" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" diff --git a/all-examples/cpp/README.md b/all-examples/cpp/README.md deleted file mode 100644 index 8875fb07..00000000 --- a/all-examples/cpp/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# C++ S3 Encryption Test - -Minimal C++ use of S3 Encryption - -## Build - -```bash -make install -``` - -## Run - -```bash -make run -``` diff --git a/all-examples/cpp/main.cpp b/all-examples/cpp/main.cpp deleted file mode 100644 index 30cb2b8d..00000000 --- a/all-examples/cpp/main.cpp +++ /dev/null @@ -1,245 +0,0 @@ -#include -#include -#include -#include - -using namespace Aws::S3Encryption; -using Aws::S3Encryption::Materials::KMSWithContextEncryptionMaterials; - -static Aws::Map get_encryption_context(const char * version) -{ - return { - {"purpose", "example"}, - {"version", version}, - {"language", "c++"} - }; -} - -static int test_migration(const char *bucket, const char *object, const char *kms_key_id, const char *region) -{ - Aws::Client::ClientConfiguration s3ClientConfig; - s3ClientConfig.region = region; - - auto materials = std::make_shared(kms_key_id, s3ClientConfig); - CryptoConfigurationV3 config(materials); - - // STEP 1: Upgrade to V3 client to prepare to read messages with commitment. - // You want to update your readers before you update your writers - config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); - auto client = std::make_shared(config, s3ClientConfig); - - auto encryption_context = get_encryption_context("V3"); - - // Put Object - writes objects WITHOUT commitment - Aws::S3::Model::PutObjectRequest put_request; - put_request.SetBucket(bucket); - put_request.SetKey(object); - - auto data = std::string("This is the sample content."); - - auto stream = std::make_shared(data); - put_request.SetBody(stream); - - // Put Object - writes objects WITHOUT commitment - auto put_outcome = client->PutObject(put_request, encryption_context); - assert(put_outcome.IsSuccess()); - - Aws::S3::Model::GetObjectRequest get_request; - get_request.SetBucket(bucket); - get_request.SetKey(object); - - // Get Object - can read objects with or without commitment - auto get_outcome = client->GetObject(get_request, encryption_context); - assert(get_outcome.IsSuccess()); - - // STEP 2: If all of the readers can read with or without commitment - // you can upgrade the commitment policy to write objects with commitment - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT); - client = std::make_shared(config, s3ClientConfig); - - stream = std::make_shared(data); - put_request.SetBody(stream); - - // Put Object - writes objects WITH commitment - put_outcome = client->PutObject(put_request, encryption_context); - assert(put_outcome.IsSuccess()); - - // Get Object - can read objects with or without commitment - get_outcome = client->GetObject(get_request, encryption_context); - assert(get_outcome.IsSuccess()); - - // STEP 3: Once your system no longer has to read messages without commitment, - // you may update your client to only read messages written with key commitment - config.SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT); - client = std::make_shared(config, s3ClientConfig); - - stream = std::make_shared(data); - put_request.SetBody(stream); - - // Put Object - writes objects WITH commitment - put_outcome = client->PutObject(put_request, encryption_context); - assert(put_outcome.IsSuccess()); - - // Get Object - can only read objects with commitment - get_outcome = client->GetObject(get_request, encryption_context); - assert(get_outcome.IsSuccess()); - - return 0; -} - -static int test_v3(const char *bucket, const char *object, const char *kms_key_id, const char *region) -{ - Aws::Client::ClientConfiguration s3ClientConfig; - s3ClientConfig.region = region; - - auto materials = std::make_shared(kms_key_id, s3ClientConfig); - CryptoConfigurationV3 config(materials); - // config.AllowLegacy(); - // config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - // config.SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT); - - - auto client = std::make_shared(config, s3ClientConfig); - - auto encryption_context = get_encryption_context("V3"); - - Aws::S3::Model::PutObjectRequest put_request; - put_request.SetBucket(bucket); - put_request.SetKey(object); - - auto data = std::string("This is the sample content."); - - auto stream = std::make_shared(data); - put_request.SetBody(stream); - - auto put_outcome = client->PutObject(put_request, encryption_context); - if (put_outcome.IsSuccess()) - { - fprintf(stderr, "PutObject V3 Successful.\n"); - } - else - { - fprintf(stderr, "PutObject V3 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); - return 1; - } - - Aws::S3::Model::GetObjectRequest get_request; - get_request.SetBucket(bucket); - get_request.SetKey(object); - auto get_outcome = client->GetObject(get_request, encryption_context); - if (get_outcome.IsSuccess()) - { - fprintf(stderr, "GetObject V3 Successful.\n"); - Aws::StringStream response_stream; - response_stream << get_outcome.GetResult().GetBody().rdbuf(); - if (response_stream.str() != data) - { - fprintf(stderr, "GetObject V3 returned the wrong data.\n"); - return 1; - } - } - else - { - fprintf(stderr, "GetObject V3 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); - return 1; - } - return 0; -} - -static int test_v2(const char *bucket, const char *object, const char *kms_key_id, const char *region) -{ - Aws::Client::ClientConfiguration s3ClientConfig; - s3ClientConfig.region = region; - - auto materials = std::make_shared(kms_key_id, s3ClientConfig); - CryptoConfigurationV2 config(materials); - // config.SetSecurityProfile(SecurityProfile::V2_AND_LEGACY); - // config.SetStorageMethod(StorageMethod::INSTRUCTION_FILE); - - - auto client = std::make_shared(config, s3ClientConfig); - - auto encryption_context = get_encryption_context("V2"); - - Aws::S3::Model::PutObjectRequest put_request; - put_request.SetBucket(bucket); - put_request.SetKey(object); - - auto data = std::string("This is the sample content."); - - auto stream = std::make_shared(data); - put_request.SetBody(stream); - - auto put_outcome = client->PutObject(put_request, encryption_context); - if (put_outcome.IsSuccess()) - { - fprintf(stderr, "PutObject V2 Successful.\n"); - } - else - { - fprintf(stderr, "PutObject V2 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); - return 1; - } - - Aws::S3::Model::GetObjectRequest get_request; - get_request.SetBucket(bucket); - get_request.SetKey(object); - auto get_outcome = client->GetObject(get_request, encryption_context); - if (get_outcome.IsSuccess()) - { - fprintf(stderr, "GetObject V2 Successful.\n"); - Aws::StringStream response_stream; - response_stream << get_outcome.GetResult().GetBody().rdbuf(); - if (response_stream.str() != data) - { - fprintf(stderr, "GetObject V2 returned the wrong data.\n"); - return 1; - } - } - else - { - fprintf(stderr, "GetObject V2 Failed : %s\n", put_outcome.GetError().GetMessage().c_str()); - return 1; - } - return 0; -} - -int main(int argc, char **argv) -{ - if (argc != 6) - { - fprintf(stderr, "USAGE : s3ec-test version bucket object key_id region"); - return 1; - } - - auto version_str = argv[1]; - auto bucket = argv[2]; - auto object = argv[3]; - auto kms_key_id = argv[4]; - auto region = argv[5]; - - bool is_v3; - if (strcasecmp(version_str, "v3") == 0) - { - is_v3 = true; - } - else if (strcasecmp(version_str, "v2") == 0) - { - is_v3 = false; - } - else - { - fprintf(stderr, "Version was <%s> must be V2 or V3\n", version_str); - return 1; - } - - Aws::SDKOptions options; - Aws::InitAPI(options); - - if (is_v3) - test_v3(bucket, object, kms_key_id, region); - else - test_v2(bucket, object, kms_key_id, region); - - Aws::ShutdownAPI(options); -} diff --git a/all-examples/go/v3/Makefile b/all-examples/go/v3/Makefile deleted file mode 100644 index d7285fe9..00000000 --- a/all-examples/go/v3/Makefile +++ /dev/null @@ -1,69 +0,0 @@ -# Makefile for S3 Encryption Client Go v3 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.go - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-go-v3 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Go modules -install: - @echo "Installing Go dependencies..." - @go mod tidy - @echo "Dependencies installed successfully!" - -# Clean Go artifacts -clean: - @echo "Cleaning Go artifacts..." - @go clean - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v3 Go example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client Go v3 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install Go dependencies using Go modules" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove Go artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Go 1.24+ installed on the system" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v3 Go SDK (included in local-go-s3ec)" diff --git a/all-examples/go/v3/README.md b/all-examples/go/v3/README.md deleted file mode 100644 index 519dfc36..00000000 --- a/all-examples/go/v3/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# S3 Encryption Client Go v3 Example - -This example demonstrates how to use the Amazon S3 Encryption Client v3 for Go to perform client-side encryption and decryption of objects. - -## Prerequisites - -1. **Go**: Requires Go 1.24 or later -2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: - - AWS CLI: `aws configure` - - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - - IAM roles (for EC2 instances) -3. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` -4. **S3 Bucket**: An existing S3 bucket where you have read/write permissions - -## Setup - -1. Initialize submodules and download dependencies: - ```bash - make install - ``` - - Or manually: - ```bash - go mod tidy - ``` - - **Note**: This example uses a local submodule for the S3EC Go v3 library via the `replace` directive in `go.mod`. - -## Usage - -### Using Make (Recommended) - -Run the example with default parameters: -```bash -make run -``` - -Run with custom parameters: -```bash -make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -``` - -### Manual Usage - -Run the example with the following command: - -```bash -go run main.go -``` - -### Example: - -```bash -go run main.go my-test-bucket s3ec-go-v3-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2 -``` diff --git a/all-examples/go/v3/go.mod b/all-examples/go/v3/go.mod deleted file mode 100644 index 1821569a..00000000 --- a/all-examples/go/v3/go.mod +++ /dev/null @@ -1,32 +0,0 @@ -module github.com/aws/amazon-s3-encryption-client-python/all-examples/go/v3 - -go 1.24 - -require ( - github.com/aws/amazon-s3-encryption-client-go/v3 v3.1.0 - github.com/aws/aws-sdk-go-v2 v1.24.0 - github.com/aws/aws-sdk-go-v2/config v1.26.1 - github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 - github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 -) - -require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect - github.com/aws/smithy-go v1.19.0 // indirect -) - -// S3EC Go V3 uses a local submodule for development -replace github.com/aws/amazon-s3-encryption-client-go/v3 => ./local-go-s3ec/v3 diff --git a/all-examples/go/v3/go.sum b/all-examples/go/v3/go.sum deleted file mode 100644 index 244c8814..00000000 --- a/all-examples/go/v3/go.sum +++ /dev/null @@ -1,40 +0,0 @@ -github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= -github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= -github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/all-examples/go/v3/local-go-s3ec b/all-examples/go/v3/local-go-s3ec deleted file mode 120000 index 7e5e770c..00000000 --- a/all-examples/go/v3/local-go-s3ec +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/go-v3-transition-server/local-go-s3ec \ No newline at end of file diff --git a/all-examples/go/v3/main.go b/all-examples/go/v3/main.go deleted file mode 100644 index 22732bc8..00000000 --- a/all-examples/go/v3/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/aws/amazon-s3-encryption-client-go/v3/client" - "github.com/aws/amazon-s3-encryption-client-go/v3/commitment" - "github.com/aws/amazon-s3-encryption-client-go/v3/materials" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -func main() { - // Check command line arguments - if len(os.Args) != 5 { - fmt.Printf("Usage: %s \n", os.Args[0]) - fmt.Printf("Example: %s avp-21638 s3ec-go-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n", os.Args[0]) - os.Exit(1) - } - - bucketName := os.Args[1] - objectKey := os.Args[2] - kmsKeyID := os.Args[3] - region := os.Args[4] - - fmt.Println("=== S3 Encryption Client v3 Example (Go) ===") - fmt.Printf("Bucket: %s\n", bucketName) - fmt.Printf("Object Key: %s\n", objectKey) - fmt.Printf("KMS Key ID: %s\n", kmsKeyID) - fmt.Printf("Region: %s\n", region) - fmt.Println() - - // Test data for encryption - testData := "Hello, World! This is a test message for S3 encryption client v3 in Go." - fmt.Printf("Original data: %s\n", testData) - fmt.Printf("Data length: %d bytes\n", len(testData)) - fmt.Println() - - fmt.Println("--- Initialize S3 Encryption Client v3 ---") - - // Create regular S3 client - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) - if err != nil { - fmt.Printf("Error loading AWS config: %v\n", err) - os.Exit(1) - } - s3Client := s3.NewFromConfig(cfg) - - // Create KMS client - kmsClient := kms.NewFromConfig(cfg) - - // Create KMS keyring - keyring := materials.NewKmsKeyring(kmsClient, kmsKeyID) - - // Create Cryptographic Materials Manager - cmm, err := materials.NewCryptographicMaterialsManager(keyring) - if err != nil { - fmt.Printf("Error creating CMM: %v\n", err) - os.Exit(1) - } - - // Create S3 Encryption Client v3 - encryptionClient, err := client.New(s3Client, cmm, func(options *client.EncryptionClientOptions) { - options.CommitmentPolicy = commitment.FORBID_ENCRYPT_ALLOW_DECRYPT - }) - if err != nil { - fmt.Printf("Error creating S3 Encryption Client: %v\n", err) - os.Exit(1) - } - - fmt.Println("Successfully initialized S3 Encryption Client v3") - fmt.Println("--- Encrypt and Upload Object to S3 ---") - - // Add encryption context - encryptionContext := map[string]string{ - "purpose": "example", - "version": "v3", - "language": "go", - } - - // Create context with encryption context - ctx := context.WithValue(context.Background(), "EncryptionContext", encryptionContext) - - // Upload encrypted object using S3 Encryption Client - putInput := &s3.PutObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(objectKey), - Body: strings.NewReader(testData), - } - - _, err = encryptionClient.PutObject(ctx, putInput) - if err != nil { - if strings.Contains(err.Error(), "NoSuchBucket") { - fmt.Printf("Error: S3 bucket '%s' does not exist or is not accessible\n", bucketName) - } else if strings.Contains(err.Error(), "NotFoundException") { - fmt.Printf("Error: KMS key '%s' not found or not accessible\n", kmsKeyID) - } else { - fmt.Printf("Error uploading encrypted object: %v\n", err) - } - os.Exit(1) - } - - fmt.Println("Successfully uploaded encrypted object to S3!") - fmt.Printf(" Bucket: %s\n", bucketName) - fmt.Printf(" Key: %s\n", objectKey) - fmt.Printf(" Encryption Context: %v\n", encryptionContext) - fmt.Println() - - fmt.Println("--- Download and Decrypt Object from S3 ---") - - // Download and decrypt object using S3 Encryption Client - getInput := &s3.GetObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(objectKey), - } - - getResponse, err := encryptionClient.GetObject(ctx, getInput) - if err != nil { - fmt.Printf("Error downloading and decrypting object: %v\n", err) - os.Exit(1) - } - defer getResponse.Body.Close() - - // Read the decrypted data - decryptedData, err := io.ReadAll(getResponse.Body) - if err != nil { - fmt.Printf("Error reading decrypted data: %v\n", err) - os.Exit(1) - } - - fmt.Println("Successfully downloaded and decrypted object from S3!") - fmt.Printf(" Object size: %d bytes\n", len(decryptedData)) - fmt.Printf(" Decrypted data: %s\n", string(decryptedData)) - fmt.Println() - - fmt.Println("--- Verify Roundtrip Success ---") - - // Verify the roundtrip was successful - if string(decryptedData) == testData { - fmt.Println("SUCCESS: Roundtrip encryption/decryption completed successfully!") - fmt.Println(" Original data matches decrypted data") - fmt.Println(" Data integrity verified") - } else { - fmt.Println("ERROR: Roundtrip failed - data mismatch") - fmt.Printf(" Original: %s\n", testData) - fmt.Printf(" Decrypted: %s\n", string(decryptedData)) - os.Exit(1) - } - - // Optionally Delete the Object - //fmt.Println("--- Cleanup ---") - // Clean up the test object using regular S3 client - // _, err = s3Client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ - // Bucket: aws.String(bucketName), - // Key: aws.String(objectKey), - // }) - // if err != nil { - // fmt.Printf("Error deleting test object: %v\n", err) - // } else { - // fmt.Println("Test object deleted from S3") - // } - - fmt.Println() - fmt.Println("=== Example completed successfully! ===") -} diff --git a/all-examples/go/v4/Makefile b/all-examples/go/v4/Makefile deleted file mode 100644 index 1e8307e7..00000000 --- a/all-examples/go/v4/Makefile +++ /dev/null @@ -1,69 +0,0 @@ -# Makefile for S3 Encryption Client Go v4 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.go - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-go-v4 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Go modules -install: - @echo "Installing Go dependencies..." - @go mod tidy - @echo "Dependencies installed successfully!" - -# Clean Go artifacts -clean: - @echo "Cleaning Go artifacts..." - @go clean - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v4 Go example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @go run $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client Go v4 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install Go dependencies using Go modules" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove Go artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Go 1.24+ installed on the system" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v4 Go SDK (included in local-go-s3ec)" diff --git a/all-examples/go/v4/README.md b/all-examples/go/v4/README.md deleted file mode 100644 index b6972f26..00000000 --- a/all-examples/go/v4/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# S3 Encryption Client Go v4 Example - -This example demonstrates how to use the Amazon S3 Encryption Client v4 for Go to perform client-side encryption and decryption of objects. - -## Prerequisites - -1. **Go**: Requires Go 1.24 or later -2. **AWS Credentials**: Configure your AWS credentials using one of the following methods: - - AWS CLI: `aws configure` - - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - - IAM roles (for EC2 instances) -3. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` -4. **S3 Bucket**: An existing S3 bucket where you have read/write permissions - -## Setup - -1. Initialize submodules and download dependencies: - ```bash - make install - ``` - - Or manually: - ```bash - go mod tidy - ``` - - **Note**: This example uses a local submodule for the S3EC Go v4 library via the `replace` directive in `go.mod`. - -## Usage - -### Using Make (Recommended) - -Run the example with default parameters: -```bash -make run -``` - -Run with custom parameters: -```bash -make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -``` - -### Manual Usage - -Run the example with the following command: - -```bash -go run main.go -``` - -### Example: - -```bash -go run main.go my-test-bucket s3ec-go-v4-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2 -``` diff --git a/all-examples/go/v4/go.mod b/all-examples/go/v4/go.mod deleted file mode 100644 index 48bea56e..00000000 --- a/all-examples/go/v4/go.mod +++ /dev/null @@ -1,32 +0,0 @@ -module github.com/aws/amazon-s3-encryption-client-python/all-examples/go/v4 - -go 1.24 - -require ( - github.com/aws/amazon-s3-encryption-client-go/v4 v4.0.0 - github.com/aws/aws-sdk-go-v2 v1.24.0 - github.com/aws/aws-sdk-go-v2/config v1.26.1 - github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 - github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 -) - -require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect - github.com/aws/smithy-go v1.19.0 // indirect -) - -// S3EC Go V4 uses a local submodule for development -replace github.com/aws/amazon-s3-encryption-client-go/v4 => ./local-go-s3ec/v4 diff --git a/all-examples/go/v4/go.sum b/all-examples/go/v4/go.sum deleted file mode 100644 index 244c8814..00000000 --- a/all-examples/go/v4/go.sum +++ /dev/null @@ -1,40 +0,0 @@ -github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= -github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= -github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4 h1:c75pHGBV3h6WOsIjbJhLyOnlCPXzap45nbiP2Z5jk5M= -github.com/aws/aws-sdk-go-v2/service/kms v1.27.4/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/all-examples/go/v4/local-go-s3ec b/all-examples/go/v4/local-go-s3ec deleted file mode 120000 index d06737d9..00000000 --- a/all-examples/go/v4/local-go-s3ec +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/go-v4-server/local-go-s3ec \ No newline at end of file diff --git a/all-examples/go/v4/main.go b/all-examples/go/v4/main.go deleted file mode 100644 index a1227790..00000000 --- a/all-examples/go/v4/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/aws/amazon-s3-encryption-client-go/v4/client" - "github.com/aws/amazon-s3-encryption-client-go/v4/commitment" - "github.com/aws/amazon-s3-encryption-client-go/v4/materials" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -func main() { - // Check command line arguments - if len(os.Args) != 5 { - fmt.Printf("Usage: %s \n", os.Args[0]) - fmt.Printf("Example: %s avp-21638 s3ec-go-v4 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n", os.Args[0]) - os.Exit(1) - } - - bucketName := os.Args[1] - objectKey := os.Args[2] - kmsKeyID := os.Args[3] - region := os.Args[4] - - fmt.Println("=== S3 Encryption Client v4 Example (Go) ===") - fmt.Printf("Bucket: %s\n", bucketName) - fmt.Printf("Object Key: %s\n", objectKey) - fmt.Printf("KMS Key ID: %s\n", kmsKeyID) - fmt.Printf("Region: %s\n", region) - fmt.Println() - - // Test data for encryption - testData := "Hello, World! This is a test message for S3 encryption client v4 in Go." - fmt.Printf("Original data: %s\n", testData) - fmt.Printf("Data length: %d bytes\n", len(testData)) - fmt.Println() - - fmt.Println("--- Initialize S3 Encryption Client v4 ---") - - // Create regular S3 client - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) - if err != nil { - fmt.Printf("Error loading AWS config: %v\n", err) - os.Exit(1) - } - s3Client := s3.NewFromConfig(cfg) - - // Create KMS client - kmsClient := kms.NewFromConfig(cfg) - - // Create KMS keyring - keyring := materials.NewKmsKeyring(kmsClient, kmsKeyID) - - // Create Cryptographic Materials Manager - cmm, err := materials.NewCryptographicMaterialsManager(keyring) - if err != nil { - fmt.Printf("Error creating CMM: %v\n", err) - os.Exit(1) - } - - // Create S3 Encryption Client v4 - encryptionClient, err := client.New(s3Client, cmm, func(options *client.EncryptionClientOptions) { - options.CommitmentPolicy = commitment.REQUIRE_ENCRYPT_ALLOW_DECRYPT - }) - if err != nil { - fmt.Printf("Error creating S3 Encryption Client: %v\n", err) - os.Exit(1) - } - - fmt.Println("Successfully initialized S3 Encryption Client v4") - fmt.Println("--- Encrypt and Upload Object to S3 ---") - - // Add encryption context - encryptionContext := map[string]string{ - "purpose": "example", - "version": "v4", - "language": "go", - } - - // Create context with encryption context - ctx := context.WithValue(context.Background(), "EncryptionContext", encryptionContext) - - // Upload encrypted object using S3 Encryption Client - putInput := &s3.PutObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(objectKey), - Body: strings.NewReader(testData), - } - - _, err = encryptionClient.PutObject(ctx, putInput) - if err != nil { - if strings.Contains(err.Error(), "NoSuchBucket") { - fmt.Printf("Error: S3 bucket '%s' does not exist or is not accessible\n", bucketName) - } else if strings.Contains(err.Error(), "NotFoundException") { - fmt.Printf("Error: KMS key '%s' not found or not accessible\n", kmsKeyID) - } else { - fmt.Printf("Error uploading encrypted object: %v\n", err) - } - os.Exit(1) - } - - fmt.Println("Successfully uploaded encrypted object to S3!") - fmt.Printf(" Bucket: %s\n", bucketName) - fmt.Printf(" Key: %s\n", objectKey) - fmt.Printf(" Encryption Context: %v\n", encryptionContext) - fmt.Println() - - fmt.Println("--- Download and Decrypt Object from S3 ---") - - // Download and decrypt object using S3 Encryption Client - getInput := &s3.GetObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(objectKey), - } - - getResponse, err := encryptionClient.GetObject(ctx, getInput) - if err != nil { - fmt.Printf("Error downloading and decrypting object: %v\n", err) - os.Exit(1) - } - defer getResponse.Body.Close() - - // Read the decrypted data - decryptedData, err := io.ReadAll(getResponse.Body) - if err != nil { - fmt.Printf("Error reading decrypted data: %v\n", err) - os.Exit(1) - } - - fmt.Println("Successfully downloaded and decrypted object from S3!") - fmt.Printf(" Object size: %d bytes\n", len(decryptedData)) - fmt.Printf(" Decrypted data: %s\n", string(decryptedData)) - fmt.Println() - - fmt.Println("--- Verify Roundtrip Success ---") - - // Verify the roundtrip was successful - if string(decryptedData) == testData { - fmt.Println("SUCCESS: Roundtrip encryption/decryption completed successfully!") - fmt.Println(" Original data matches decrypted data") - fmt.Println(" Data integrity verified") - } else { - fmt.Println("ERROR: Roundtrip failed - data mismatch") - fmt.Printf(" Original: %s\n", testData) - fmt.Printf(" Decrypted: %s\n", string(decryptedData)) - os.Exit(1) - } - - // Optionally Delete the Object - //fmt.Println("--- Cleanup ---") - // Clean up the test object using regular S3 client - // _, err = s3Client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ - // Bucket: aws.String(bucketName), - // Key: aws.String(objectKey), - // }) - // if err != nil { - // fmt.Printf("Error deleting test object: %v\n", err) - // } else { - // fmt.Println("Test object deleted from S3") - // } - - fmt.Println() - fmt.Println("=== Example completed successfully! ===") -} diff --git a/all-examples/java/v3/Makefile b/all-examples/java/v3/Makefile deleted file mode 100644 index 81e4228d..00000000 --- a/all-examples/java/v3/Makefile +++ /dev/null @@ -1,75 +0,0 @@ -# Makefile for S3 Encryption Client Java v3 Example - -# Default target -.PHONY: all install clean run help s3ec-staging - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-java-v3 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install S3 Encryption Client library from source -s3ec-staging: - @echo "[JAVA V3] Installing S3 Encryption Client library from source..." - @cd s3ec-staging && mvn -B -ntp install -DskipTests - @echo "[JAVA V3] S3 Encryption Client library installed successfully!" - -# Install dependencies using Gradle -install: s3ec-staging - @echo "[JAVA V3] Installing Java dependencies..." - @chmod +x ./gradlew - @./gradlew build - @echo "[JAVA V3] Dependencies installed successfully!" - -# Clean Gradle artifacts -clean: - @echo "[JAVA V3] Cleaning Gradle artifacts..." - @./gradlew clean - @echo "[JAVA V3] Clean completed!" - -# Run the example with default arguments -run: install - @echo "[JAVA V3] Running S3 Encryption Client v3 Java example..." - @echo "[JAVA V3] Bucket: $(BUCKET_NAME)" - @echo "[JAVA V3] Object Key: $(OBJECT_KEY)" - @echo "[JAVA V3] KMS Key ID: $(KMS_KEY_ID)" - @echo "[JAVA V3] Region: $(AWS_REGION)" - @echo "" - @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" - -# Show help -help: - @echo "S3 Encryption Client Java v3 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " s3ec-staging - Install S3 Encryption Client library from source" - @echo " install - Install Java dependencies using Gradle" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove Gradle artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Java 11+ installed on the system" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v3 library installed in local Maven repository" - @echo " (Install by running: cd s3ec-staging && mvn install)" diff --git a/all-examples/java/v3/README.md b/all-examples/java/v3/README.md deleted file mode 100644 index 50e67684..00000000 --- a/all-examples/java/v3/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# S3 Encryption Client Java v3 Example - -This example demonstrates how to use the Amazon S3 Encryption Client v3 for Java to perform client-side encryption and decryption of objects. - -## Prerequisites - -1. **Java**: Requires Java 11 or later -2. **Gradle**: The project uses Gradle wrapper (included - `./gradlew`) -3. **Maven**: Required to install the S3 Encryption Client library from source -4. **AWS Credentials**: Configure your AWS credentials using one of the following methods: - - AWS CLI: `aws configure` - - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - - IAM roles (for EC2 instances) -5. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` -6. **S3 Bucket**: An existing S3 bucket where you have read/write permissions - -## Setup - -Install dependencies and build (this automatically installs the S3 Encryption Client library from source): -```bash -make install -``` - -Or manually: -```bash -cd s3ec-staging && mvn clean install && cd - -./gradlew build -``` - -**Note**: This example uses a local library installed in Maven local repository via the symbolic link `s3ec-staging`. - -## Usage - -### Using Make (Recommended) - -Run the example with default parameters: -```bash -make run -``` - -Run with custom parameters: -```bash -make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -``` - -### Manual Usage - -Run the example with the following command: - -```bash -./gradlew run --args=" " -``` - -### Example: - -```bash -./gradlew run --args="my-test-bucket s3ec-java-v3-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" diff --git a/all-examples/java/v3/build.gradle.kts b/all-examples/java/v3/build.gradle.kts deleted file mode 100644 index f0748625..00000000 --- a/all-examples/java/v3/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - java - application -} - -group = "software.amazon.encryption.s3.example" -version = "1.0.0" - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - // AWS SDK v2 dependencies - implementation(platform("software.amazon.awssdk:bom:2.20.0")) - implementation("software.amazon.awssdk:s3") - implementation("software.amazon.awssdk:kms") - implementation("software.amazon.awssdk:auth") - - // S3 Encryption Client v3 from local Maven repository - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-read-kc") -} - -application { - mainClass.set("software.amazon.encryption.s3.example.Main") -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks.jar { - manifest { - attributes["Main-Class"] = "software.amazon.encryption.s3.example.Main" - } - - // Create a fat jar with all dependencies - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveBaseName.set("s3ec-java-v3-example") -} - -tasks.named("run") { - standardInput = System.`in` -} diff --git a/all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar b/all-examples/java/v3/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e6441136f3d4ba8a0da8d277868979cfbc8ad796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/all-examples/java/v3/gradlew.bat b/all-examples/java/v3/gradlew.bat deleted file mode 100644 index 25da30db..00000000 --- a/all-examples/java/v3/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/all-examples/java/v3/s3ec-staging b/all-examples/java/v3/s3ec-staging deleted file mode 120000 index 1a52a7b9..00000000 --- a/all-examples/java/v3/s3ec-staging +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/java-v3-transition-server/s3ec-staging \ No newline at end of file diff --git a/all-examples/java/v3/settings.gradle.kts b/all-examples/java/v3/settings.gradle.kts deleted file mode 100644 index a4dcbbae..00000000 --- a/all-examples/java/v3/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "s3ec-java-v3-example" diff --git a/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java b/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java deleted file mode 100644 index b0d9c112..00000000 --- a/all-examples/java/v3/src/main/java/software/amazon/encryption/s3/example/Main.java +++ /dev/null @@ -1,161 +0,0 @@ -package software.amazon.encryption.s3.example; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.kms.KmsClient; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.encryption.s3.CommitmentPolicy; -import software.amazon.encryption.s3.S3EncryptionClient; -import software.amazon.encryption.s3.algorithms.AlgorithmSuite; -import software.amazon.encryption.s3.materials.CryptographicMaterialsManager; -import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; -import software.amazon.encryption.s3.materials.KmsKeyring; - -/** - * Example demonstrating the use of Amazon S3 Encryption Client v3 for Java. - * - * This example shows how to: - * 1. Initialize the S3 Encryption Client with KMS keyring - * 2. Encrypt and upload an object to S3 - * 3. Download and decrypt the object - * 4. Verify the roundtrip encryption/decryption - */ -public class Main { - - public static void main(String[] args) { - // Check command line arguments - if (args.length != 4) { - System.out.println("Usage: ./gradlew run --args=\" \""); - System.out.println("Example: ./gradlew run --args=\"avp-21638 s3ec-java-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\""); - System.exit(1); - } - - String bucketName = args[0]; - String objectKey = args[1]; - String kmsKeyId = args[2]; - String region = args[3]; - - System.out.println("=== S3 Encryption Client v3 Example (Java) ==="); - System.out.println("Bucket: " + bucketName); - System.out.println("Object Key: " + objectKey); - System.out.println("KMS Key ID: " + kmsKeyId); - System.out.println("Region: " + region); - System.out.println(); - - // Test data for encryption - String testData = "Hello, World! This is a test message for S3 encryption client v3 in Java."; - System.out.println("Original data: " + testData); - System.out.println("Data length: " + testData.length() + " bytes"); - System.out.println(); - - try { - System.out.println("--- Initialize S3 Encryption Client v3 ---"); - - // Create standard S3 client - S3Client s3Client = S3Client.builder() - .region(Region.of(region)) - .credentialsProvider(DefaultCredentialsProvider.create()) - .build(); - - // Create KMS client - KmsClient kmsClient = KmsClient.builder() - .region(Region.of(region)) - .credentialsProvider(DefaultCredentialsProvider.create()) - .build(); - - // Create KMS keyring - KmsKeyring keyring = KmsKeyring.builder() - .kmsClient(kmsClient) - .wrappingKeyId(kmsKeyId) - .build(); - - // Create Cryptographic Materials Manager - CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder() - .keyring(keyring) - .build(); - - // Create S3 Encryption Client v3 - S3EncryptionClient encryptionClient = S3EncryptionClient.builder() - .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) - .wrappedClient(s3Client) - .cryptoMaterialsManager(cmm) - .enableLegacyUnauthenticatedModes(true) - .enableLegacyWrappingAlgorithms(true) - .build(); - - System.out.println("Successfully initialized S3 Encryption Client v3"); - System.out.println("--- Encrypt and Upload Object to S3 ---"); - - // Add encryption context - Map encryptionContext = new HashMap<>(); - encryptionContext.put("purpose", "example"); - encryptionContext.put("version", "v3"); - encryptionContext.put("language", "java"); - - // Upload encrypted object using S3 Encryption Client - PutObjectRequest putRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(objectKey) - .build(); - - encryptionClient.putObject(putRequest, RequestBody.fromString(testData)); - - System.out.println("Successfully uploaded encrypted object to S3!"); - System.out.println(" Bucket: " + bucketName); - System.out.println(" Key: " + objectKey); - System.out.println(" Encryption Context: " + encryptionContext); - System.out.println(); - - System.out.println("--- Download and Decrypt Object from S3 ---"); - - // Download and decrypt object using S3 Encryption Client - GetObjectRequest getRequest = GetObjectRequest.builder() - .bucket(bucketName) - .key(objectKey) - .build(); - - String decryptedData = encryptionClient.getObjectAsBytes(getRequest) - .asString(StandardCharsets.UTF_8); - - System.out.println("Successfully downloaded and decrypted object from S3!"); - System.out.println(" Object size: " + decryptedData.length() + " bytes"); - System.out.println(" Decrypted data: " + decryptedData); - System.out.println(); - - System.out.println("--- Verify Roundtrip Success ---"); - - // Verify the roundtrip was successful - if (decryptedData.equals(testData)) { - System.out.println("SUCCESS: Roundtrip encryption/decryption completed successfully!"); - System.out.println(" Original data matches decrypted data"); - System.out.println(" Data integrity verified"); - } else { - System.out.println("ERROR: Roundtrip failed - data mismatch"); - System.out.println(" Original: " + testData); - System.out.println(" Decrypted: " + decryptedData); - System.exit(1); - } - - System.out.println(); - System.out.println("=== Example completed successfully! ==="); - - // Clean up clients - encryptionClient.close(); - s3Client.close(); - kmsClient.close(); - - } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } - } -} diff --git a/all-examples/java/v4/Makefile b/all-examples/java/v4/Makefile deleted file mode 100644 index 3390c5e4..00000000 --- a/all-examples/java/v4/Makefile +++ /dev/null @@ -1,75 +0,0 @@ -# Makefile for S3 Encryption Client Java v4 Example - -# Default target -.PHONY: all install clean run help s3ec-staging - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-java-v4 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install S3 Encryption Client library from source -s3ec-staging: - @echo "[JAVA V4] Installing S3 Encryption Client library from source..." - @cd s3ec-staging && mvn -B -ntp install -DskipTests - @echo "[JAVA V4] S3 Encryption Client library installed successfully!" - -# Install dependencies using Gradle -install: s3ec-staging - @echo "[JAVA V4] Installing Java dependencies..." - @chmod +x ./gradlew - @./gradlew build - @echo "[JAVA V4] Dependencies installed successfully!" - -# Clean Gradle artifacts -clean: - @echo "[JAVA V4] Cleaning Gradle artifacts..." - @./gradlew clean - @echo "[JAVA V4] Clean completed!" - -# Run the example with default arguments -run: install - @echo "[JAVA V4] Running S3 Encryption Client v4 Java example..." - @echo "[JAVA V4] Bucket: $(BUCKET_NAME)" - @echo "[JAVA V4] Object Key: $(OBJECT_KEY)" - @echo "[JAVA V4] KMS Key ID: $(KMS_KEY_ID)" - @echo "[JAVA V4] Region: $(AWS_REGION)" - @echo "" - @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @./gradlew run --args="$(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION)" - -# Show help -help: - @echo "S3 Encryption Client Java v4 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " s3ec-staging - Install S3 Encryption Client library from source" - @echo " install - Install Java dependencies using Gradle" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove Gradle artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Java 11+ installed on the system" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v4 library installed." - @echo " (Install by running: cd s3ec-staging && mvn install)" diff --git a/all-examples/java/v4/README.md b/all-examples/java/v4/README.md deleted file mode 100644 index deedec94..00000000 --- a/all-examples/java/v4/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# S3 Encryption Client Java v4 Example - -This example demonstrates how to use the Amazon S3 Encryption Client v4 for Java to perform client-side encryption and decryption of objects with **key commitment** enabled for enhanced security. - -## Prerequisites - -1. **Java**: Requires Java 11 or later -2. **Gradle**: The example project uses Gradle wrapper (included - `./gradlew`) -3. **Maven**: Required to install the S3 Encryption Client library from source -4. **AWS Credentials**: Configure your AWS credentials using one of the following methods: - - AWS CLI: `aws configure` - - Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - - IAM roles (for EC2 instances) -5. **KMS Key**: You'll need a KMS key ID or ARN. You can use the default example key: `arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01` -6. **S3 Bucket**: An existing S3 bucket where you have read/write permissions - -## Setup - -Install dependencies and build (this automatically installs the S3 Encryption Client library from source): -```bash -make install -``` - -Or manually: -```bash -cd s3ec-staging && mvn clean install && cd .. -./gradlew build -``` - -**Note**: This example uses a local library installed in Maven local repository via the symbolic link `s3ec-staging`. - -## Usage - -### Using Make (Recommended) - -Run the example with default parameters: -```bash -make run -``` - -Run with custom parameters: -```bash -make run BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -``` - -### Manual Usage - -Run the example with the following command: - -```bash -./gradlew run --args=" " -``` - -### Example: - -```bash -./gradlew run --args="my-test-bucket s3ec-java-v4-test arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" diff --git a/all-examples/java/v4/build.gradle.kts b/all-examples/java/v4/build.gradle.kts deleted file mode 100644 index 56b02b97..00000000 --- a/all-examples/java/v4/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - java - application -} - -group = "software.amazon.encryption.s3.example" -version = "1.0.0" - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - // AWS SDK v2 dependencies - implementation(platform("software.amazon.awssdk:bom:2.20.0")) - implementation("software.amazon.awssdk:s3") - implementation("software.amazon.awssdk:kms") - implementation("software.amazon.awssdk:auth") - - // S3 Encryption Client v4 from local Maven repository - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-add-kc") -} - -application { - mainClass.set("software.amazon.encryption.s3.example.Main") -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks.jar { - manifest { - attributes["Main-Class"] = "software.amazon.encryption.s3.example.Main" - } - - // Create a fat jar with all dependencies - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveBaseName.set("s3ec-java-v4-example") -} - -tasks.named("run") { - standardInput = System.`in` -} diff --git a/all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar b/all-examples/java/v4/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e6441136f3d4ba8a0da8d277868979cfbc8ad796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/all-examples/java/v4/gradlew.bat b/all-examples/java/v4/gradlew.bat deleted file mode 100644 index 25da30db..00000000 --- a/all-examples/java/v4/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/all-examples/java/v4/s3ec-staging b/all-examples/java/v4/s3ec-staging deleted file mode 120000 index 970f5de7..00000000 --- a/all-examples/java/v4/s3ec-staging +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/java-v4-server/s3ec-staging \ No newline at end of file diff --git a/all-examples/java/v4/settings.gradle.kts b/all-examples/java/v4/settings.gradle.kts deleted file mode 100644 index e20b5a12..00000000 --- a/all-examples/java/v4/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "s3ec-java-v4-example" diff --git a/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java b/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java deleted file mode 100644 index 4ec8fd9b..00000000 --- a/all-examples/java/v4/src/main/java/software/amazon/encryption/s3/example/Main.java +++ /dev/null @@ -1,160 +0,0 @@ -package software.amazon.encryption.s3.example; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.kms.KmsClient; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.encryption.s3.S3EncryptionClient; -import software.amazon.encryption.s3.materials.CryptographicMaterialsManager; -import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; -import software.amazon.encryption.s3.materials.KmsKeyring; - -/** - * Example demonstrating the use of Amazon S3 Encryption Client v4 for Java. - * - * This example shows how to: - * 1. Initialize the S3 Encryption Client with KMS keyring and key commitment - * 2. Encrypt and upload an object to S3 with key commitment - * 3. Download and decrypt the object - * 4. Verify the roundtrip encryption/decryption - */ -public class Main { - - public static void main(String[] args) { - // Check command line arguments - if (args.length != 4) { - System.out.println("Usage: ./gradlew run --args=\" \""); - System.out.println("Example: ./gradlew run --args=\"avp-21638 s3ec-java-v4 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\""); - System.exit(1); - } - - String bucketName = args[0]; - String objectKey = args[1]; - String kmsKeyId = args[2]; - String region = args[3]; - - System.out.println("=== S3 Encryption Client v4 Example (Java) ==="); - System.out.println("Bucket: " + bucketName); - System.out.println("Object Key: " + objectKey); - System.out.println("KMS Key ID: " + kmsKeyId); - System.out.println("Region: " + region); - System.out.println(); - - // Test data for encryption - String testData = "Hello, World! This is a test message for S3 encryption client v4 in Java."; - System.out.println("Original data: " + testData); - System.out.println("Data length: " + testData.length() + " bytes"); - System.out.println(); - - try { - System.out.println("--- Initialize S3 Encryption Client v4 ---"); - - // Create standard S3 client - S3Client s3Client = S3Client.builder() - .region(Region.of(region)) - .credentialsProvider(DefaultCredentialsProvider.create()) - .build(); - - // Create KMS client - KmsClient kmsClient = KmsClient.builder() - .region(Region.of(region)) - .credentialsProvider(DefaultCredentialsProvider.create()) - .build(); - - // Create KMS keyring - KmsKeyring keyring = KmsKeyring.builder() - .kmsClient(kmsClient) - .wrappingKeyId(kmsKeyId) - .build(); - - // Create Cryptographic Materials Manager - CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder() - .keyring(keyring) - .build(); - - // Create S3 Encryption Client v4 with key commitment enabled (Defaults to REQUIRE_ENCRYPT_REQUIRE_DECRYPT) - S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4() - .wrappedClient(s3Client) - .cryptoMaterialsManager(cmm) - .enableLegacyUnauthenticatedModes(false) - .enableLegacyWrappingAlgorithms(false) - .build(); - - System.out.println("Successfully initialized S3 Encryption Client v4"); - System.out.println("Key commitment: ENABLED"); - System.out.println("--- Encrypt and Upload Object to S3 ---"); - - // Add encryption context - Map encryptionContext = new HashMap<>(); - encryptionContext.put("purpose", "example"); - encryptionContext.put("version", "v4"); - encryptionContext.put("language", "java"); - - // Upload encrypted object using S3 Encryption Client - PutObjectRequest putRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(objectKey) - .build(); - - encryptionClient.putObject(putRequest, RequestBody.fromString(testData)); - - System.out.println("Successfully uploaded encrypted object to S3!"); - System.out.println(" Bucket: " + bucketName); - System.out.println(" Key: " + objectKey); - System.out.println(" Encryption Context: " + encryptionContext); - System.out.println(" Key Commitment: ENABLED"); - System.out.println(); - - System.out.println("--- Download and Decrypt Object from S3 ---"); - - // Download and decrypt object using S3 Encryption Client - GetObjectRequest getRequest = GetObjectRequest.builder() - .bucket(bucketName) - .key(objectKey) - .build(); - - String decryptedData = encryptionClient.getObjectAsBytes(getRequest) - .asString(StandardCharsets.UTF_8); - - System.out.println("Successfully downloaded and decrypted object from S3!"); - System.out.println(" Object size: " + decryptedData.length() + " bytes"); - System.out.println(" Decrypted data: " + decryptedData); - System.out.println(); - - System.out.println("--- Verify Roundtrip Success ---"); - - // Verify the roundtrip was successful - if (decryptedData.equals(testData)) { - System.out.println("SUCCESS: Roundtrip encryption/decryption completed successfully!"); - System.out.println(" Original data matches decrypted data"); - System.out.println(" Data integrity verified"); - System.out.println(" Key commitment verified"); - } else { - System.out.println("ERROR: Roundtrip failed - data mismatch"); - System.out.println(" Original: " + testData); - System.out.println(" Decrypted: " + decryptedData); - System.exit(1); - } - - System.out.println(); - System.out.println("=== Example completed successfully! ==="); - - // Clean up clients - encryptionClient.close(); - s3Client.close(); - kmsClient.close(); - - } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } - } -} diff --git a/all-examples/net/.gitignore b/all-examples/net/.gitignore deleted file mode 100644 index c6a52ab1..00000000 --- a/all-examples/net/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Build results -bin/ -obj/ - -# User-specific files -*.user -*.suo -*.userosscache -*.sln.docstates - -# Visual Studio -.vs/ - -# Rider -.idea/ - -# NuGet packages -packages/ -*.nupkg diff --git a/all-examples/net/v3/Makefile b/all-examples/net/v3/Makefile deleted file mode 100644 index c375acc7..00000000 --- a/all-examples/net/v3/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -# Makefile for S3 Encryption Client .NET v3 Example - -# Default target -.PHONY: all install clean run help - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-dotnet-v3 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using .NET modules -install: - @echo "[NET V3] Installing .NET dependencies..." - dotnet restore - @echo "[NET V3] Dependencies installed successfully!" - -# Clean .NET artifacts -clean: - @echo "[NET V3] Cleaning .NET artifacts..." - dotnet clean - @echo "[NET V3] Clean completed!" - -# Run the example with default arguments -run: install - @echo "[NET V3] Running S3 Encryption Client v3 .NET example..." - @echo "[NET V3] Bucket: $(BUCKET_NAME)" - @echo "[NET V3] Object Key: $(OBJECT_KEY)" - @echo "[NET V3] KMS Key ID: $(KMS_KEY_ID)" - @echo "[NET V3] Region: $(AWS_REGION)" - @echo "" - @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client .NET v3 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install .NET dependencies using .NET modules" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove .NET artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Supported .NET framework installed on the system. See https://www.nuget.org/packages/Amazon.Extensions.S3.Encryption for supported one." - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v3 .NET SDK (included in s3ec-v3-local)" \ No newline at end of file diff --git a/all-examples/net/v3/Program.cs b/all-examples/net/v3/Program.cs deleted file mode 100644 index 6c1336f6..00000000 --- a/all-examples/net/v3/Program.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Amazon; -using Amazon.Extensions.S3.Encryption; -using Amazon.Extensions.S3.Encryption.Primitives; -using Amazon.S3; -using Amazon.S3.Model; - -using Amazon.Extensions.S3.Encryption; -using Amazon.Extensions.S3.Encryption.Primitives; -using Amazon.S3; -using Amazon.S3.Model; - -namespace S3EncryptionClientV3Example -{ - class Program - { - static async Task Main(string[] args) - { - if (args.Length != 4) - { - Console.WriteLine("[NET V3] Usage: dotnet run "); - Environment.Exit(1); - } - - var (bucketName, objectKey, kmsKeyId, region) = (args[0], args[1], args[2], args[3]); - var testData = "Hello, World! This is a test message for S3 encryption client v3 in .NET."; - - Console.WriteLine("=== S3 Encryption Client v3 Example (.NET) ==="); - - try - { - var s3Client = CreateS3ECWithKms(kmsKeyId, region); - - await s3Client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = objectKey, - ContentBody = testData - }); - - var getResponse = await s3Client.GetObjectAsync(bucketName, objectKey); - using var reader = new StreamReader(getResponse.ResponseStream); - var decryptedData = await reader.ReadToEndAsync(); - - if (decryptedData != testData) - { - Console.WriteLine("[NET V3] ERROR: Roundtrip failed - data mismatch"); - Environment.Exit(1); - } - - Console.WriteLine("[NET V3] SUCCESS: Roundtrip encryption/decryption completed successfully!"); - } - catch (Exception ex) - { - Console.WriteLine($"[NET V3] Error: {ex.Message}"); - Environment.Exit(1); - } - } - - private static AmazonS3Client CreateS3ECWithKms(string kmsKeyId, string region) - { - var encryptionContextPerClient = new Dictionary - { - ["purpose"] = "example", - ["version"] = "v3", - ["language"] = "dotnet" - }; - - var encryptionMaterial = new EncryptionMaterialsV2(kmsKeyId, KmsType.KmsContext, encryptionContextPerClient); - var configuration = new AmazonS3CryptoConfigurationV2(SecurityProfile.V2, CommitmentPolicy.ForbidEncryptAllowDecrypt, ContentEncryptionAlgorithm.AesGcm) - { - RegionEndpoint = RegionEndpoint.GetBySystemName(region) - }; - return new AmazonS3EncryptionClientV2(configuration, encryptionMaterial); - } - } -} diff --git a/all-examples/net/v3/s3ec-v3-local b/all-examples/net/v3/s3ec-v3-local deleted file mode 120000 index a2d8df44..00000000 --- a/all-examples/net/v3/s3ec-v3-local +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/net-v3-transition-server/s3ec-v3-transition-branch \ No newline at end of file diff --git a/all-examples/net/v3/v3.csproj b/all-examples/net/v3/v3.csproj deleted file mode 100644 index cfb74fad..00000000 --- a/all-examples/net/v3/v3.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net8.0 - enable - enable - false - - - - - - - - - - - diff --git a/all-examples/net/v4/Makefile b/all-examples/net/v4/Makefile deleted file mode 100644 index f45fbdfd..00000000 --- a/all-examples/net/v4/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -# Makefile for S3 Encryption Client .NET v4 Example - -# Default target -.PHONY: all install clean run help - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-dotnet-v4 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using .NET modules -install: - @echo "[NET V4] Installing .NET dependencies..." - dotnet restore - @echo "[NET V4] Dependencies installed successfully!" - -# Clean .NET artifacts -clean: - @echo "[NET V4] Cleaning .NET artifacts..." - dotnet clean - @echo "[NET V4] Clean completed!" - -# Run the example with default arguments -run: install - @echo "[NET V4] Running S3 Encryption Client v4 .NET example..." - @echo "[NET V4] Bucket: $(BUCKET_NAME)" - @echo "[NET V4] Object Key: $(OBJECT_KEY)" - @echo "[NET V4] KMS Key ID: $(KMS_KEY_ID)" - @echo "[NET V4] Region: $(AWS_REGION)" - @echo "" - @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @dotnet run -- $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client .NET v4 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install .NET dependencies using .NET modules" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove .NET artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Supported .NET framework installed on the system. See https://www.nuget.org/packages/Amazon.Extensions.S3.Encryption for supported one." - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v4 .NET SDK (included in s3ec-v4-local)" \ No newline at end of file diff --git a/all-examples/net/v4/Program.cs b/all-examples/net/v4/Program.cs deleted file mode 100644 index a8c799a6..00000000 --- a/all-examples/net/v4/Program.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Amazon; -using Amazon.Extensions.S3.Encryption; -using Amazon.Extensions.S3.Encryption.Primitives; -using Amazon.S3; -using Amazon.S3.Model; - -using Amazon.Extensions.S3.Encryption; -using Amazon.Extensions.S3.Encryption.Primitives; -using Amazon.S3; -using Amazon.S3.Model; - -namespace S3EncryptionClientV4Example -{ - class Program - { - static async Task Main(string[] args) - { - if (args.Length != 4) - { - Console.WriteLine("[NET V4] Usage: dotnet run "); - Environment.Exit(1); - } - - var (bucketName, objectKey, kmsKeyId, region) = (args[0], args[1], args[2], args[3]); - var testData = "Hello, World! This is a test message for S3 encryption client v4 in .NET."; - - Console.WriteLine("=== S3 Encryption Client v4 Example (.NET) ==="); - - try - { - var s3Client = CreateS3ECWithKms(kmsKeyId, region); - - await s3Client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = objectKey, - ContentBody = testData - }); - - var getResponse = await s3Client.GetObjectAsync(bucketName, objectKey); - using var reader = new StreamReader(getResponse.ResponseStream); - var decryptedData = await reader.ReadToEndAsync(); - - if (decryptedData != testData) - { - Console.WriteLine("[NET V4] ERROR: Roundtrip failed - data mismatch"); - Environment.Exit(1); - } - - Console.WriteLine("[NET V4] SUCCESS: Roundtrip encryption/decryption completed successfully!"); - } - catch (Exception ex) - { - Console.WriteLine($"[NET V4] Error: {ex.Message}"); - Environment.Exit(1); - } - } - - private static AmazonS3Client CreateS3ECWithKms(string kmsKeyId, string region) - { - var encryptionContextPerClient = new Dictionary - { - ["purpose"] = "example", - ["version"] = "v4", - ["language"] = "dotnet" - }; - - var encryptionMaterial = new EncryptionMaterialsV4(kmsKeyId, KmsType.KmsContext, encryptionContextPerClient); - var configuration = new AmazonS3CryptoConfigurationV4(SecurityProfile.V4, CommitmentPolicy.RequireEncryptRequireDecrypt, ContentEncryptionAlgorithm.AesGcmWithCommitment) - { - RegionEndpoint = RegionEndpoint.GetBySystemName(region) - }; - return new AmazonS3EncryptionClientV4(configuration, encryptionMaterial); - } - } -} diff --git a/all-examples/net/v4/s3ec-v4-local b/all-examples/net/v4/s3ec-v4-local deleted file mode 120000 index 371b1a90..00000000 --- a/all-examples/net/v4/s3ec-v4-local +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/net-v4-server/s3ec-net-v4-improved \ No newline at end of file diff --git a/all-examples/net/v4/v4.csproj b/all-examples/net/v4/v4.csproj deleted file mode 100644 index 6d223a92..00000000 --- a/all-examples/net/v4/v4.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net8.0 - enable - enable - false - - - - - - - - - - - diff --git a/all-examples/php/v2/.gitignore b/all-examples/php/v2/.gitignore deleted file mode 100644 index 07108589..00000000 --- a/all-examples/php/v2/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -vendor/* -cookies.txt -server.pid -composer.lock \ No newline at end of file diff --git a/all-examples/php/v2/Makefile b/all-examples/php/v2/Makefile deleted file mode 100644 index 0747d7b8..00000000 --- a/all-examples/php/v2/Makefile +++ /dev/null @@ -1,71 +0,0 @@ -# Makefile for S3 Encryption Client PHP v2 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.php - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-php-v2 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Composer -install: - @echo "Installing PHP dependencies..." - @composer install --no-dev --optimize-autoloader - @echo "Dependencies installed successfully!" - -# Clean composer artifacts -clean: - @echo "Cleaning composer artifacts..." - @rm -rf vendor/ - @rm -f composer.lock - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v2 PHP example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client PHP v2 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install PHP dependencies using Composer" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove composer artifacts (vendor/, composer.lock)" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - PHP 7.4+ installed on the system" - @echo " - Composer installed (https://getcomposer.org/)" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v2 PHP SDK (included in local-php-sdk)" diff --git a/all-examples/php/v2/composer.json b/all-examples/php/v2/composer.json deleted file mode 100644 index 914bf900..00000000 --- a/all-examples/php/v2/composer.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "aws/s3ec-php-v2-example", - "description": "PHP v2 example for Amazon S3 Encryption Client", - "type": "project", - "license": "Apache-2.0", - "repositories": [ - { - "type": "path", - "url": "./local-php-sdk", - "options": { - "symlink": true - } - } - ], - "require": { - "php": ">=7.4", - "aws/aws-sdk-php": "@dev", - "ramsey/uuid": "^4.9" - }, - "autoload": { - "psr-4": { - "AWS\\S3EC\\Example\\": "src/" - } - }, - "config": { - "optimize-autoloader": true, - "platform": { - "php": "8.1" - } - }, - "minimum-stability": "dev", - "prefer-stable": true -} diff --git a/all-examples/php/v2/local-php-sdk b/all-examples/php/v2/local-php-sdk deleted file mode 120000 index 04ad0cf7..00000000 --- a/all-examples/php/v2/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/php-v2-transition-server/local-php-sdk \ No newline at end of file diff --git a/all-examples/php/v2/main.php b/all-examples/php/v2/main.php deleted file mode 100755 index df2fdd32..00000000 --- a/all-examples/php/v2/main.php +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env php - \n"; - echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v2 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; - exit(1); - } - - $bucketName = $GLOBALS['argv'][1]; - $objectKey = $GLOBALS['argv'][2]; - $kmsKeyId = $GLOBALS['argv'][3]; - $region = $GLOBALS['argv'][4]; - - echo "=== S3 Encryption Client v2 Example (PHP) ===\n"; - echo "Bucket: {$bucketName}\n"; - echo "Object Key: {$objectKey}\n"; - echo "KMS Key ID: {$kmsKeyId}\n"; - echo "Region: {$region}\n"; - echo "\n"; - - try { - // Test data for encryption - $testData = "Hello, World! This is a test message for S3 encryption client v2 in PHP."; - echo "Original data: {$testData}\n"; - echo "Data length: " . strlen($testData) . " bytes\n"; - echo "\n"; - - echo "--- Initialize S3 Encryption Client v2 ---\n"; - - // Create regular S3 client - $s3Client = new S3Client([ - 'region' => $region, - 'version' => 'latest' - ]); - - // Create KMS client - $kmsClient = new KmsClient([ - 'region' => $region, - 'version' => 'latest' - ]); - - // Create S3 Encryption Client v2 - $encryptionClient = new S3EncryptionClientV2($s3Client); - $materialsProvider = new KmsMaterialsProviderV2($kmsClient, $kmsKeyId); - - echo "Successfully initialized S3 Encryption Client v2\n"; - echo "--- Encrypt and Upload Object to S3 ---\n"; - - // Add encryption context - $encryptionContext = [ - 'purpose' => 'example', - 'version' => 'v2', - 'language' => 'php' - ]; - - $cipherOptions = [ - 'Cipher' => 'gcm', - 'KeySize' => 256, - ]; - - // Upload encrypted object using S3 Encryption Client - $putResponse = $encryptionClient->putObject([ - 'Bucket' => $bucketName, - 'Key' => $objectKey, - 'Body' => $testData, - '@MaterialsProvider' => $materialsProvider, - '@KmsEncryptionContext' => $encryptionContext, - '@CipherOptions' => $cipherOptions, - ]); - - echo "Successfully uploaded encrypted object to S3!\n"; - echo " Bucket: {$bucketName}\n"; - echo " Key: {$objectKey}\n"; - echo " Encryption Context: " . json_encode($encryptionContext) . "\n"; - echo "\n"; - - echo "--- Download and Decrypt Object from S3 ---\n"; - - // Download and decrypt object using S3 Encryption Client - $getResponse = $encryptionClient->getObject([ - 'Bucket' => $bucketName, - 'Key' => $objectKey, - '@KmsEncryptionContext' => $encryptionContext, - '@MaterialsProvider' => $materialsProvider, - '@CommitmentPolicy' => 'FORBID_ENCRYPT_ALLOW_DECRYPT', - '@SecurityProfile' => 'V2' - ]); - - // Read the decrypted data - $decryptedData = (string) $getResponse['Body']; - - echo "Successfully downloaded and decrypted object from S3!\n"; - echo " Object size: " . strlen($decryptedData) . " bytes\n"; - echo " Decrypted data: {$decryptedData}\n"; - echo "\n"; - - echo "--- Verify Roundtrip Success ---\n"; - - // Verify the roundtrip was successful - if ($decryptedData === $testData) { - echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; - echo " Original data matches decrypted data\n"; - echo " Data integrity verified\n"; - } else { - echo "ERROR: Roundtrip failed - data mismatch\n"; - echo " Original: {$testData}\n"; - echo " Decrypted: {$decryptedData}\n"; - exit(1); - } - - // Optionally Delete the Object - // echo "--- Cleanup ---\n"; - // Clean up the test object using regular S3 client - // $s3Client->deleteObject([ - // 'Bucket' => $bucketName, - // 'Key' => $objectKey - // ]); - // echo "Test object deleted from S3\n"; - - echo "\n"; - echo "=== Example completed successfully! ===\n"; - - } catch (AwsException $e) { - $errorCode = $e->getAwsErrorCode(); - $errorMessage = $e->getMessage(); - - if (strpos($errorCode, 'NoSuchBucket') !== false) { - echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorCode, 'NotFoundException') !== false) { - echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorMessage, 'encryption') !== false) { - echo "S3 Encryption Error: {$errorMessage}\n"; - } else { - echo "AWS Service Error: {$errorMessage}\n"; - echo " Error Code: {$errorCode}\n"; - } - exit(1); - } catch (Exception $e) { - echo "Unexpected error: {$e->getMessage()}\n"; - echo " File: {$e->getFile()}:{$e->getLine()}\n"; - exit(1); - } -} - -// Run the main function if this script is executed directly -if (php_sapi_name() === 'cli' && isset($GLOBALS['argv']) && basename($GLOBALS['argv'][0]) === basename(__FILE__)) { - main(); -} diff --git a/all-examples/php/v3/.gitignore b/all-examples/php/v3/.gitignore deleted file mode 100644 index 07108589..00000000 --- a/all-examples/php/v3/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -vendor/* -cookies.txt -server.pid -composer.lock \ No newline at end of file diff --git a/all-examples/php/v3/Makefile b/all-examples/php/v3/Makefile deleted file mode 100644 index 328a901a..00000000 --- a/all-examples/php/v3/Makefile +++ /dev/null @@ -1,71 +0,0 @@ -# Makefile for S3 Encryption Client PHP v3 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.php - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-php-v3 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Composer -install: - @echo "Installing PHP dependencies..." - @composer install --no-dev --optimize-autoloader - @echo "Dependencies installed successfully!" - -# Clean composer artifacts -clean: - @echo "Cleaning composer artifacts..." - @rm -rf vendor/ - @rm -f composer.lock - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v3 PHP example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @php $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client PHP v3 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install PHP dependencies using Composer" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove composer artifacts (vendor/, composer.lock)" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - PHP 7.4+ installed on the system" - @echo " - Composer installed (https://getcomposer.org/)" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v3 PHP SDK (included in local-php-sdk)" diff --git a/all-examples/php/v3/composer.json b/all-examples/php/v3/composer.json deleted file mode 100644 index 2ad2469a..00000000 --- a/all-examples/php/v3/composer.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "aws/s3ec-php-v3-example", - "description": "PHP v3 example for Amazon S3 Encryption Client", - "type": "project", - "license": "Apache-2.0", - "repositories": [ - { - "type": "path", - "url": "./local-php-sdk", - "options": { - "symlink": true - } - } - ], - "require": { - "php": ">=7.4", - "aws/aws-sdk-php": "@dev", - "ramsey/uuid": "^4.9" - }, - "autoload": { - "psr-4": { - "AWS\\S3EC\\Example\\": "src/" - } - }, - "config": { - "optimize-autoloader": true, - "platform": { - "php": "8.1" - } - }, - "minimum-stability": "dev", - "prefer-stable": true -} diff --git a/all-examples/php/v3/local-php-sdk b/all-examples/php/v3/local-php-sdk deleted file mode 120000 index 3b9b4cd7..00000000 --- a/all-examples/php/v3/local-php-sdk +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/php-v3-server/local-php-sdk \ No newline at end of file diff --git a/all-examples/php/v3/main.php b/all-examples/php/v3/main.php deleted file mode 100755 index 949a0ce1..00000000 --- a/all-examples/php/v3/main.php +++ /dev/null @@ -1,508 +0,0 @@ -#!/usr/bin/env php - \n"; - echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; - exit(1); - } - - $bucketName = $GLOBALS['argv'][1]; - $objectKey = $GLOBALS['argv'][2]; - $kmsKeyId = $GLOBALS['argv'][3]; - $region = $GLOBALS['argv'][4]; - - echo "=== S3 Encryption Client v3 Example (PHP) ===\n"; - echo "Bucket: {$bucketName}\n"; - echo "Object Key: {$objectKey}\n"; - echo "KMS Key ID: {$kmsKeyId}\n"; - echo "Region: {$region}\n"; - echo "\n"; - - try { - // Test data for encryption - $testData = "Hello, World! This is a test message for S3 encryption client v3 in PHP."; - echo "Original data: {$testData}\n"; - echo "Data length: " . strlen($testData) . " bytes\n"; - echo "\n"; - - echo "--- Initialize S3 Encryption Client v3 ---\n"; - - // Create regular S3 client - $s3Client = new S3Client([ - 'region' => $region, - 'version' => 'latest' - ]); - - // Create KMS client - $kmsClient = new KmsClient([ - 'region' => $region, - 'version' => 'latest' - ]); - - // Create S3 Encryption Client v3 - // Create S3 Encryption Client v2 - $encryptionClient = new S3EncryptionClientV3($s3Client); - $materialsProvider = new KmsMaterialsProviderV3($kmsClient, $kmsKeyId); - - echo "Successfully initialized S3 Encryption Client v3\n"; - echo "--- Encrypt and Upload Object to S3 ---\n"; - - // Add encryption context - $encryptionContext = [ - 'purpose' => 'example', - 'version' => 'v3', - 'language' => 'php' - ]; - - $cipherOptions = [ - 'Cipher' => 'gcm', - 'KeySize' => 256, - ]; - - // Upload encrypted object using S3 Encryption Client - $putResponse = $encryptionClient->putObject([ - 'Bucket' => $bucketName, - 'Key' => $objectKey, - 'Body' => $testData, - '@MaterialsProvider' => $materialsProvider, - '@KmsEncryptionContext' => $encryptionContext, - '@CommitmentPolicy' => "REQUIRE_ENCRYPT_REQUIRE_DECRYPT", - '@CipherOptions' => $cipherOptions, - ]); - - echo "Successfully uploaded encrypted object to S3!\n"; - echo " Bucket: {$bucketName}\n"; - echo " Key: {$objectKey}\n"; - echo " Encryption Context: " . json_encode($encryptionContext) . "\n"; - echo "\n"; - - echo "--- Download and Decrypt Object from S3 ---\n"; - - // Download and decrypt object using S3 Encryption Client - $getResponse = $encryptionClient->getObject([ - 'Bucket' => $bucketName, - 'Key' => $objectKey, - '@KmsEncryptionContext' => $encryptionContext, - '@MaterialsProvider' => $materialsProvider, - '@CommitmentPolicy' => "REQUIRE_ENCRYPT_REQUIRE_DECRYPT", - '@SecurityProfile' => 'V3' - ]); - - // Read the decrypted data - $decryptedData = (string) $getResponse['Body']; - - echo "Successfully downloaded and decrypted object from S3!\n"; - echo " Object size: " . strlen($decryptedData) . " bytes\n"; - echo " Decrypted data: {$decryptedData}\n"; - echo "\n"; - - echo "--- Verify Roundtrip Success ---\n"; - - // Verify the roundtrip was successful - if ($decryptedData === $testData) { - echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; - echo " Original data matches decrypted data\n"; - echo " Data integrity verified\n"; - } else { - echo "ERROR: Roundtrip failed - data mismatch\n"; - echo " Original: {$testData}\n"; - echo " Decrypted: {$decryptedData}\n"; - exit(1); - } - - // Optionally Delete the Object - // echo "--- Cleanup ---\n"; - // Clean up the test object using regular S3 client - // $s3Client->deleteObject([ - // 'Bucket' => $bucketName, - // 'Key' => $objectKey - // ]); - // echo "Test object deleted from S3\n"; - - echo "\n"; - echo "=== Example completed successfully! ===\n"; - - } catch (AwsException $e) { - $errorCode = $e->getAwsErrorCode(); - $errorMessage = $e->getMessage(); - - if (strpos($errorCode, 'NoSuchBucket') !== false) { - echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorCode, 'NotFoundException') !== false) { - echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorMessage, 'encryption') !== false) { - echo "S3 Encryption Error: {$errorMessage}\n"; - } else { - echo "AWS Service Error: {$errorMessage}\n"; - echo " Error Code: {$errorCode}\n"; - } - exit(1); - } catch (Exception $e) { - echo "Unexpected error: {$e->getMessage()}\n"; - echo " File: {$e->getFile()}:{$e->getLine()}\n"; - exit(1); - } -} - -function testMigration(): void { - if (count($GLOBALS['argv']) !== 5) { - echo "Usage: {$GLOBALS['argv'][0]} \n"; - echo "Example: {$GLOBALS['argv'][0]} avp-21638 s3ec-php-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2\n"; - exit(1); - } - - $bucketName = $GLOBALS['argv'][1]; - $objectKey = $GLOBALS['argv'][2]; - $kmsKeyId = $GLOBALS['argv'][3]; - $region = $GLOBALS['argv'][4]; - - echo "=== S3 Encryption Client Pre-migration (V2) Example ===\n"; - echo "Bucket: {$bucketName}\n"; - echo "Object Key: {$objectKey}\n"; - echo "KMS Key ID: {$kmsKeyId}\n"; - echo "Region: {$region}\n"; - echo "\n"; - - try { - $testData = "Hello, World! This is a test message for S3 encryption client Pre-migration (V2) in PHP."; - echo "Original data: {$testData}\n"; - echo "Data length: " . strlen($testData) . " bytes\n"; - echo "\n"; - - $v2EncryptionClient = new S3EncryptionClientV2( - new S3Client([ - 'region' => $region, - 'version' => 'latest', - ]) - ); - - $materialsProviderV2 = new KmsMaterialsProviderV2( - new KmsClient([ - 'region' => $region, - 'version' => 'latest', - ]), - $kmsKeyId - ); - - $cipherOptions = [ - 'Cipher' => 'gcm', - 'KeySize' => 256, - ]; - - $v2EncryptionClient->putObject([ - '@MaterialsProvider' => $materialsProviderV2, - '@CipherOptions' => $cipherOptions, - '@KmsEncryptionContext' => ['context-key' => 'context-value'], - 'Bucket' => $bucketName, - 'Key' => $objectKey, - 'Body' => $testData, - ]); - - $getResponse = $v2EncryptionClient->getObject([ - '@KmsAllowDecryptWithAnyCmk' => true, - '@SecurityProfile' => 'V2_AND_LEGACY', - '@CommitmentPolicy' => 'FORBID_ENCRYPT_ALLOW_DECRYPT', - '@MaterialsProvider' => $materialsProviderV2, - '@CipherOptions' => $cipherOptions, - 'Bucket' => $bucketName, - 'Key' => $objectKey, - ]); - - // Read the decrypted data - $decryptedData = (string) $getResponse['Body']; - - echo "Successfully downloaded and decrypted object from S3!\n"; - echo " Object size: " . strlen($decryptedData) . " bytes\n"; - echo " Decrypted data: {$decryptedData}\n"; - echo "\n"; - - echo "--- Verify Roundtrip Success ---\n"; - - // Verify the roundtrip was successful - if ($decryptedData === $testData) { - echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; - echo " Original data matches decrypted data\n"; - echo " Data integrity verified\n"; - } else { - echo "ERROR: Roundtrip failed - data mismatch\n"; - echo " Original: {$testData}\n"; - echo " Decrypted: {$decryptedData}\n"; - exit(1); - } - - // Optionally Delete the Object - // echo "--- Cleanup ---\n"; - // Clean up the test object using regular S3 client - // $s3Client->deleteObject([ - // 'Bucket' => $bucketName, - // 'Key' => $objectKey - // ]); - // echo "Test object deleted from S3\n"; - - echo "\n"; - echo "=== Example completed successfully! ===\n"; - - } catch (AwsException $e) { - $errorCode = $e->getAwsErrorCode(); - $errorMessage = $e->getMessage(); - - if (strpos($errorCode, 'NoSuchBucket') !== false) { - echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorCode, 'NotFoundException') !== false) { - echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorMessage, 'encryption') !== false) { - echo "S3 Encryption Error: {$errorMessage}\n"; - } else { - echo "AWS Service Error: {$errorMessage}\n"; - echo " Error Code: {$errorCode}\n"; - } - exit(1); - } catch (Exception $e) { - echo "Unexpected error: {$e->getMessage()}\n"; - echo " File: {$e->getFile()}:{$e->getLine()}\n"; - exit(1); - } - - echo "=== S3 Encryption Client during migration (V3 with backward compatibility) Example ===\n"; - echo "Bucket: {$bucketName}\n"; - echo "Object Key: {$objectKey}\n"; - echo "KMS Key ID: {$kmsKeyId}\n"; - echo "Region: {$region}\n"; - echo "\n"; - - try { - $testData = "Hello, World! This is a test message for S3 encryption client during migration (V3 with backward compatibility) in PHP."; - echo "Original data: {$testData}\n"; - echo "Data length: " . strlen($testData) . " bytes\n"; - echo "\n"; - - $v3EncryptionClient = new S3EncryptionClientV3( - new S3Client([ - 'region' => $region, - 'version' => 'latest', - ]) - ); - - $materialsProviderV3 = new KmsMaterialsProviderV3( - new KmsClient([ - 'region' => $region, - 'version' => 'latest', - ]), - $kmsKeyId - ); - - $cipherOptions = [ - 'Cipher' => 'gcm', - 'KeySize' => 256, - ]; - - $v3EncryptionClient->putObject([ - '@MaterialsProvider' => $materialsProviderV3, - '@CipherOptions' => $cipherOptions, - '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_ALLOW_DECRYPT', - '@KmsEncryptionContext' => ['context-key' => 'context-value'], - 'Bucket' => $bucketName, - 'Key' => $objectKey, - 'Body' => $testData, - ]); - - $getResponse = $v3EncryptionClient->getObject([ - '@KmsAllowDecryptWithAnyCmk' => true, - '@SecurityProfile' => 'V3_AND_LEGACY', - '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_ALLOW_DECRYPT', - '@MaterialsProvider' => $materialsProviderV3, - '@CipherOptions' => $cipherOptions, - 'Bucket' => $bucketName, - 'Key' => $objectKey, - ]); - - // Read the decrypted data - $decryptedData = (string) $getResponse['Body']; - - echo "Successfully downloaded and decrypted object from S3!\n"; - echo " Object size: " . strlen($decryptedData) . " bytes\n"; - echo " Decrypted data: {$decryptedData}\n"; - echo "\n"; - - echo "--- Verify Roundtrip Success ---\n"; - - // Verify the roundtrip was successful - if ($decryptedData === $testData) { - echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; - echo " Original data matches decrypted data\n"; - echo " Data integrity verified\n"; - } else { - echo "ERROR: Roundtrip failed - data mismatch\n"; - echo " Original: {$testData}\n"; - echo " Decrypted: {$decryptedData}\n"; - exit(1); - } - - // Optionally Delete the Object - // echo "--- Cleanup ---\n"; - // Clean up the test object using regular S3 client - // $s3Client->deleteObject([ - // 'Bucket' => $bucketName, - // 'Key' => $objectKey - // ]); - // echo "Test object deleted from S3\n"; - - echo "\n"; - echo "=== Example completed successfully! ===\n"; - - } catch (AwsException $e) { - $errorCode = $e->getAwsErrorCode(); - $errorMessage = $e->getMessage(); - - if (strpos($errorCode, 'NoSuchBucket') !== false) { - echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorCode, 'NotFoundException') !== false) { - echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorMessage, 'encryption') !== false) { - echo "S3 Encryption Error: {$errorMessage}\n"; - } else { - echo "AWS Service Error: {$errorMessage}\n"; - echo " Error Code: {$errorCode}\n"; - } - exit(1); - } catch (Exception $e) { - echo "Unexpected error: {$e->getMessage()}\n"; - echo " File: {$e->getFile()}:{$e->getLine()}\n"; - exit(1); - } - - echo "=== S3 Encryption Client post-migration (V3 with key commitment) Example ===\n"; - echo "Bucket: {$bucketName}\n"; - echo "Object Key: {$objectKey}\n"; - echo "KMS Key ID: {$kmsKeyId}\n"; - echo "Region: {$region}\n"; - echo "\n"; - - try { - $testData = "Hello, World! This is a test message for S3 encryption client post-migration (V3 with key commitment) in PHP."; - echo "Original data: {$testData}\n"; - echo "Data length: " . strlen($testData) . " bytes\n"; - echo "\n"; - - $v3EncryptionClient = new S3EncryptionClientV3( - new S3Client([ - 'region' => $region, - 'version' => 'latest', - ]) - ); - - $materialsProviderV3 = new KmsMaterialsProviderV3( - new KmsClient([ - 'region' => $region, - 'version' => 'latest', - ]), - $kmsKeyId - ); - - $cipherOptions = [ - 'Cipher' => 'gcm', - 'KeySize' => 256, - ]; - - $v3EncryptionClient->putObject([ - '@MaterialsProvider' => $materialsProviderV3, - '@CipherOptions' => $cipherOptions, - '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_REQUIRE_DECRYPT', - '@KmsEncryptionContext' => ['context-key' => 'context-value'], - 'Bucket' => $bucketName, - 'Key' => $objectKey, - 'Body' => $testData, - ]); - - $getResponse = $v3EncryptionClient->getObject([ - '@KmsAllowDecryptWithAnyCmk' => true, - '@SecurityProfile' => 'V3', - '@CommitmentPolicy' => 'REQUIRE_ENCRYPT_REQUIRE_DECRYPT', - '@MaterialsProvider' => $materialsProviderV3, - '@CipherOptions' => $cipherOptions, - 'Bucket' => $bucketName, - 'Key' => $objectKey, - ]); - - // Read the decrypted data - $decryptedData = (string) $getResponse['Body']; - - echo "Successfully downloaded and decrypted object from S3!\n"; - echo " Object size: " . strlen($decryptedData) . " bytes\n"; - echo " Decrypted data: {$decryptedData}\n"; - echo "\n"; - - echo "--- Verify Roundtrip Success ---\n"; - - // Verify the roundtrip was successful - if ($decryptedData === $testData) { - echo "SUCCESS: Roundtrip encryption/decryption completed successfully!\n"; - echo " Original data matches decrypted data\n"; - echo " Data integrity verified\n"; - } else { - echo "ERROR: Roundtrip failed - data mismatch\n"; - echo " Original: {$testData}\n"; - echo " Decrypted: {$decryptedData}\n"; - exit(1); - } - - // Optionally Delete the Object - // echo "--- Cleanup ---\n"; - // Clean up the test object using regular S3 client - // $s3Client->deleteObject([ - // 'Bucket' => $bucketName, - // 'Key' => $objectKey - // ]); - // echo "Test object deleted from S3\n"; - - echo "\n"; - echo "=== Example completed successfully! ===\n"; - - } catch (AwsException $e) { - $errorCode = $e->getAwsErrorCode(); - $errorMessage = $e->getMessage(); - - if (strpos($errorCode, 'NoSuchBucket') !== false) { - echo "Error: S3 bucket '{$bucketName}' does not exist or is not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorCode, 'NotFoundException') !== false) { - echo "Error: KMS key '{$kmsKeyId}' not found or not accessible\n"; - echo " {$errorMessage}\n"; - } elseif (strpos($errorMessage, 'encryption') !== false) { - echo "S3 Encryption Error: {$errorMessage}\n"; - } else { - echo "AWS Service Error: {$errorMessage}\n"; - echo " Error Code: {$errorCode}\n"; - } - exit(1); - } catch (Exception $e) { - echo "Unexpected error: {$e->getMessage()}\n"; - echo " File: {$e->getFile()}:{$e->getLine()}\n"; - exit(1); - } -} - -// Run the main function if this script is executed directly -if (php_sapi_name() === 'cli' && isset($GLOBALS['argv']) && basename($GLOBALS['argv'][0]) === basename(__FILE__)) { - main(); - testMigration(); -} diff --git a/all-examples/ruby/v2/Gemfile b/all-examples/ruby/v2/Gemfile deleted file mode 100644 index 5f51bf18..00000000 --- a/all-examples/ruby/v2/Gemfile +++ /dev/null @@ -1,12 +0,0 @@ -source 'https://rubygems.org' - -ruby '>= 2.7.0' - -gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' -gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' -gem 'json', '~> 2.0' -gem 'rexml', '~> 3.0' - -group :development do - gem 'rubocop', '~> 1.0' -end diff --git a/all-examples/ruby/v2/Gemfile.lock b/all-examples/ruby/v2/Gemfile.lock deleted file mode 100644 index 7c4a32c4..00000000 --- a/all-examples/ruby/v2/Gemfile.lock +++ /dev/null @@ -1,82 +0,0 @@ -PATH - remote: local-ruby-sdk/gems/aws-sdk-kms - specs: - aws-sdk-kms (1.115.0) - aws-sdk-core (~> 3, >= 3.234.0) - aws-sigv4 (~> 1.5) - -PATH - remote: local-ruby-sdk/gems/aws-sdk-s3 - specs: - aws-sdk-s3 (1.201.0) - aws-sdk-core (~> 3, >= 3.234.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - -GEM - remote: https://rubygems.org/ - specs: - ast (2.4.3) - aws-eventstream (1.4.0) - aws-partitions (1.1177.0) - aws-sdk-core (3.235.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - bigdecimal - jmespath (~> 1, >= 1.6.1) - logger - aws-sigv4 (1.12.1) - aws-eventstream (~> 1, >= 1.0.2) - base64 (0.3.0) - bigdecimal (3.3.1) - jmespath (1.6.2) - json (2.15.2) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - parallel (1.27.0) - parser (3.3.10.0) - ast (~> 2.4.1) - racc - prism (1.6.0) - racc (1.8.1) - rainbow (3.1.1) - regexp_parser (2.11.3) - rexml (3.4.4) - rubocop (1.81.6) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.47.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - ruby-progressbar (1.13.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - aws-sdk-kms! - aws-sdk-s3! - json (~> 2.0) - rexml (~> 3.0) - rubocop (~> 1.0) - -RUBY VERSION - ruby 3.4.7p58 - -BUNDLED WITH - 2.7.2 diff --git a/all-examples/ruby/v2/Makefile b/all-examples/ruby/v2/Makefile deleted file mode 100644 index cfa1adef..00000000 --- a/all-examples/ruby/v2/Makefile +++ /dev/null @@ -1,70 +0,0 @@ -# Makefile for S3 Encryption Client Ruby v2 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.rb - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-ruby-v2 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Bundler -install: - @echo "Installing Ruby dependencies..." - @bundle install - @echo "Dependencies installed successfully!" - -# Clean bundle artifacts -clean: - @echo "Cleaning bundle artifacts..." - @bundle clean --force - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v2 Ruby example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client Ruby v2 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install Ruby dependencies using Bundler" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove bundle artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Ruby 3.0+ installed on the system" - @echo " - Bundler gem installed (gem install bundler)" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v2 Ruby SDK (included in local-ruby-sdk)" diff --git a/all-examples/ruby/v2/local-ruby-sdk b/all-examples/ruby/v2/local-ruby-sdk deleted file mode 120000 index 7abd378c..00000000 --- a/all-examples/ruby/v2/local-ruby-sdk +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/ruby-v2-server/local-ruby-sdk \ No newline at end of file diff --git a/all-examples/ruby/v2/main.rb b/all-examples/ruby/v2/main.rb deleted file mode 100644 index 3fc86f7c..00000000 --- a/all-examples/ruby/v2/main.rb +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env ruby - -require 'aws-sdk-s3' -require 'aws-sdk-kms' -require 'json' - -# See: https://github.com/ruby/openssl/issues/949 -Aws.use_bundled_cert! - -def main - # Check command line arguments - if ARGV.length != 4 - puts "Usage: #{$0} " - puts "Example: #{$0} avp-21638 s3ec-ruby-v2 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" - exit 1 - end - - bucket_name = ARGV[0] - object_key = ARGV[1] - kms_key_id = ARGV[2] - region = ARGV[3] - - puts "=== S3 Encryption Client v2 Example (Ruby) ===" - puts "Bucket: #{bucket_name}" - puts "Object Key: #{object_key}" - puts "KMS Key ID: #{kms_key_id}" - puts "Region: #{region}" - puts - - begin - # Test data for encryption - test_data = "Hello, World! This is a test message for S3 encryption client v2 in Ruby." - puts "Original data: #{test_data}" - puts "Data length: #{test_data.length} bytes" - puts - - puts "--- Initialize S3 Encryption Client v2 ---" - - # Create regular S3 client - s3_client = Aws::S3::Client.new(region: region) - - # Create KMS client - kms_client = Aws::KMS::Client.new(region: region) - - # Create S3 Encryption Client v2 - encryption_client = Aws::S3::EncryptionV2::Client.new( - client: s3_client, - kms_key_id: kms_key_id, - kms_client: kms_client, - key_wrap_schema: :kms_context, - content_encryption_schema: :aes_gcm_no_padding, - security_profile: :v2 - ) - - puts "Successfully initialized S3 Encryption Client v2" - puts "--- Encrypt and Upload Object to S3 ---" - - # Add encryption context - encryption_context = { - 'purpose' => 'example', - 'version' => 'v2', - 'language' => 'ruby' - } - - # Upload encrypted object using S3 Encryption Client - put_response = encryption_client.put_object({ - bucket: bucket_name, - key: object_key, - body: test_data, - kms_encryption_context: encryption_context - }) - - puts "Successfully uploaded encrypted object to S3!" - puts " Bucket: #{bucket_name}" - puts " Key: #{object_key}" - puts " Encryption Context: #{encryption_context}" - puts - - puts "--- Download and Decrypt Object from S3 ---" - - # Download and decrypt object using S3 Encryption Client - get_response = encryption_client.get_object({ - bucket: bucket_name, - key: object_key, - kms_encryption_context: encryption_context - }) - - # Read the decrypted data - decrypted_data = get_response.body.read - - puts "Successfully downloaded and decrypted object from S3!" - puts " Object size: #{decrypted_data.length} bytes" - puts " Decrypted data: #{decrypted_data}" - puts - - puts "--- Verify Roundtrip Success ---" - - # Verify the roundtrip was successful - if decrypted_data == test_data - puts "SUCCESS: Roundtrip encryption/decryption completed successfully!" - puts " Original data matches decrypted data" - puts " Data integrity verified" - else - puts "ERROR: Roundtrip failed - data mismatch" - puts " Original: #{test_data}" - puts " Decrypted: #{decrypted_data}" - exit 1 - end - - # Optionally Delete the Object - #puts "--- Cleanup ---" - # Clean up the test object using regular S3 client - # s3_client.delete_object({ - # bucket: bucket_name, - # key: object_key - # }) - # puts "Test object deleted from S3" - - puts - puts "=== Example completed successfully! ===" - - rescue Aws::S3::Errors::NoSuchBucket => e - puts "Error: S3 bucket '#{bucket_name}' does not exist or is not accessible" - puts " #{e.message}" - exit 1 - rescue Aws::KMS::Errors::NotFoundException => e - puts "Error: KMS key '#{kms_key_id}' not found or not accessible" - puts " #{e.message}" - exit 1 - rescue Aws::S3::EncryptionV2::Errors::EncryptionError => e - puts "S3 Encryption Error: #{e.message}" - exit 1 - rescue Aws::S3::EncryptionV2::Errors::DecryptionError => e - puts "S3 Decryption Error: #{e.message}" - exit 1 - rescue Aws::Errors::ServiceError => e - puts "AWS Service Error: #{e.message}" - puts " Error Code: #{e.code}" if e.respond_to?(:code) - exit 1 - rescue StandardError => e - puts "Unexpected error: #{e.message}" - puts e.backtrace.first(5) - exit 1 - end -end - -# Run the main function if this script is executed directly -if __FILE__ == $0 - main -end diff --git a/all-examples/ruby/v3/Gemfile b/all-examples/ruby/v3/Gemfile deleted file mode 100644 index 5f51bf18..00000000 --- a/all-examples/ruby/v3/Gemfile +++ /dev/null @@ -1,12 +0,0 @@ -source 'https://rubygems.org' - -ruby '>= 2.7.0' - -gem 'aws-sdk-s3', path: 'local-ruby-sdk/gems/aws-sdk-s3' -gem 'aws-sdk-kms', path: 'local-ruby-sdk/gems/aws-sdk-kms' -gem 'json', '~> 2.0' -gem 'rexml', '~> 3.0' - -group :development do - gem 'rubocop', '~> 1.0' -end diff --git a/all-examples/ruby/v3/Makefile b/all-examples/ruby/v3/Makefile deleted file mode 100644 index b27bf29f..00000000 --- a/all-examples/ruby/v3/Makefile +++ /dev/null @@ -1,70 +0,0 @@ -# Makefile for S3 Encryption Client Ruby v3 Example - -# Default target -.PHONY: all install clean run help - -# Variables -SCRIPT = main.rb - -# Default arguments for running the example -# Override these when calling make run -BUCKET_NAME ?= avp-21638 -OBJECT_KEY ?= s3ec-ruby-v3 -KMS_KEY_ID ?= arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 -AWS_REGION ?= us-east-2 - -all: install - -# Install dependencies using Bundler -install: - @echo "Installing Ruby dependencies..." - @bundle install - @echo "Dependencies installed successfully!" - -# Clean bundle artifacts -clean: - @echo "Cleaning bundle artifacts..." - @bundle clean --force - @echo "Clean completed!" - -# Run the example with default arguments -run: install - @echo "Running S3 Encryption Client v3 Ruby example..." - @echo "Bucket: $(BUCKET_NAME)" - @echo "Object Key: $(OBJECT_KEY)" - @echo "KMS Key ID: $(KMS_KEY_ID)" - @echo "Region: $(AWS_REGION)" - @echo "" - @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Run with custom arguments -# Usage: make run-custom BUCKET_NAME=my-bucket OBJECT_KEY=my-key KMS_KEY_ID=my-kms-key AWS_REGION=my-region -run-custom: install - @bundle exec ruby $(SCRIPT) $(BUCKET_NAME) $(OBJECT_KEY) $(KMS_KEY_ID) $(AWS_REGION) - -# Show help -help: - @echo "S3 Encryption Client Ruby v3 Example Makefile" - @echo "" - @echo "Available targets:" - @echo " install - Install Ruby dependencies using Bundler" - @echo " run - Install dependencies and run the example with default parameters" - @echo " run-custom - Install dependencies and run with custom parameters" - @echo " clean - Remove bundle artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Default parameters:" - @echo " BUCKET_NAME = $(BUCKET_NAME)" - @echo " OBJECT_KEY = $(OBJECT_KEY)" - @echo " KMS_KEY_ID = $(KMS_KEY_ID)" - @echo " AWS_REGION = $(AWS_REGION)" - @echo "" - @echo "To run with custom parameters:" - @echo " make run BUCKET_NAME=your-bucket OBJECT_KEY=your-key KMS_KEY_ID=your-kms-key AWS_REGION=your-region" - @echo "" - @echo "Prerequisites:" - @echo " - Ruby 3.0+ installed on the system" - @echo " - Bundler gem installed (gem install bundler)" - @echo " - AWS credentials configured (AWS CLI, environment variables, or IAM role)" - @echo " - Valid S3 bucket and KMS key with appropriate permissions" - @echo " - S3 Encryption Client v3 Ruby SDK (included in local-ruby-sdk)" diff --git a/all-examples/ruby/v3/local-ruby-sdk b/all-examples/ruby/v3/local-ruby-sdk deleted file mode 120000 index 49be2657..00000000 --- a/all-examples/ruby/v3/local-ruby-sdk +++ /dev/null @@ -1 +0,0 @@ -../../../test-server/ruby-v3-server/local-ruby-sdk \ No newline at end of file diff --git a/all-examples/ruby/v3/main.rb b/all-examples/ruby/v3/main.rb deleted file mode 100644 index 59743515..00000000 --- a/all-examples/ruby/v3/main.rb +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env ruby - -require 'aws-sdk-s3' -require 'aws-sdk-kms' -require 'json' - -# See: https://github.com/ruby/openssl/issues/949 -Aws.use_bundled_cert! - -def main - # Check command line arguments - if ARGV.length != 4 - puts "Usage: #{$0} " - puts "Example: #{$0} avp-21638 s3ec-ruby-v3 arn:aws:kms:us-east-2:648638458147:key/a47079da-17e4-45a5-b82e-2bac101cad01 us-east-2" - exit 1 - end - - bucket_name = ARGV[0] - object_key = ARGV[1] - kms_key_id = ARGV[2] - region = ARGV[3] - - puts "=== S3 Encryption Client v3 Example (Ruby) ===" - puts "Bucket: #{bucket_name}" - puts "Object Key: #{object_key}" - puts "KMS Key ID: #{kms_key_id}" - puts "Region: #{region}" - puts - - begin - # Test data for encryption - test_data = "Hello, World! This is a test message for S3 encryption client v3 in Ruby." - puts "Original data: #{test_data}" - puts "Data length: #{test_data.length} bytes" - puts - - puts "--- Initialize S3 Encryption Client v3 ---" - - # Create regular S3 client - s3_client = Aws::S3::Client.new(region: region) - - # Create KMS client - kms_client = Aws::KMS::Client.new(region: region) - - # Create S3 Encryption Client v3 - encryption_client = Aws::S3::EncryptionV3::Client.new( - client: s3_client, - kms_key_id: kms_key_id, - kms_client: kms_client, - key_wrap_schema: :kms_context - ) - - puts "Successfully initialized S3 Encryption Client v3" - puts "--- Encrypt and Upload Object to S3 ---" - - # Add encryption context - encryption_context = { - 'purpose' => 'example', - 'version' => 'v3', - 'language' => 'ruby' - } - - # Upload encrypted object using S3 Encryption Client - put_response = encryption_client.put_object({ - bucket: bucket_name, - key: object_key, - body: test_data, - kms_encryption_context: encryption_context - }) - - puts "Successfully uploaded encrypted object to S3!" - puts " Bucket: #{bucket_name}" - puts " Key: #{object_key}" - puts " Encryption Context: #{encryption_context}" - puts - - puts "--- Download and Decrypt Object from S3 ---" - - # Download and decrypt object using S3 Encryption Client - get_response = encryption_client.get_object({ - bucket: bucket_name, - key: object_key, - kms_encryption_context: encryption_context - }) - - # Read the decrypted data - decrypted_data = get_response.body.read - - puts "Successfully downloaded and decrypted object from S3!" - puts " Object size: #{decrypted_data.length} bytes" - puts " Decrypted data: #{decrypted_data}" - puts - - puts "--- Verify Roundtrip Success ---" - - # Verify the roundtrip was successful - if decrypted_data == test_data - puts "SUCCESS: Roundtrip encryption/decryption completed successfully!" - puts " Original data matches decrypted data" - puts " Data integrity verified" - else - puts "ERROR: Roundtrip failed - data mismatch" - puts " Original: #{test_data}" - puts " Decrypted: #{decrypted_data}" - exit 1 - end - - # Optionally Delete the Object - #puts "--- Cleanup ---" - # Clean up the test object using regular S3 client - # s3_client.delete_object({ - # bucket: bucket_name, - # key: object_key - # }) - # puts "Test object deleted from S3" - - puts - puts "=== Example completed successfully! ===" - - rescue Aws::S3::Errors::NoSuchBucket => e - puts "Error: S3 bucket '#{bucket_name}' does not exist or is not accessible" - puts " #{e.message}" - exit 1 - rescue Aws::KMS::Errors::NotFoundException => e - puts "Error: KMS key '#{kms_key_id}' not found or not accessible" - puts " #{e.message}" - exit 1 - rescue Aws::S3::EncryptionV3::Errors::EncryptionError => e - puts "S3 Encryption Error: #{e.message}" - exit 1 - rescue Aws::S3::EncryptionV3::Errors::DecryptionError => e - puts "S3 Decryption Error: #{e.message}" - exit 1 - rescue Aws::Errors::ServiceError => e - puts "AWS Service Error: #{e.message}" - puts " Error Code: #{e.code}" if e.respond_to?(:code) - exit 1 - rescue StandardError => e - puts "Unexpected error: #{e.message}" - puts e.backtrace.first(5) - exit 1 - end -end - -# Run the main function if this script is executed directly -if __FILE__ == $0 - main -end From a277b60b51761ed4aca753dccfa32e70f0d14ef2 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 13:45:15 -0800 Subject: [PATCH 188/201] fix CI --- .github/workflows/main.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d53b9dca..52c3e465 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,11 +35,3 @@ jobs: name: Run Duvet uses: ./.github/workflows/duvet.yml secrets: inherit - - run-examples: - permissions: - id-token: write - contents: read - name: Run Examples - uses: ./.github/workflows/examples.yml - secrets: inherit From 1d9fac0743c7dba7caf707642c7c777a535f72fd Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 14:05:07 -0800 Subject: [PATCH 189/201] remove commented out code from java-tests --- .../amazon/encryption/s3/TestUtils.java | 63 ------------------- 1 file changed, 63 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 a9334eed..2b9cd062 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 @@ -75,7 +75,6 @@ public class TestUtils { public static final String CPP_V2_TRANSITION = "CPP-V2-Transition"; public static final String CPP_V3 = "CPP-V3"; -// public static final String RUBY_V2_CURRENT = "Ruby-V2-Current"; public static final String RUBY_V2_TRANSITION = "Ruby-V2-Transition"; public static final String RUBY_V3 = "Ruby-V3"; @@ -92,30 +91,24 @@ public class TestUtils { // Sets of unsupported features by language public static final Set ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED = Set.of(PHP_V2_TRANSITION, PHP_V3, 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_V3_TRANSITION, NET_V4); public static final Set ENCRYPTION_CONTEXT_ON_ENCRYPT_UNSUPPORTED = Set.of(NET_V3_TRANSITION, NET_V4); -// Set.of(NET_V2_CURRENT, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); public static final Set RE_ENCRYPT_SUPPORTED = Set.of(JAVA_V3_TRANSITION, JAVA_V4); -// Set.of(JAVA_V3_CURRENT, JAVA_V3_TRANSITION, JAVA_V4); public static final Set RANGED_GETS_SUPPORTED = Set.of( JAVA_V3_TRANSITION, JAVA_V4, CPP_V2_TRANSITION, CPP_V3 -// 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 = Set.of(JAVA_V3_TRANSITION, JAVA_V4, NET_V3_TRANSITION, NET_V4, RUBY_V2_TRANSITION, RUBY_V3, CPP_V2_TRANSITION, CPP_V3); -// 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_TRANSITION, JAVA_V4, NET_V3_TRANSITION, NET_V4, RUBY_V2_TRANSITION, RUBY_V3); -// 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 = @@ -127,13 +120,10 @@ public class TestUtils { // Python MUST support decrypting KMS instruction files, but does not yet. public static final Set KMS_INSTRUCTION_FILE_UNSUPPORTED = Set.of(NET_V2_TRANSITION, NET_V3_TRANSITION, NET_V4); -// Set.of(NET_V2_CURRENT, NET_V2_TRANSITION, NET_V3_CURRENT, NET_V3_TRANSITION, NET_V4); // Go does not write with instruction files public static final Set INSTRUCTION_FILE_PUT_UNSUPPORTED = Set.of(GO_V3_TRANSITION, GO_V4, PYTHON_V3); -// Set.of(GO_V3_CURRENT, GO_V3_TRANSITION, GO_V4, PYTHON_V3, CPP_V2_CURRENT); - // Apparently C++ V2 Current does not work, even though it should // Not implemented yet in Python. public static final Set INSTRUCTION_FILE_GET_UNSUPPORTED = @@ -152,22 +142,10 @@ public class TestUtils { PHP_V3 ); -// public static final Set CURRENT_VERSIONS = -// Set.of( -// JAVA_V3_CURRENT, -// GO_V3_CURRENT, -// NET_V2_CURRENT, -// NET_V3_CURRENT, -// CPP_V2_CURRENT, -// RUBY_V2_CURRENT, -// PHP_V2_CURRENT -// ); -// public static final Set TRANSITION_VERSIONS = Set.of( JAVA_V3_TRANSITION, GO_V3_TRANSITION, - // NET_V2_TRANSITION, NET_V3_TRANSITION, CPP_V2_TRANSITION, PHP_V2_TRANSITION, @@ -362,15 +340,6 @@ public static Stream clientsForTest() { .map(Arguments::of); } -// /** -// * Get stream of arguments for current version clients for testing. -// */ -// public static Stream currentClientsForTest() { -// return serverMap.values().stream() -// .filter(target -> CURRENT_VERSIONS.contains(target.getLanguageName())) -// .map(Arguments::of); -// } - /** * Get stream of arguments for transition version clients for testing. */ @@ -439,38 +408,6 @@ public static Stream encryptTransitionDecryptImproved() { ))); } -// public static Stream encryptImprovedDecryptCurrent() { -// return improvedClientsForTest() -// .flatMap(encrypt -> currentClientsForTest() -// .flatMap(decrypt -> Stream.of( -// Arguments.of(encrypt.get()[0], decrypt.get()[0]) -// ))); -// } - -// public static Stream encryptCurrentDecryptImproved() { -// return currentClientsForTest() -// .flatMap(encrypt -> improvedClientsForTest() -// .flatMap(decrypt -> Stream.of( -// Arguments.of(encrypt.get()[0], decrypt.get()[0]) -// ))); -// } - -// public static Stream encryptTransitionDecryptCurrent() { -// return transitionClientsForTest() -// .flatMap(encrypt -> currentClientsForTest() -// .flatMap(decrypt -> Stream.of( -// Arguments.of(encrypt.get()[0], decrypt.get()[0]) -// ))); -// } - -// public static Stream encryptCurrentDecryptTransition() { -// return currentClientsForTest() -// .flatMap(encrypt -> transitionClientsForTest() -// .flatMap(decrypt -> Stream.of( -// Arguments.of(encrypt.get()[0], decrypt.get()[0]) -// ))); -// } - /** * Provides a stream of arguments for parameterized tests that test cross-language compatibility * @return Stream of Arguments containing pairs of LanguageServerTarget for encryption and decryption From 7e5547dbbb83a2a173fc7eab9eb6e6914adc8ddc Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 14:08:56 -0800 Subject: [PATCH 190/201] tweak cpp client to maybe have fewer timeouts --- test-server/cpp-v3-server/main.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp index 4e7227df..169fa517 100644 --- a/test-server/cpp-v3-server/main.cpp +++ b/test-server/cpp-v3-server/main.cpp @@ -296,6 +296,11 @@ MHD_Result handle_create_client(struct MHD_Connection *connection, clientConfig.maxConnections = 512; // Large pool per client clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard"); + // Increase timeouts for CI environments where SSL handshakes can be slow + // Default connectTimeoutMs is 1000ms, which is too short for busy CI runners + clientConfig.connectTimeoutMs = 10000; // 10 seconds for SSL connection establishment + clientConfig.requestTimeoutMs = 30000; // 30 seconds for complete request/response + // 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 From 8ddb8f4f49f36358e60c503c9b59295be08bb6ae Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 14:44:06 -0800 Subject: [PATCH 191/201] point to public Java, Go repos --- .gitmodules | 15 ++++++++------- test-server/go-v3-transition-server/local-go-s3ec | 2 +- test-server/go-v4-server/local-go-s3ec | 2 +- .../java-v3-transition-server/s3ec-staging | 2 +- test-server/java-v4-server/s3ec-staging | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitmodules b/.gitmodules index df9a207f..75e91f99 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,15 +12,16 @@ branch = master [submodule "test-server/go-v4-server/local-go-s3ec"] path = test-server/go-v4-server/local-go-s3ec - url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging + url = https://github.com/aws/amazon-s3-encryption-client-go + branch = main [submodule "test-server/java-v3-transition-server/s3ec-staging"] path = test-server/java-v3-transition-server/s3ec-staging - url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git - branch = imabhichow/transition-read-kc + url = git@github.com:aws/amazon-s3-encryption-client-java.git + branch = main-3.x [submodule "test-server/java-v4-server/s3ec-staging"] path = test-server/java-v4-server/s3ec-staging - url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git - branch = imabhichow/add-kc + url = git@github.com:aws/amazon-s3-encryption-client-java.git + branch = main [submodule "test-server/specification"] path = test-server/specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git @@ -31,8 +32,8 @@ branch = main [submodule "test-server/go-v3-transition-server/local-go-s3ec"] path = test-server/go-v3-transition-server/local-go-s3ec - url = https://github.com/aws/private-amazon-s3-encryption-client-go-staging - branch = v3-strip + url = https://github.com/aws/amazon-s3-encryption-client-go + branch = main [submodule "test-server/net-v3-transition-server/s3ec-v3-transition-branch"] path = test-server/net-v3-transition-server/s3ec-v3-transition-branch url = https://github.com/aws/amazon-s3-encryption-client-dotnet.git diff --git a/test-server/go-v3-transition-server/local-go-s3ec b/test-server/go-v3-transition-server/local-go-s3ec index 912914ad..bf8a12f6 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 912914ad1c14942c3ea8638fb37f9e2c46445a84 +Subproject commit bf8a12f61694d750a13a44f0a691dd7ced0ff904 diff --git a/test-server/go-v4-server/local-go-s3ec b/test-server/go-v4-server/local-go-s3ec index 912914ad..bf8a12f6 160000 --- a/test-server/go-v4-server/local-go-s3ec +++ b/test-server/go-v4-server/local-go-s3ec @@ -1 +1 @@ -Subproject commit 912914ad1c14942c3ea8638fb37f9e2c46445a84 +Subproject commit bf8a12f61694d750a13a44f0a691dd7ced0ff904 diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging index 183f1984..d829a235 160000 --- a/test-server/java-v3-transition-server/s3ec-staging +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -1 +1 @@ -Subproject commit 183f1984ed1679e8aa4cb368aeda66f2131a2061 +Subproject commit d829a235854996e0f25736662510c2aa25e61fae diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging index 7a1899bb..a95aa3fd 160000 --- a/test-server/java-v4-server/s3ec-staging +++ b/test-server/java-v4-server/s3ec-staging @@ -1 +1 @@ -Subproject commit 7a1899bb8be6f137a3031ff76f2a1bf3f278e98d +Subproject commit a95aa3fddb5abf4e17551c0ef3c247c7a43edf40 From 45c5de00afe3745fb02ea746177f7c71ae8d9942 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 14:54:59 -0800 Subject: [PATCH 192/201] fix ci branch name --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52c3e465..50f3abcb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: Main Workflow on: push: - branches: [ main, fireegg-test-servers ] + branches: [ main, staging ] pull_request: workflow_dispatch: inputs: From fef65e45791980e5ff9b6ea80fc34b348f2ab8d8 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 14:56:55 -0800 Subject: [PATCH 193/201] fix merge error --- .github/workflows/main.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a7f97be3..50f3abcb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,9 +25,6 @@ jobs: uses: ./.github/workflows/test.yml with: python-version: ${{ inputs.python-version || '3.11' }} - permissions: - id-token: write - contents: read secrets: inherit run-duvet: From da904a56bb3db48dc2c68b25710a05cf995d1bc2 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:14:42 -0800 Subject: [PATCH 194/201] remove PAT --- .github/workflows/duvet.yml | 1 - .github/workflows/test.yml | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index 8b277ec0..e6549030 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -17,7 +17,6 @@ jobs: uses: actions/checkout@v5 with: submodules: true - token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - name: Checkout CPP code cpp-v3 uses: actions/checkout@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ea728af..39b2b322 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,10 +22,6 @@ jobs: uses: actions/checkout@v5 with: submodules: false - # This is Ryan Emery's (seebees) PAT. - # To grant this workflow access to a new private repo, - # ask Ryan to edit this PAT's permissions to add access to a new private repo. - token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} # There are a lot of submodules here # This initializes the checkouts in parallel (--jobs) @@ -35,12 +31,6 @@ jobs: id: cpu-count run: echo "count=$(node -p 'require("os").cpus().length')" >> $GITHUB_OUTPUT - - name: Setup git submodules with PAT - run: | - git config --global url."https://github.com/".insteadOf "git@github.com:" - git config --global credential.helper store - echo "https://x-token-auth:${{ secrets.PAT_FOR_PRIVATE_RUBY }}@github.com" > ~/.git-credentials - - name: Optimize git for performance run: | git config --global fetch.parallel ${{ steps.cpu-count.outputs.count }} From 76d559f76626c41247aad06f8ce56dcdfb374792 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:26:40 -0800 Subject: [PATCH 195/201] dynamically load version from submodule pom --- test-server/java-v3-transition-server/build.gradle.kts | 7 ++++++- test-server/java-v4-server/build.gradle.kts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test-server/java-v3-transition-server/build.gradle.kts b/test-server/java-v3-transition-server/build.gradle.kts index 7f249d65..7f474fa0 100644 --- a/test-server/java-v3-transition-server/build.gradle.kts +++ b/test-server/java-v3-transition-server/build.gradle.kts @@ -4,6 +4,10 @@ plugins { application } +// Dynamically read S3EC version from submodule's pom.xml +val s3ecVersion = file("s3ec-staging/pom.xml").readText() + .let { Regex("(.*?)").find(it)?.groupValues?.get(1) ?: "3.6.0" } + dependencies { val smithyJavaVersion: String by project @@ -14,7 +18,8 @@ dependencies { implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") // S3EC from local Maven repository (installed by mvn install) - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-read-kc") + // Version is dynamically read from s3ec-staging/pom.xml + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:$s3ecVersion") } // Use that application plugin to start the service via the `run` task. diff --git a/test-server/java-v4-server/build.gradle.kts b/test-server/java-v4-server/build.gradle.kts index fcb3c3ca..d55d93d7 100644 --- a/test-server/java-v4-server/build.gradle.kts +++ b/test-server/java-v4-server/build.gradle.kts @@ -4,6 +4,10 @@ plugins { application } +// Dynamically read S3EC version from submodule's pom.xml +val s3ecVersion = file("s3ec-staging/pom.xml").readText() + .let { Regex("(.*?)").find(it)?.groupValues?.get(1) ?: "4.0.0" } + dependencies { val smithyJavaVersion: String by project @@ -14,7 +18,8 @@ dependencies { implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") // S3EC from local Maven repository (installed by mvn install) - implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-add-kc") + // Version is dynamically read from s3ec-staging/pom.xml + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:$s3ecVersion") } // Use that application plugin to start the service via the `run` task. From d8a2e21dfc4417de70ee3f6a203e99d6b0c7da3f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:29:39 -0800 Subject: [PATCH 196/201] use HTTPS --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39b2b322..4d5570ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,10 @@ jobs: id: cpu-count run: echo "count=$(node -p 'require("os").cpus().length')" >> $GITHUB_OUTPUT + - name: Configure git to use HTTPS instead of SSH for public repos + run: | + git config --global url."https://github.com/".insteadOf "git@github.com:" + - name: Optimize git for performance run: | git config --global fetch.parallel ${{ steps.cpu-count.outputs.count }} From 06b3a757586ecfc3e618717d4b3303f0adf9fda7 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:34:51 -0800 Subject: [PATCH 197/201] restore PAT --- .github/workflows/duvet.yml | 1 + .github/workflows/test.yml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml index e6549030..8b277ec0 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v5 with: submodules: true + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} - name: Checkout CPP code cpp-v3 uses: actions/checkout@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d5570ca..201862c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: uses: actions/checkout@v5 with: submodules: false + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} # There are a lot of submodules here # This initializes the checkouts in parallel (--jobs) @@ -31,9 +32,11 @@ jobs: id: cpu-count run: echo "count=$(node -p 'require("os").cpus().length')" >> $GITHUB_OUTPUT - - name: Configure git to use HTTPS instead of SSH for public repos + - name: Setup git submodules with PAT run: | git config --global url."https://github.com/".insteadOf "git@github.com:" + git config --global credential.helper store + echo "https://x-token-auth:${{ secrets.PAT_FOR_PRIVATE_RUBY }}@github.com" > ~/.git-credentials - name: Optimize git for performance run: | From 6cfa131ea8f2e51da6bfed73efda1fae863e5031 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:52:31 -0800 Subject: [PATCH 198/201] restore Python integ tests --- .github/workflows/main.yml | 10 ++++++ .github/workflows/python-integ.yml | 57 ++++++++++++++++++++++++++++++ .github/workflows/test.yml | 30 ---------------- 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/python-integ.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 50f3abcb..50f817fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,16 @@ jobs: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit + python-integ: + permissions: + id-token: write + contents: read + name: Python Integration Tests + uses: ./.github/workflows/python-integ.yml + with: + python-version: ${{ inputs.python-version || '3.11' }} + secrets: inherit + run-duvet: permissions: id-token: write diff --git a/.github/workflows/python-integ.yml b/.github/workflows/python-integ.yml new file mode 100644 index 00000000..9e5ae818 --- /dev/null +++ b/.github/workflows/python-integ.yml @@ -0,0 +1,57 @@ +name: Python Integration Tests + +on: + workflow_call: + inputs: + python-version: + description: "Python version to use" + default: "3.11" + required: false + type: string + +jobs: + python-integ: + runs-on: macos-14-large + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + submodules: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version || '3.11' }} + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Install Uv + run: pip install uv + + - name: Install dependencies + run: make install + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role + aws-region: us-west-2 + + - name: Run unit tests + run: make test-unit + + - name: Run integration tests + run: make test-integration + env: + CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} + CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 201862c8..a6311eeb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,36 +87,6 @@ jobs: run: | brew install libmicrohttpd nlohmann-json ossp-uuid - # Legacy Python tests: - # - name: Set up Python - # uses: actions/setup-python@v5 - # with: - # python-version: ${{ inputs.python-version || '3.11' }} - # - # # Cache uv dependencies - # - name: Cache uv dependencies - # uses: actions/cache@v3 - # with: - # path: ~/.cache/uv - # key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v3-server/**/pyproject.toml') }} - # restore-keys: | - # ${{ runner.os }}-uv- - - # - name: Install Uv - # run: pip install uv - - # - name: Install dependencies - # run: make install - - # - name: Run unit tests - # run: make test-unit - - # - name: Run integration tests - # run: make test-integration - # env: - # CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} - # CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} - # Cache Gradle dependencies and build outputs - name: Cache Gradle packages uses: actions/cache@v4 From 727f05d8a00820370b9c9f789d8101edd5a9ac23 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Fri, 30 Jan 2026 15:55:00 -0800 Subject: [PATCH 199/201] rename tests to testserver tests --- .github/workflows/main.yml | 6 +++--- .github/workflows/{test.yml => test-server.yml} | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename .github/workflows/{test.yml => test-server.yml} (99%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 50f817fe..c3be058a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,12 +17,12 @@ jobs: name: Lint uses: ./.github/workflows/lint.yml - run-tests: + run-test-server: permissions: id-token: write contents: read - name: Run Tests - uses: ./.github/workflows/test.yml + name: Run TestServer Tests + uses: ./.github/workflows/test-server.yml with: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test-server.yml similarity index 99% rename from .github/workflows/test.yml rename to .github/workflows/test-server.yml index a6311eeb..6e8d3fa5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-server.yml @@ -1,4 +1,4 @@ -name: Run Tests +name: Run TestServer Tests on: workflow_call: @@ -11,7 +11,7 @@ on: type: string jobs: - test: + test-server: runs-on: macos-14-large permissions: id-token: write From d9e7a35e32971499e8c81eb9f541716d0d78bed0 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 2 Feb 2026 14:41:00 -0800 Subject: [PATCH 200/201] apply PR feedback --- .github/workflows/{main.yml => all-ci.yml} | 8 ++--- .../{duvet.yml => duvet-test-server.yml} | 10 +++--- .github/workflows/test-server.yml | 4 +-- test-server/Makefile | 32 +++++++++---------- test-server/model/client.smithy | 9 ++++-- 5 files changed, 34 insertions(+), 29 deletions(-) rename .github/workflows/{main.yml => all-ci.yml} (89%) rename .github/workflows/{duvet.yml => duvet-test-server.yml} (85%) diff --git a/.github/workflows/main.yml b/.github/workflows/all-ci.yml similarity index 89% rename from .github/workflows/main.yml rename to .github/workflows/all-ci.yml index c3be058a..c65364b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/all-ci.yml @@ -1,4 +1,4 @@ -name: Main Workflow +name: All CI on: push: @@ -13,7 +13,7 @@ on: type: string jobs: - lint: + python-lint: name: Lint uses: ./.github/workflows/lint.yml @@ -37,11 +37,11 @@ jobs: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit - run-duvet: + run-duvet-test-server: permissions: id-token: write contents: read pages: write name: Run Duvet - uses: ./.github/workflows/duvet.yml + uses: ./.github/workflows/duvet-test-server.yml secrets: inherit diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet-test-server.yml similarity index 85% rename from .github/workflows/duvet.yml rename to .github/workflows/duvet-test-server.yml index 8b277ec0..5f8da5f0 100644 --- a/.github/workflows/duvet.yml +++ b/.github/workflows/duvet-test-server.yml @@ -1,11 +1,11 @@ -name: Run Tests +name: Generate Duvet Report for TestServer on: workflow_call: # Optional inputs that can be provided when calling this workflow jobs: - test: + duvet: runs-on: macos-latest permissions: id-token: write @@ -90,15 +90,15 @@ jobs: test-server/index.html - name: Setup Pages - if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && (github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/fireegg-test-servers') && github.event_name == 'push' uses: actions/configure-pages@v5 - name: Upload Pages artifact - if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && (github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/fireegg-test-servers') && github.event_name == 'push' uses: actions/upload-pages-artifact@v3 with: path: test-server/ - name: Deploy to GitHub Pages - if: always() && github.ref == 'refs/heads/fireegg-test-servers' && github.event_name == 'push' + if: always() && (github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/fireegg-test-servers') && github.event_name == 'push' uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test-server.yml b/.github/workflows/test-server.yml index 6e8d3fa5..d2b542ee 100644 --- a/.github/workflows/test-server.yml +++ b/.github/workflows/test-server.yml @@ -124,7 +124,7 @@ jobs: MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }} - name: Run run-tests - run: cd test-server && make run-tests + run: cd test-server && make test-servers-run-tests env: AWS_REGION: us-west-2 TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} @@ -140,7 +140,7 @@ jobs: test-server/*/server.log - name: Stop the servers - run: cd test-server && make stop-servers + run: cd test-server && make test-servers-stop - name: Upload results if: always() diff --git a/test-server/Makefile b/test-server/Makefile index 94a76b3f..21b5c98b 100644 --- a/test-server/Makefile +++ b/test-server/Makefile @@ -1,14 +1,14 @@ # Makefile for S3 Encryption Client Testing -.PHONY: all start-servers run-tests stop-servers clean ci check-env help +.PHONY: test-servers-all test-servers-start test-servers-run-tests test-servers-stop test-servers-clean test-servers-ci test-servers-check-env test-servers-help # CI target for GitHub Actions -ci: +test-servers-ci: $(MAKE) build-all-servers $(MAKE) start-all-servers $(MAKE) wait-all-servers - $(MAKE) run-tests - $(MAKE) stop-servers + $(MAKE) test-servers-run-tests + $(MAKE) test-servers-stop SERVER_DIRS := $(shell find . -maxdepth 1 -type d -name '*-server' | sed 's|^\./||' | $(if $(FILTER),grep -E "$$(echo '$(FILTER)' | sed 's/,/|/g')",cat) | sort) @@ -36,7 +36,7 @@ $(BUILD_SERVER_TARGETS): build-%: fi # Build and start all servers -start-servers: +test-servers-start: @echo "Building all servers..." $(MAKE) build-all-servers @echo "Starting all servers..." @@ -75,7 +75,7 @@ $(WAIT_SERVER_TARGETS): wait-%: # Run the Java tests -run-tests: +test-servers-run-tests: @echo "Running Java tests..." @echo "Exporting environment variables from servers to tests..." @# Extract AWS environment variables from the current shell and pass them to the tests @@ -90,7 +90,7 @@ run-tests: @echo "Tests completed successfully" # Stop the servers -stop-servers: +test-servers-stop: @echo "Stopping servers..." @for dir in $(SERVER_DIRS); do \ echo "Stopping server in $$dir..."; \ @@ -99,18 +99,18 @@ stop-servers: @echo "Servers stopped" # Help target -help: +test-servers-help: @echo "Available targets:" - @echo " all : Start servers and run tests (default, output to stdout)" - @echo " ci : Run in CI mode (start servers, run tests, stop servers)" - @echo " start-servers : Start all servers in parallel" - @echo " run-tests : Run Java tests" - @echo " stop-servers : Stop running servers" - @echo " check-env : Check if required environment variables are set" - @echo " help : Show this help message" + @echo " test-servers-all : Start servers and run tests (default, output to stdout)" + @echo " test-servers-ci : Run in CI mode (start servers, run tests, stop servers)" + @echo " test-servers-start : Start all servers in parallel" + @echo " test-servers-run-tests : Run Java tests" + @echo " test-servers-stop : Stop running servers" + @echo " test-servers-check-env : Check if required environment variables are set" + @echo " test-servers-help : Show this help message" # Check if required environment variables are set -check-env: +test-servers-check-env: @echo "Checking required environment variables..." @if [ -z "$$AWS_ACCESS_KEY_ID" ]; then echo "AWS_ACCESS_KEY_ID is not set"; else echo "AWS_ACCESS_KEY_ID is set"; fi @if [ -z "$$AWS_SECRET_ACCESS_KEY" ]; then echo "AWS_SECRET_ACCESS_KEY is not set"; else echo "AWS_SECRET_ACCESS_KEY is set"; fi diff --git a/test-server/model/client.smithy b/test-server/model/client.smithy index 5772e8c2..11f65f57 100644 --- a/test-server/model/client.smithy +++ b/test-server/model/client.smithy @@ -52,9 +52,14 @@ enum EncryptionAlgorithm { structure InstructionFileConfig { /// This allows specifying a (non-encrypted) client for languages which /// support this for instruction files. - /// In general, languages should not require specifying it, - /// so it is best to leave it null until there's a good reason not to. + /// In general, languages do not require specifying a client; + /// they use the usual wrapped client for instruction file operations, + /// so it is fine to leave it null for now. /// This also requires a way to create non-encrypted clients which we don't have yet. + /// Some languages (Java) do allow a client to be passed specifically for instruction files, + /// so this should be implemented eventually for full coverage, + /// especially if other languages add this feature. Until then, + /// the Java integ tests are sufficient. clientId: String, enableInstructionFilePutObject: Boolean = false, disableInstructionFile: Boolean = false From 9f65202c414a0af29665f9678f412e299bdb0053 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 3 Feb 2026 15:40:08 -0800 Subject: [PATCH 201/201] new PAT --- .github/workflows/duvet-test-server.yml | 2 +- .github/workflows/test-server.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/duvet-test-server.yml b/.github/workflows/duvet-test-server.yml index 5f8da5f0..f4bac5a8 100644 --- a/.github/workflows/duvet-test-server.yml +++ b/.github/workflows/duvet-test-server.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v5 with: submodules: true - token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + token: ${{ secrets.PAT_FOR_SPEC }} - name: Checkout CPP code cpp-v3 uses: actions/checkout@v5 diff --git a/.github/workflows/test-server.yml b/.github/workflows/test-server.yml index d2b542ee..4fa10666 100644 --- a/.github/workflows/test-server.yml +++ b/.github/workflows/test-server.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v5 with: submodules: false - token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + token: ${{ secrets.PAT_FOR_SPEC }} # There are a lot of submodules here # This initializes the checkouts in parallel (--jobs) @@ -36,7 +36,7 @@ jobs: run: | git config --global url."https://github.com/".insteadOf "git@github.com:" git config --global credential.helper store - echo "https://x-token-auth:${{ secrets.PAT_FOR_PRIVATE_RUBY }}@github.com" > ~/.git-credentials + echo "https://x-token-auth:${{ secrets.PAT_FOR_SPEC }}@github.com" > ~/.git-credentials - name: Optimize git for performance run: |