diff --git a/.github/workflows/all-ci.yml b/.github/workflows/all-ci.yml
new file mode 100644
index 00000000..c65364b7
--- /dev/null
+++ b/.github/workflows/all-ci.yml
@@ -0,0 +1,47 @@
+name: All CI
+
+on:
+ push:
+ branches: [ main, staging ]
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ python-version:
+ description: 'Python version to use'
+ default: '3.11'
+ required: false
+ type: string
+
+jobs:
+ python-lint:
+ name: Lint
+ uses: ./.github/workflows/lint.yml
+
+ run-test-server:
+ permissions:
+ id-token: write
+ contents: read
+ name: Run TestServer Tests
+ uses: ./.github/workflows/test-server.yml
+ with:
+ 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-test-server:
+ permissions:
+ id-token: write
+ contents: read
+ pages: write
+ name: Run Duvet
+ uses: ./.github/workflows/duvet-test-server.yml
+ secrets: inherit
diff --git a/.github/workflows/duvet-test-server.yml b/.github/workflows/duvet-test-server.yml
new file mode 100644
index 00000000..f4bac5a8
--- /dev/null
+++ b/.github/workflows/duvet-test-server.yml
@@ -0,0 +1,104 @@
+name: Generate Duvet Report for TestServer
+
+on:
+ workflow_call:
+ # Optional inputs that can be provided when calling this workflow
+
+jobs:
+ duvet:
+ runs-on: macos-latest
+ permissions:
+ id-token: write
+ contents: read
+ pages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ submodules: true
+ token: ${{ secrets.PAT_FOR_SPEC }}
+
+ - name: Checkout CPP code cpp-v3
+ uses: actions/checkout@v5
+ with:
+ submodules: recursive
+ repository: aws/aws-sdk-cpp
+ ref: main
+ path: test-server/cpp-v3-server/aws-sdk-cpp/
+
+ - 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
+
+ - 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: 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/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/staging' || github.ref == 'refs/heads/fireegg-test-servers') && github.event_name == 'push'
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 16057711..bb1655bb 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-15
steps:
- name: Checkout code
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
deleted file mode 100644
index 675a0099..00000000
--- a/.github/workflows/main.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: Main Workflow
-
-on:
- push:
- branches: [ staging ]
- pull_request:
- workflow_dispatch:
- inputs:
- python-version:
- description: 'Python version to use'
- default: '3.11'
- required: false
- type: string
-
-jobs:
- lint:
- name: Lint
- uses: ./.github/workflows/lint.yml
-
- run-tests:
- name: Run Tests
- uses: ./.github/workflows/test.yml
- with:
- python-version: ${{ inputs.python-version || '3.11' }}
- permissions:
- id-token: write
- contents: read
- secrets: inherit
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-server.yml b/.github/workflows/test-server.yml
new file mode 100644
index 00000000..4fa10666
--- /dev/null
+++ b/.github/workflows/test-server.yml
@@ -0,0 +1,150 @@
+name: Run TestServer Tests
+
+on:
+ workflow_call:
+ # Optional inputs that can be provided when calling this workflow
+ inputs:
+ python-version:
+ description: "Python version to use"
+ default: "3.11"
+ required: false
+ type: string
+
+jobs:
+ test-server:
+ runs-on: macos-14-large
+ permissions:
+ id-token: write
+ contents: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ submodules: false
+ token: ${{ secrets.PAT_FOR_SPEC }}
+
+ # 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: Get CPU count
+ 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_SPEC }}@github.com" > ~/.git-credentials
+
+ - 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 --single-branch --jobs ${{ steps.cpu-count.outputs.count }}
+
+ - name: Update cpp submodules recursively with --jobs
+ run: |
+ git submodule update --init --recursive \
+ --depth 1 --single-branch \
+ --jobs ${{ steps.cpu-count.outputs.count }} \
+ --force \
+ test-server/cpp-v2-transition-server/aws-sdk-cpp \
+ test-server/cpp-v3-server/aws-sdk-cpp
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.4.7"
+ bundler-cache: true
+
+ - name: Set up PHP with Composer
+ uses: shivammathur/setup-php@verbose
+ with:
+ php-version: "8.1"
+
+ - 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:
+ go-version: 1.25
+
+ - name: Install C++ dependencies
+ run: |
+ brew install libmicrohttpd nlohmann-json ossp-uuid
+
+ # 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: Build the servers
+ 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
+ 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
+ env:
+ MAKEFLAGS: -j${{ steps.cpu-count.outputs.count }}
+
+ - name: Run 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 }}
+ 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
+
+ - name: Stop the servers
+ run: cd test-server && make test-servers-stop
+
+ - name: Upload results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: results
+ path: test-server/java-tests/build/reports/tests/integ
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index f8025246..00000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-name: Run Tests
-
-on:
- workflow_call:
- # Optional inputs that can be provided when calling this workflow
- inputs:
- python-version:
- description: 'Python version to use'
- default: '3.11'
- required: false
- type: string
-
-jobs:
- test:
- runs-on: ubuntu-latest
- permissions:
- id-token: write
- contents: read
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - 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('**/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@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- test-server/java-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
-
- - 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:
- 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"
diff --git a/.gitignore b/.gitignore
index 0e29a9fb..5cd8f239 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ __pycache__/
# Distribution / packaging
dist/
build/
+bin/
*.egg-info/
# Uv
@@ -51,3 +52,6 @@ gradle-app.setting
.DS_Store
smithy-java-core/out
+
+# test server
+*.pid
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..75e91f99
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,48 @@
+[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.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.git
+ branch = version-3
+[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
+ 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/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/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/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
+ branch = fire-egg-staging
+[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
+ 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/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
+ 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: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:aws/aws-sdk-cpp.git
+ branch = main
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..1fad4b74 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 {
@@ -99,23 +101,31 @@ export class S3ECPythonGithub extends cdk.Stack {
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
+ "s3:HeadObject", // Only get object metadata
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
+ "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 repo
],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
+ "s3:CreateBucket", // For S3EC-NET repo
+ "s3:DeleteBucket", // For S3EC-NET repo
"s3:ListBucket",
+ "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 repo
],
}),
]
@@ -155,16 +165,29 @@ 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:*" // For S3EC-NET repo
+ ]
}
},
"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: "/",
diff --git a/test-server/Makefile b/test-server/Makefile
index afbe97b5..21b5c98b 100644
--- a/test-server/Makefile
+++ b/test-server/Makefile
@@ -1,63 +1,81 @@
# 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
-
-# Default target
-all: start-servers run-tests
+.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: start-servers run-tests stop-servers
+test-servers-ci:
+ $(MAKE) build-all-servers
+ $(MAKE) start-all-servers
+ $(MAKE) wait-all-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)
-# Start Python server in background
-start-python-server:
- @echo "Starting Python server..."
- cd python-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-server.pid
- @echo "Python server starting..."
+BUILD_SERVER_TARGETS := $(addprefix build-, $(SERVER_DIRS))
+START_SERVER_TARGETS := $(addprefix start-, $(SERVER_DIRS))
+WAIT_SERVER_TARGETS := $(addprefix wait-, $(SERVER_DIRS))
-# Start Java server in background
-start-java-server:
- @echo "Starting Java server..."
- cd java-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
- @echo "Java server starting..."
-
-# Start both servers in parallel
-start-servers:
- @echo "Starting servers in parallel..."
- @$(MAKE) -j2 start-python-server start-java-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 \
- echo "Ports are open, waiting for servers to initialize..."; \
- sleep 5; \
- echo "Both 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; \
+# Build all servers in parallel
+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 "[`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; \
+ fi
+
+# Build and start all servers
+test-servers-start:
+ @echo "Building all servers..."
+ $(MAKE) build-all-servers
+ @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:
+ @$(MAKE) $(START_SERVER_TARGETS)
+
+$(START_SERVER_TARGETS): start-%:
+ @if [ -f $*/Makefile ]; then \
+ echo "Starting server in $*..." && \
+ $(MAKE) -C $* start-server; \
+ else \
+ echo "❌ Error: no Makefile found in $*"; \
+ exit 1; \
+ fi
+
+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 \
+ echo "Waiting server in $*..." && \
+ $(MAKE) -C $* wait-for-server; \
+ else \
+ echo "❌ Error: no Makefile found in $*"; \
+ exit 1; \
+ fi
+
# 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
@@ -66,46 +84,75 @@ 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 --info --parallel --no-daemon integ \
+ $(if $(TEST),--tests "$(TEST)",) \
+ -Dtest.filter.servers="$(FILTER)"
@echo "Tests completed successfully"
# Stop the servers
-stop-servers:
+test-servers-stop:
@echo "Stopping servers..."
- @if [ -f python-server.pid ]; then \
- kill $$(cat python-server.pid) 2>/dev/null || true; \
- rm python-server.pid; \
- fi
- @if [ -f java-server.pid ]; then \
- kill $$(cat java-server.pid) 2>/dev/null || true; \
- rm java-server.pid; \
- fi
+ @for dir in $(SERVER_DIRS); do \
+ echo "Stopping 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-server.log java-server.log
- @echo "Cleanup complete"
-
# 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 Python and Java 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 " 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 " 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
@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
+ @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..."; \
+ 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 3; \
+ 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
+
+duvet:
+ @echo "Running duvet reports..."
+ @for dir in $(SERVER_DIRS); do \
+ echo "Running make duvet in $$dir..."; \
+ $(MAKE) -C $$dir duvet; \
+ done
diff --git a/test-server/README.md b/test-server/README.md
index a320d1d1..48187fc3 100644
--- a/test-server/README.md
+++ b/test-server/README.md
@@ -28,11 +28,8 @@ make ci
# Start Python and Java servers in parallel
make start-servers
-# Start only the Python server
-make start-python-server
-
-# Start only the Java server
-make start-java-server
+# Start only the Python S3EC V3 server
+make start-python-v3-server
# Run Java tests
make run-tests
@@ -59,3 +56,28 @@ 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`
+
+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-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/CMakeLists.txt b/test-server/cpp-v2-transition-server/CMakeLists.txt
new file mode 100644
index 00000000..b282dbc4
--- /dev/null
+++ b/test-server/cpp-v2-transition-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-v2-transition-server/Makefile b/test-server/cpp-v2-transition-server/Makefile
new file mode 100644
index 00000000..0383b4d8
--- /dev/null
+++ b/test-server/cpp-v2-transition-server/Makefile
@@ -0,0 +1,37 @@
+# Makefile for S3 Encryption Client Testing
+
+.PHONY: build-server start-server stop-server wait-for-server
+
+PID_FILE := server.pid
+PORT := 8097
+
+build/s3ec-server:
+ mkdir -p build && cd build && cmake ..
+
+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 > ../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 \
+ 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-transition-server/README.md b/test-server/cpp-v2-transition-server/README.md
new file mode 100644
index 00000000..8e77feda
--- /dev/null
+++ b/test-server/cpp-v2-transition-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-v2-transition-server/aws-sdk-cpp b/test-server/cpp-v2-transition-server/aws-sdk-cpp
new file mode 160000
index 00000000..9110b0ff
--- /dev/null
+++ b/test-server/cpp-v2-transition-server/aws-sdk-cpp
@@ -0,0 +1 @@
+Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c
diff --git a/test-server/cpp-v2-transition-server/main.cpp b/test-server/cpp-v2-transition-server/main.cpp
new file mode 100644
index 00000000..9e9f942d
--- /dev/null
+++ b/test-server/cpp-v2-transition-server/main.cpp
@@ -0,0 +1,748 @@
+/*
+ * 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
+#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-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 =
+ 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 + "\"}";
+}
+
+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) {
+ // 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");
+ std::string encryptionAlgorithm = get_config(request, "encryptionAlgorithm");
+
+ if (unsupported(commitmentPolicy, encryptionAlgorithm)) {
+ send_response(connection, 404, "{\"error\":\"Unsupported Option.\"}");
+ return MHD_YES;
+ }
+
+ // 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;
+ if (request["config"].contains("instructionFileConfig") &&
+ request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) {
+ inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"];
+ }
+
+ // 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);
+ if (legacy2)
+ config->SetUnAuthenticatedRangeGet(RangeGetMode::ALL);
+ if (inst_put)
+ config->SetStorageMethod(StorageMethod::INSTRUCTION_FILE);
+
+ // Create S3EncryptionClientV2 with standard configuration
+ Aws::Client::ClientConfiguration clientConfig;
+ clientConfig.maxConnections = 512; // Large pool per client
+ clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard");
+ auto encryption_client = std::make_shared(*config, clientConfig);
+
+ std::string client_id = generate_uuid();
+ set_client(client_id, encryption_client);
+
+ 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 (...) {
+ 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-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
+ 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-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);
+
+ // 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-TRANSITION] [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-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\"}");
+ }
+
+ 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-TRANSITION] [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-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::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) {
+ 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,
+ 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);
+
+ 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-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) {
+ try {
+ std::string method_str(method);
+ std::string url_str(url);
+ bool is_push = method_str == "POST" || method_str == "PUT";
+
+ // LOG: Every request entry (even first-time calls)
+ if (*con_cls == nullptr) {
+ fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST START: method=%s, url=%s, version=%s, con_cls=NULL, upload_data_size=%zu\n",
+ method, url, version, *upload_data_size);
+ }
+
+ // Initialize request context on first call
+ if (*con_cls == nullptr) {
+ // Allocate unique state for each request to avoid race conditions
+ *con_cls = new std::string();
+ fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST INIT: allocated new request context for %s %s\n", method, url);
+ return MHD_YES;
+ }
+
+ // LOG: Subsequent calls
+ if (is_push && *upload_data_size > 0) {
+ fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST DATA: %s %s receiving %zu bytes\n", method, url, *upload_data_size);
+ } else if (*upload_data_size == 0) {
+ fprintf(stderr, "[CPP-V2-TRANSITION] REQUEST COMPLETE: %s %s ready for processing\n", method, url);
+ }
+
+ // Accumulate request body data for POST/PUT requests
+ if (is_push && *upload_data_size > 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-TRANSITION] PROCESSING: %s %s\n", method, url);
+
+ // Handle client creation endpoint
+ if (is_push && url_str == "/client") {
+ fprintf(stderr, "[CPP-V2-TRANSITION] Handling /client endpoint\n");
+ std::string *body = static_cast(*con_cls);
+ 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") {
+ 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") {
+ 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 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_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-TRANSITION] 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/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..3a49ac85
--- /dev/null
+++ b/test-server/cpp-v3-server/.duvet/config.toml
@@ -0,0 +1,45 @@
+'$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 = "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"
+
+[[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-v3-server/CMakeLists.txt b/test-server/cpp-v3-server/CMakeLists.txt
new file mode 100644
index 00000000..0faac5f0
--- /dev/null
+++ b/test-server/cpp-v3-server/CMakeLists.txt
@@ -0,0 +1,49 @@
+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")
+set(ENABLE_ADDRESS_SANITIZER ON CACHE BOOL "Enable Address Sanitizer")
+
+# 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)
+
+# 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
+ ${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
+)
diff --git a/test-server/cpp-v3-server/Makefile b/test-server/cpp-v3-server/Makefile
new file mode 100644
index 00000000..e90c8d73
--- /dev/null
+++ b/test-server/cpp-v3-server/Makefile
@@ -0,0 +1,43 @@
+# Makefile for S3 Encryption Client Testing
+
+.PHONY: build-server start-server stop-server wait-for-server
+
+PID_FILE := server.pid
+PORT := 8091
+
+build/s3ec-server:
+ mkdir -p build && cd build && cmake ..
+
+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 > ../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 \
+ 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/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/aws-sdk-cpp b/test-server/cpp-v3-server/aws-sdk-cpp
new file mode 160000
index 00000000..9110b0ff
--- /dev/null
+++ b/test-server/cpp-v3-server/aws-sdk-cpp
@@ -0,0 +1 @@
+Subproject commit 9110b0ff85094134a4f78316485fd7fe754a2a9c
diff --git a/test-server/cpp-v3-server/compliance.txt b/test-server/cpp-v3-server/compliance.txt
new file mode 100644
index 00000000..8225d8a9
--- /dev/null
+++ b/test-server/cpp-v3-server/compliance.txt
@@ -0,0 +1,119 @@
+** 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
+//# 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.
+
+//= ../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
+//= type=exception
+//# 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 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 **
+
+//= ../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/client.md#optional-api-operations
+//= type=exception
+//# - 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.
diff --git a/test-server/cpp-v3-server/main.cpp b/test-server/cpp-v3-server/main.cpp
new file mode 100644
index 00000000..169fa517
--- /dev/null
+++ b/test-server/cpp-v3-server/main.cpp
@@ -0,0 +1,776 @@
+/*
+ * 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
+#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-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 =
+ 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) {
+ // 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);
+
+ // 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;
+ if (request["config"].contains("instructionFileConfig") &&
+ request["config"]["instructionFileConfig"].contains("enableInstructionFilePutObject")) {
+ inst_put = request["config"]["instructionFileConfig"]["enableInstructionFilePutObject"];
+ }
+
+ 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" ||
+ encryptionAlgorithm == "ALG_AES_256_CBC_IV16_NO_KDF") {
+ return unsupported(connection, commitmentPolicy, encryptionAlgorithm);
+ }
+ config->SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_REQUIRE_DECRYPT);
+ } else if (commitmentPolicy == "REQUIRE_ENCRYPT_ALLOW_DECRYPT") {
+ if (encryptionAlgorithm == "ALG_AES_256_GCM_IV12_TAG16_NO_KDF") {
+ return unsupported(connection, commitmentPolicy, encryptionAlgorithm);
+ }
+ config->SetCommitmentPolicy(CommitmentPolicy::REQUIRE_ENCRYPT_ALLOW_DECRYPT);
+ } else if (commitmentPolicy == "FORBID_ENCRYPT_ALLOW_DECRYPT") {
+ if (encryptionAlgorithm == "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY") {
+ return unsupported(connection, commitmentPolicy, encryptionAlgorithm);
+ }
+ config->SetCommitmentPolicy(CommitmentPolicy::FORBID_ENCRYPT_ALLOW_DECRYPT);
+ }
+
+ // Create S3EncryptionClientV3 with standard configuration
+ Aws::Client::ClientConfiguration clientConfig;
+ clientConfig.maxConnections = 512; // Large pool per client
+ clientConfig.retryStrategy = Aws::Client::InitRetryStrategy("standard");
+
+ // 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
+ // 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, "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-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
+ 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-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);
+
+ // 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-V3] [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-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\"}");
+ }
+
+ 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-V3] [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-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::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, "[CPP-V3] GetObject AWS error: %s\n", msg.c_str());
+ return send_response(connection, 500, msg);
+ }
+ } catch (const std::exception &e) {
+ fprintf(stderr, "[CPP-V3] [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,
+ 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);
+
+ 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-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, "[CPP-V3] PutObject AWS error: %s\n", msg.c_str());
+ return send_response(connection, 500, msg);
+ }
+ } catch (const std::exception &e) {
+ 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) {
+ try {
+ std::string method_str(method);
+ std::string url_str(url);
+ bool is_push = method_str == "POST" || method_str == "PUT";
+
+ // LOG: Every request entry (even first-time calls)
+ if (*con_cls == nullptr) {
+ fprintf(stderr, "[CPP-V3] REQUEST START: method=%s, url=%s, version=%s, con_cls=NULL, upload_data_size=%zu\n",
+ method, url, version, *upload_data_size);
+ }
+
+ // Initialize request context on first call
+ if (*con_cls == nullptr) {
+ // Allocate unique state for each request to avoid race conditions
+ *con_cls = new std::string();
+ fprintf(stderr, "[CPP-V3] REQUEST INIT: allocated new request context for %s %s\n", method, url);
+ return MHD_YES;
+ }
+
+ // LOG: Subsequent calls
+ if (is_push && *upload_data_size > 0) {
+ fprintf(stderr, "[CPP-V3] REQUEST DATA: %s %s receiving %zu bytes\n", method, url, *upload_data_size);
+ } else if (*upload_data_size == 0) {
+ fprintf(stderr, "[CPP-V3] REQUEST COMPLETE: %s %s ready for processing\n", method, url);
+ }
+
+ // Accumulate request body data for POST/PUT requests
+ if (is_push && *upload_data_size > 0) {
+ std::string *body = static_cast(*con_cls);
+ 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);
+
+ // Handle client creation endpoint
+ if (is_push && url_str == "/client") {
+ fprintf(stderr, "[CPP-V3] Handling /client endpoint\n");
+ std::string *body = static_cast(*con_cls);
+ MHD_Result result = handle_create_client(connection, *body);
+ fprintf(stderr, "[CPP-V3] /client handler returned: %d\n", result);
+ return result;
+ }
+
+ // Handle object operations
+ if (url_str.find("/object/") == 0) {
+ fprintf(stderr, "[CPP-V3] Handling /object/ endpoint\n");
+ std::string path = url_str.substr(8); // Remove "/object/"
+ size_t slash_pos = path.find('/');
+ if (slash_pos != std::string::npos) {
+ 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") {
+ 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") {
+ fprintf(stderr, "[CPP-V3] Dispatching to handle_put_object\n");
+ std::string *body = static_cast(*con_cls);
+ MHD_Result result = handle_put_object(connection, bucket, key, client_id, *body, metadata);
+ fprintf(stderr, "[CPP-V3] handle_put_object returned: %d\n", result);
+ return result;
+ } else {
+ fprintf(stderr, "[CPP-V3] Method not allowed: %s\n", method);
+ return send_response(connection, 405, "{\"error\":\"Method not allowed\"}");
+ }
+ }
+ }
+
+ // Return error for unrecognized endpoints
+ 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_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);
+ 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/go-v3-transition-server/.duvet/.gitignore b/test-server/go-v3-transition-server/.duvet/.gitignore
new file mode 100644
index 00000000..32ad579b
--- /dev/null
+++ b/test-server/go-v3-transition-server/.duvet/.gitignore
@@ -0,0 +1,3 @@
+reports/
+requirements/
+specification/
diff --git a/test-server/go-v3-transition-server/.duvet/config.toml b/test-server/go-v3-transition-server/.duvet/config.toml
new file mode 100644
index 00000000..713e72d3
--- /dev/null
+++ b/test-server/go-v3-transition-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-v3-transition-server/Makefile b/test-server/go-v3-transition-server/Makefile
new file mode 100644
index 00000000..a254acdf
--- /dev/null
+++ b/test-server/go-v3-transition-server/Makefile
@@ -0,0 +1,39 @@
+# Makefile for S3 Encryption Client Testing
+
+.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..."
+ 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 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 \
+ 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-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..bf8a12f6
--- /dev/null
+++ b/test-server/go-v3-transition-server/local-go-s3ec
@@ -0,0 +1 @@
+Subproject commit bf8a12f61694d750a13a44f0a691dd7ced0ff904
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..64556f12
--- /dev/null
+++ b/test-server/go-v3-transition-server/main.go
@@ -0,0 +1,385 @@
+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/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
+ 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"`
+ 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"),
+ 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-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"),
+ 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
+ }
+
+ 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 (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-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 (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)
+
+ 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/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
new file mode 100644
index 00000000..6c549db2
--- /dev/null
+++ b/test-server/go-v4-server/Makefile
@@ -0,0 +1,39 @@
+# Makefile for S3 Encryption Client Testing
+
+.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..."
+ 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 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 \
+ 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-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..33b1cc9f
--- /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.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
+ 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..f4e3646a
--- /dev/null
+++ b/test-server/go-v4-server/go.sum
@@ -0,0 +1,44 @@
+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..bf8a12f6
--- /dev/null
+++ b/test-server/go-v4-server/local-go-s3ec
@@ -0,0 +1 @@
+Subproject commit bf8a12f61694d750a13a44f0a691dd7ced0ff904
diff --git a/test-server/go-v4-server/main.go b/test-server/go-v4-server/main.go
new file mode 100644
index 00000000..50999e95
--- /dev/null
+++ b/test-server/go-v4-server/main.go
@@ -0,0 +1,385 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "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"
+ "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"
+ "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
+ 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"`
+ 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"),
+ 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.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"),
+ 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
+ }
+
+ 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.S3EncryptionClientV4
+ 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 (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 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 (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)
+
+ 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-server/gradle.properties b/test-server/java-server/gradle.properties
deleted file mode 100644
index 08afce82..00000000
--- a/test-server/java-server/gradle.properties
+++ /dev/null
@@ -1,11 +0,0 @@
-# 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-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java
deleted file mode 100644
index d992c435..00000000
--- a/test-server/java-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java
+++ /dev/null
@@ -1,109 +0,0 @@
-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-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java
deleted file mode 100644
index e7c5493f..00000000
--- a/test-server/java-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java
+++ /dev/null
@@ -1,72 +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)));
-
- 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-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java
deleted file mode 100644
index 036289ec..00000000
--- a/test-server/java-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-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java
deleted file mode 100644
index 4c772673..00000000
--- a/test-server/java-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-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/build.gradle.kts b/test-server/java-tests/build.gradle.kts
index f35a2ac6..2d1cbdeb 100644
--- a/test-server/java-tests/build.gradle.kts
+++ b/test-server/java-tests/build.gradle.kts
@@ -16,7 +16,11 @@ 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")
}
@@ -46,6 +50,30 @@ tasks {
useJUnitPlatform()
testClassesDirs = sourceSets["it"].output.classesDirs
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
+ // // 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/CBCDecryptTests.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java
new file mode 100644
index 00000000..093ba2ab
--- /dev/null
+++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/CBCDecryptTests.java
@@ -0,0 +1,184 @@
+/*
+* 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)
+ .enableLegacyWrappingAlgorithms(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)
+ .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
+ .enableLegacyUnauthenticatedModes(true)
+ .enableLegacyWrappingAlgorithms(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)
+ .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
+ .enableLegacyUnauthenticatedModes(true)
+ .enableLegacyWrappingAlgorithms(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)
+ .enableLegacyWrappingAlgorithms(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/ExhaustiveRoundTripTests1_25.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java
new file mode 100644
index 00000000..b161feb6
--- /dev/null
+++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/ExhaustiveRoundTripTests1_25.java
@@ -0,0 +1,232 @@
+/*
+ * 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.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
+ *
+ * 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)
+ .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
+ .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)
+ .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
+ .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
+ ) throws Exception {
+
+ S3ECTestServerClient encClient = TestUtils.testServerClientFor(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)
+ .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)
+ .encryptionAlgorithm(EncryptionAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
+ .build())
+ .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)
+ .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/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/InstructionFileFailures.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java
new file mode 100644
index 00000000..18ebca47
--- /dev/null
+++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/InstructionFileFailures.java
@@ -0,0 +1,1136 @@
+/*
+* 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 software.amazon.encryption.s3.TestUtils.*;
+
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.*;
+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 software.amazon.encryption.s3.TestUtils.LanguageServerTarget;
+import software.amazon.encryption.s3.client.S3ECTestServerClient;
+import software.amazon.encryption.s3.model.*;
+
+/**
+* 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";
+ 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
+ *
+ * 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<>());
+
+ // 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 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;
+
+ @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;
+ }
+
+ static List getCrossLanguageObjectsMetadataOnly() {
+ return new ArrayList<>(crossLanguageObjectsMetadataOnly);
+ }
+
+ 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()))
+ .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
+ );
+ }
+
+ @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
+ );
+ }
+
+ @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()) {
+ 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
+ );
+
+ }
+
+ // 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
+ }
+ }
+
+ // 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
+ );
+ }
+ }
+ }
+
+ 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 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;
+
+ @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();
+ crossLanguageObjectsMetadataOnly = EncryptTests.getCrossLanguageObjectsMetadataOnly();
+ crossLanguageObjectsInstructionFileDeleted = EncryptTests.getCrossLanguageObjectsInstructionFileDeleted();
+ crossLanguageObjectsInstructionFileManipulatedV3 = EncryptTests.getCrossLanguageObjectsInstructionFileManipulatedV3();
+ crossLanguageObjectsInstructionFileManipulatedV2 = EncryptTests.getCrossLanguageObjectsInstructionFileManipulatedV2();
+ 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
+ );
+ }
+
+ // 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
+ );
+ }
+
+ @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/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/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