mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 03:46:37 +00:00
898aa4db95
* chore: adjudicate the last 18 bare dead_code allows in the library crates Finishes backlog#1823 step 10 outside `rustfs/src` and `protocols`: config, s3select-query, common, madmin, heal, ecstore, signer and notify. Stripped first, then clippy asked which the compiler actually missed — 8 of the 18 were inert. Seven items are deleted, each checked by grep as well as by clippy: - `common/last_minute.rs`'s private `TimedAction` (with its impl) and `SizeCategory` (with its `Display` impl). The file's public surface — `AccElem`, `LastMinuteLatency` — stays; ecstore consumes it. - `s3select-query`'s three `with_*` builders. `DefaultLogicalOptimizer::with_optimizer_rules` looks used, but the call in the same file is `SessionStateBuilder::with_optimizer_rules` from DataFusion; the local methods have no callers. - `heal/manager.rs`'s `contains_key`. Its six apparent references are all `HashMap::contains_key`. Three keep their code: - `heal/storage.rs`'s `Test` variant is constructed by the `#[cfg(test)] test()` helper, which the lib target cannot see, so it takes a reasoned allow. - `signer`'s `STREAMING_PAYLOAD_HDR` and `try_build_chunk_string_to_sign` gain the `_` prefix instead. That file already marks deliberately-unheld code that way — `_STREAMING_TRAILER_HDR`, `_PAYLOAD_CHUNK_SIZE`, and `_try_build_chunk_signature`, which is the only caller of that function. Following the existing convention removes the allow without an attribute. `protocols` keeps its four; that crate needs `--features swift,sftp` to compile fully and is verified differently. The four `#![allow(dead_code)]` in `e2e_test` are module-root blankets in test-support files, which belong to steps 1-5 rather than step 10. Refs backlog#1823 * chore(e2e_test): adjudicate the two dead_code allows the lib test target still needs `cargo clippy --all-targets` compiles e2e_test's lib test target, which the earlier pass did not cover, so these two removals only surfaced in CI. test_large_multipart_upload's allow was load-bearing: its call site in test_local_kms_multipart_upload is commented out behind "TODO: Re-enable after fixing streaming encryption issues with large files". The allow comes back with the reason string this batch uses everywhere else, so the next reader sees why it is parked instead of deleting a test we intend to run again. TestDefinition.category was the opposite: written at all six definitions, read nowhere, and its enum's impl block is empty. The live copy of that type is crates/e2e_test/src/kms/test_runner.rs, which has an as_str; the policy copy is a vestige of it. Dropping the field, the enum, and the constructor parameter leaves the runner unchanged — it dispatches on name and filters on is_critical. Verification: cargo clippy --all-targets -- -D warnings (workspace, the CI command) and cargo fmt --all --check both pass. --------- Co-authored-by: houseme <housemecn@gmail.com>
760 lines
26 KiB
Rust
760 lines
26 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! End-to-end tests for Local KMS backend
|
|
//!
|
|
//! This test suite validates complete workflow including:
|
|
//! - Dynamic KMS configuration via HTTP admin API
|
|
//! - S3 object upload/download with SSE-S3, SSE-KMS, SSE-C encryption
|
|
//! - Complete encryption/decryption lifecycle
|
|
|
|
use super::common::{
|
|
LocalKMSTestEnvironment, get_kms_status, skip_if_kms_admin_tool_unavailable, sse_customer_key_md5_base64,
|
|
test_kms_key_management, test_sse_c_encryption,
|
|
};
|
|
use crate::common::{TEST_BUCKET, init_logging};
|
|
use tracing::{error, info};
|
|
|
|
#[tokio::test]
|
|
async fn test_local_kms_end_to_end() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
if skip_if_kms_admin_tool_unavailable("test_local_kms_end_to_end") {
|
|
return Ok(());
|
|
}
|
|
info!("Starting Local KMS End-to-End Test");
|
|
|
|
// Create LocalKMS test environment
|
|
let mut kms_env = LocalKMSTestEnvironment::new()
|
|
.await
|
|
.expect("Failed to create LocalKMS test environment");
|
|
|
|
// Start RustFS with Local KMS backend (KMS should be auto-started with --kms-backend local)
|
|
let default_key_id = kms_env
|
|
.start_rustfs_for_local_kms()
|
|
.await
|
|
.expect("Failed to start RustFS with Local KMS");
|
|
|
|
// Wait a moment for RustFS to fully start up and initialize KMS
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
|
|
|
info!("RustFS started with KMS auto-configuration, default_key_id: {}", default_key_id);
|
|
|
|
// Verify KMS status
|
|
match get_kms_status(&kms_env.base_env.url, &kms_env.base_env.access_key, &kms_env.base_env.secret_key).await {
|
|
Ok(status) => {
|
|
info!("KMS Status after auto-configuration: {}", status);
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to get KMS status after auto-configuration: {}", e);
|
|
return Err(e);
|
|
}
|
|
}
|
|
|
|
// Create S3 client and test bucket
|
|
let s3_client = kms_env.base_env.create_s3_client();
|
|
kms_env
|
|
.base_env
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
// Test KMS Key Management APIs
|
|
test_kms_key_management(&kms_env.base_env.url, &kms_env.base_env.access_key, &kms_env.base_env.secret_key)
|
|
.await
|
|
.expect("KMS key management test failed");
|
|
|
|
// Test different encryption methods
|
|
test_sse_c_encryption(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("SSE-C encryption test failed");
|
|
|
|
info!("SSE-C encryption test completed successfully, ending test early for debugging");
|
|
|
|
// TEMPORARILY COMMENTED OUT FOR DEBUGGING:
|
|
// // Wait a moment and verify KMS is ready for SSE-S3
|
|
// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
// match get_kms_status(&kms_env.base_env.url, &kms_env.base_env.access_key, &kms_env.base_env.secret_key).await {
|
|
// Ok(status) => info!("KMS Status before SSE-S3 test: {}", status),
|
|
// Err(e) => warn!("Failed to get KMS status before SSE-S3 test: {}", e),
|
|
// }
|
|
|
|
// test_sse_s3_encryption(&s3_client, TEST_BUCKET).await
|
|
// .expect("SSE-S3 encryption test failed");
|
|
|
|
// // Test SSE-KMS encryption
|
|
// test_sse_kms_encryption(&s3_client, TEST_BUCKET).await
|
|
// .expect("SSE-KMS encryption test failed");
|
|
|
|
// // Test error scenarios
|
|
// test_error_scenarios(&s3_client, TEST_BUCKET).await
|
|
// .expect("Error scenarios test failed");
|
|
|
|
// Clean up
|
|
kms_env
|
|
.base_env
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Local KMS End-to-End Test completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_local_kms_key_isolation() {
|
|
init_logging();
|
|
info!("Starting Local KMS Key Isolation Test");
|
|
|
|
let mut kms_env = LocalKMSTestEnvironment::new()
|
|
.await
|
|
.expect("Failed to create LocalKMS test environment");
|
|
|
|
// Start RustFS with Local KMS backend (KMS should be auto-started with --kms-backend local)
|
|
let default_key_id = kms_env
|
|
.start_rustfs_for_local_kms()
|
|
.await
|
|
.expect("Failed to start RustFS with Local KMS");
|
|
|
|
// Wait a moment for RustFS to fully start up and initialize KMS
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
|
|
|
info!("RustFS started with KMS auto-configuration, default_key_id: {}", default_key_id);
|
|
|
|
let s3_client = kms_env.base_env.create_s3_client();
|
|
kms_env
|
|
.base_env
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
// Test that different SSE-C keys create isolated encrypted objects
|
|
let key1 = "01234567890123456789012345678901";
|
|
let key2 = "98765432109876543210987654321098";
|
|
let key1_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key1);
|
|
let key2_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key2);
|
|
let key1_md5 = sse_customer_key_md5_base64(key1);
|
|
let key2_md5 = sse_customer_key_md5_base64(key2);
|
|
|
|
let data1 = b"Data encrypted with key 1";
|
|
let data2 = b"Data encrypted with key 2";
|
|
|
|
// Upload two objects with different SSE-C keys
|
|
s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("object1")
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(data1.to_vec()))
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key1_b64)
|
|
.sse_customer_key_md5(&key1_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload object1");
|
|
|
|
s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("object2")
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(data2.to_vec()))
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key2_b64)
|
|
.sse_customer_key_md5(&key2_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload object2");
|
|
|
|
// Verify each object can only be decrypted with its own key
|
|
let get1 = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("object1")
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key1_b64)
|
|
.sse_customer_key_md5(&key1_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to get object1 with key1");
|
|
|
|
let retrieved_data1 = get1.body.collect().await.expect("Failed to read object1 body").into_bytes();
|
|
assert_eq!(retrieved_data1.as_ref(), data1);
|
|
|
|
// Try to access object1 with key2 - should fail
|
|
let wrong_key_result = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("object1")
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key2_b64)
|
|
.sse_customer_key_md5(&key2_md5)
|
|
.send()
|
|
.await;
|
|
|
|
assert!(wrong_key_result.is_err(), "Should not be able to decrypt object1 with key2");
|
|
|
|
kms_env
|
|
.base_env
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Local KMS Key Isolation Test completed successfully");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_local_kms_large_file() {
|
|
init_logging();
|
|
info!("Starting Local KMS Large File Test");
|
|
|
|
let mut kms_env = LocalKMSTestEnvironment::new()
|
|
.await
|
|
.expect("Failed to create LocalKMS test environment");
|
|
|
|
// Start RustFS with Local KMS backend (KMS should be auto-started with --kms-backend local)
|
|
let default_key_id = kms_env
|
|
.start_rustfs_for_local_kms()
|
|
.await
|
|
.expect("Failed to start RustFS with Local KMS");
|
|
|
|
// Wait a moment for RustFS to fully start up and initialize KMS
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
|
|
|
info!("RustFS started with KMS auto-configuration, default_key_id: {}", default_key_id);
|
|
|
|
let s3_client = kms_env.base_env.create_s3_client();
|
|
kms_env
|
|
.base_env
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
// Test progressively larger file sizes to find the exact threshold where encryption fails
|
|
// Starting with 1MB to reproduce the issue first
|
|
let large_data = vec![0xABu8; 1024 * 1024];
|
|
let object_key = "large-encrypted-file";
|
|
|
|
// Test SSE-S3 with large file
|
|
let put_response = s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(object_key)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(large_data.clone()))
|
|
.server_side_encryption(aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload large file with SSE-S3");
|
|
|
|
assert_eq!(
|
|
put_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
// Download and verify
|
|
let get_response = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(object_key)
|
|
.send()
|
|
.await
|
|
.expect("Failed to download large file");
|
|
|
|
// Verify SSE-S3 encryption header in GET response
|
|
assert_eq!(
|
|
get_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
let downloaded_data = get_response
|
|
.body
|
|
.collect()
|
|
.await
|
|
.expect("Failed to read large file body")
|
|
.into_bytes();
|
|
|
|
assert_eq!(downloaded_data.len(), large_data.len());
|
|
assert_eq!(&downloaded_data[..], &large_data[..]);
|
|
|
|
kms_env
|
|
.base_env
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Local KMS Large File Test completed successfully");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_local_kms_multipart_upload() {
|
|
init_logging();
|
|
info!("Starting Local KMS Multipart Upload Test");
|
|
|
|
let mut kms_env = LocalKMSTestEnvironment::new()
|
|
.await
|
|
.expect("Failed to create LocalKMS test environment");
|
|
|
|
// Start RustFS with Local KMS backend
|
|
let default_key_id = kms_env
|
|
.start_rustfs_for_local_kms()
|
|
.await
|
|
.expect("Failed to start RustFS with Local KMS");
|
|
|
|
// Wait for KMS initialization
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
|
|
|
info!("RustFS started with KMS auto-configuration, default_key_id: {}", default_key_id);
|
|
|
|
let s3_client = kms_env.base_env.create_s3_client();
|
|
kms_env
|
|
.base_env
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
// Test multipart upload with different encryption types
|
|
|
|
// Test 1: Multipart upload with SSE-S3 (focus on this first)
|
|
info!("Testing multipart upload with SSE-S3");
|
|
test_multipart_upload_with_sse_s3(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("SSE-S3 multipart upload test failed");
|
|
|
|
// Test 2: Multipart upload with SSE-KMS
|
|
info!("Testing multipart upload with SSE-KMS");
|
|
test_multipart_upload_with_sse_kms(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("SSE-KMS multipart upload test failed");
|
|
|
|
// Test 3: Multipart upload with SSE-C
|
|
info!("Testing multipart upload with SSE-C");
|
|
test_multipart_upload_with_sse_c(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("SSE-C multipart upload test failed");
|
|
|
|
// Test 4: Large multipart upload (test streaming encryption with multiple blocks)
|
|
// TODO: Re-enable after fixing streaming encryption issues with large files
|
|
// info!("Testing large multipart upload with streaming encryption");
|
|
// test_large_multipart_upload(&s3_client, TEST_BUCKET).await
|
|
// .expect("Large multipart upload test failed");
|
|
|
|
// Clean up
|
|
kms_env
|
|
.base_env
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Local KMS Multipart Upload Test completed successfully");
|
|
}
|
|
|
|
/// Test multipart upload with SSE-S3 encryption
|
|
async fn test_multipart_upload_with_sse_s3(
|
|
s3_client: &aws_sdk_s3::Client,
|
|
bucket: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let object_key = "multipart-sse-s3-test";
|
|
let part_size = 5 * 1024 * 1024; // 5MB per part (minimum S3 multipart size)
|
|
let total_parts = 2;
|
|
let total_size = part_size * total_parts;
|
|
|
|
// Generate test data
|
|
let test_data: Vec<u8> = (0..total_size).map(|i| (i % 256) as u8).collect();
|
|
|
|
// Step 1: Initiate multipart upload with SSE-S3
|
|
let create_multipart_output = s3_client
|
|
.create_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.server_side_encryption(aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
.send()
|
|
.await?;
|
|
|
|
let upload_id = create_multipart_output.upload_id().unwrap();
|
|
info!("Created multipart upload with SSE-S3, upload_id: {}", upload_id);
|
|
|
|
// Note: CreateMultipartUpload response may not include server_side_encryption header in some implementations
|
|
// The encryption will be verified in the final GetObject response
|
|
if let Some(sse) = create_multipart_output.server_side_encryption() {
|
|
info!("CreateMultipartUpload response includes SSE: {:?}", sse);
|
|
assert_eq!(sse, &aws_sdk_s3::types::ServerSideEncryption::Aes256);
|
|
} else {
|
|
info!("CreateMultipartUpload response does not include SSE header (implementation specific)");
|
|
}
|
|
|
|
// Step 2: Upload parts
|
|
info!("CLAUDE TEST DEBUG: Starting to upload {} parts", total_parts);
|
|
let mut completed_parts = Vec::new();
|
|
for part_number in 1..=total_parts {
|
|
let start = (part_number - 1) * part_size;
|
|
let end = std::cmp::min(start + part_size, total_size);
|
|
let part_data = &test_data[start..end];
|
|
|
|
let upload_part_output = s3_client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.part_number(part_number as i32)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(part_data.to_vec()))
|
|
.send()
|
|
.await?;
|
|
|
|
let etag = upload_part_output.e_tag().unwrap().to_string();
|
|
completed_parts.push(
|
|
aws_sdk_s3::types::CompletedPart::builder()
|
|
.part_number(part_number as i32)
|
|
.e_tag(&etag)
|
|
.build(),
|
|
);
|
|
|
|
info!("CLAUDE TEST DEBUG: Uploaded part {} with etag: {}", part_number, etag);
|
|
}
|
|
|
|
// Step 3: Complete multipart upload
|
|
let completed_multipart_upload = aws_sdk_s3::types::CompletedMultipartUpload::builder()
|
|
.set_parts(Some(completed_parts))
|
|
.build();
|
|
|
|
info!("CLAUDE TEST DEBUG: About to call complete_multipart_upload");
|
|
let complete_output = s3_client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.multipart_upload(completed_multipart_upload)
|
|
.send()
|
|
.await?;
|
|
|
|
info!(
|
|
"CLAUDE TEST DEBUG: complete_multipart_upload succeeded, etag: {:?}",
|
|
complete_output.e_tag()
|
|
);
|
|
|
|
// Step 4: Try a HEAD request to debug metadata before GET
|
|
let head_response = s3_client.head_object().bucket(bucket).key(object_key).send().await?;
|
|
|
|
info!("CLAUDE TEST DEBUG: HEAD response metadata: {:?}", head_response.metadata());
|
|
info!("CLAUDE TEST DEBUG: HEAD response SSE: {:?}", head_response.server_side_encryption());
|
|
|
|
// Step 5: Download and verify
|
|
let get_response = s3_client.get_object().bucket(bucket).key(object_key).send().await?;
|
|
|
|
// Verify encryption headers
|
|
assert_eq!(
|
|
get_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
let downloaded_data = get_response.body.collect().await?.into_bytes();
|
|
assert_eq!(downloaded_data.len(), total_size);
|
|
assert_eq!(&downloaded_data[..], &test_data[..]);
|
|
|
|
info!("✅ SSE-S3 multipart upload test passed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test multipart upload with SSE-KMS encryption
|
|
async fn test_multipart_upload_with_sse_kms(
|
|
s3_client: &aws_sdk_s3::Client,
|
|
bucket: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let object_key = "multipart-sse-kms-test";
|
|
let part_size = 5 * 1024 * 1024; // 5MB per part (minimum S3 multipart size)
|
|
let total_parts = 2;
|
|
let total_size = part_size * total_parts;
|
|
|
|
// Generate test data
|
|
let test_data: Vec<u8> = (0..total_size).map(|i| ((i / 1000) % 256) as u8).collect();
|
|
|
|
// Step 1: Initiate multipart upload with SSE-KMS
|
|
let create_multipart_output = s3_client
|
|
.create_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.server_side_encryption(aws_sdk_s3::types::ServerSideEncryption::AwsKms)
|
|
.send()
|
|
.await?;
|
|
|
|
let upload_id = create_multipart_output.upload_id().unwrap();
|
|
|
|
// Note: CreateMultipartUpload response may not include server_side_encryption header in some implementations
|
|
if let Some(sse) = create_multipart_output.server_side_encryption() {
|
|
info!("CreateMultipartUpload response includes SSE-KMS: {:?}", sse);
|
|
assert_eq!(sse, &aws_sdk_s3::types::ServerSideEncryption::AwsKms);
|
|
} else {
|
|
info!("CreateMultipartUpload response does not include SSE-KMS header (implementation specific)");
|
|
}
|
|
|
|
// Step 2: Upload parts
|
|
let mut completed_parts = Vec::new();
|
|
for part_number in 1..=total_parts {
|
|
let start = (part_number - 1) * part_size;
|
|
let end = std::cmp::min(start + part_size, total_size);
|
|
let part_data = &test_data[start..end];
|
|
|
|
let upload_part_output = s3_client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.part_number(part_number as i32)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(part_data.to_vec()))
|
|
.send()
|
|
.await?;
|
|
|
|
let etag = upload_part_output.e_tag().unwrap().to_string();
|
|
completed_parts.push(
|
|
aws_sdk_s3::types::CompletedPart::builder()
|
|
.part_number(part_number as i32)
|
|
.e_tag(&etag)
|
|
.build(),
|
|
);
|
|
}
|
|
|
|
// Step 3: Complete multipart upload
|
|
let completed_multipart_upload = aws_sdk_s3::types::CompletedMultipartUpload::builder()
|
|
.set_parts(Some(completed_parts))
|
|
.build();
|
|
|
|
let _complete_output = s3_client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.multipart_upload(completed_multipart_upload)
|
|
.send()
|
|
.await?;
|
|
|
|
// Step 4: Download and verify
|
|
let get_response = s3_client.get_object().bucket(bucket).key(object_key).send().await?;
|
|
|
|
assert_eq!(
|
|
get_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::AwsKms)
|
|
);
|
|
|
|
let downloaded_data = get_response.body.collect().await?.into_bytes();
|
|
assert_eq!(downloaded_data.len(), total_size);
|
|
assert_eq!(&downloaded_data[..], &test_data[..]);
|
|
|
|
info!("✅ SSE-KMS multipart upload test passed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test multipart upload with SSE-C encryption
|
|
async fn test_multipart_upload_with_sse_c(
|
|
s3_client: &aws_sdk_s3::Client,
|
|
bucket: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let object_key = "multipart-sse-c-test";
|
|
let part_size = 5 * 1024 * 1024; // 5MB per part (minimum S3 multipart size)
|
|
let total_parts = 2;
|
|
let total_size = part_size * total_parts;
|
|
|
|
// SSE-C encryption key
|
|
let encryption_key = "01234567890123456789012345678901";
|
|
let key_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, encryption_key);
|
|
let key_md5 = sse_customer_key_md5_base64(encryption_key);
|
|
|
|
// Generate test data
|
|
let test_data: Vec<u8> = (0..total_size).map(|i| ((i * 3) % 256) as u8).collect();
|
|
|
|
// Step 1: Initiate multipart upload with SSE-C
|
|
let create_multipart_output = s3_client
|
|
.create_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key_b64)
|
|
.sse_customer_key_md5(&key_md5)
|
|
.send()
|
|
.await?;
|
|
|
|
let upload_id = create_multipart_output.upload_id().unwrap();
|
|
|
|
// Step 2: Upload parts with same SSE-C key
|
|
let mut completed_parts = Vec::new();
|
|
for part_number in 1..=total_parts {
|
|
let start = (part_number - 1) * part_size;
|
|
let end = std::cmp::min(start + part_size, total_size);
|
|
let part_data = &test_data[start..end];
|
|
|
|
let upload_part_output = s3_client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.part_number(part_number as i32)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(part_data.to_vec()))
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key_b64)
|
|
.sse_customer_key_md5(&key_md5)
|
|
.send()
|
|
.await?;
|
|
|
|
let etag = upload_part_output.e_tag().unwrap().to_string();
|
|
completed_parts.push(
|
|
aws_sdk_s3::types::CompletedPart::builder()
|
|
.part_number(part_number as i32)
|
|
.e_tag(&etag)
|
|
.build(),
|
|
);
|
|
}
|
|
|
|
// Step 3: Complete multipart upload
|
|
let completed_multipart_upload = aws_sdk_s3::types::CompletedMultipartUpload::builder()
|
|
.set_parts(Some(completed_parts))
|
|
.build();
|
|
|
|
let _complete_output = s3_client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key_b64)
|
|
.sse_customer_key_md5(&key_md5)
|
|
.multipart_upload(completed_multipart_upload)
|
|
.send()
|
|
.await?;
|
|
|
|
// Step 4: Download and verify with same SSE-C key
|
|
let get_response = s3_client
|
|
.get_object()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key_b64)
|
|
.sse_customer_key_md5(&key_md5)
|
|
.send()
|
|
.await?;
|
|
|
|
let downloaded_data = get_response.body.collect().await?.into_bytes();
|
|
assert_eq!(downloaded_data.len(), total_size);
|
|
assert_eq!(&downloaded_data[..], &test_data[..]);
|
|
|
|
info!("✅ SSE-C multipart upload test passed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test large multipart upload to verify streaming encryption works correctly
|
|
#[allow(
|
|
dead_code,
|
|
reason = "parked behind the TODO in test_local_kms_multipart_upload until streaming encryption is fixed for large files (backlog#1823)"
|
|
)]
|
|
async fn test_large_multipart_upload(
|
|
s3_client: &aws_sdk_s3::Client,
|
|
bucket: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let object_key = "large-multipart-test";
|
|
let part_size = 6 * 1024 * 1024; // 6MB per part (larger than 1MB block size)
|
|
let total_parts = 5; // Total: 30MB
|
|
let total_size = part_size * total_parts;
|
|
|
|
info!(
|
|
"Testing large multipart upload: {} parts of {}MB each = {}MB total",
|
|
total_parts,
|
|
part_size / (1024 * 1024),
|
|
total_size / (1024 * 1024)
|
|
);
|
|
|
|
// Generate test data with pattern for verification
|
|
let test_data: Vec<u8> = (0..total_size)
|
|
.map(|i| {
|
|
let part_num = i / part_size;
|
|
let offset_in_part = i % part_size;
|
|
((part_num * 100 + offset_in_part / 1000) % 256) as u8
|
|
})
|
|
.collect();
|
|
|
|
// Step 1: Initiate multipart upload with SSE-S3
|
|
let create_multipart_output = s3_client
|
|
.create_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.server_side_encryption(aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
.send()
|
|
.await?;
|
|
|
|
let upload_id = create_multipart_output.upload_id().unwrap();
|
|
|
|
// Step 2: Upload parts
|
|
let mut completed_parts = Vec::new();
|
|
for part_number in 1..=total_parts {
|
|
let start = (part_number - 1) * part_size;
|
|
let end = std::cmp::min(start + part_size, total_size);
|
|
let part_data = &test_data[start..end];
|
|
|
|
info!("Uploading part {} ({} bytes)", part_number, part_data.len());
|
|
|
|
let upload_part_output = s3_client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.part_number(part_number as i32)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(part_data.to_vec()))
|
|
.send()
|
|
.await?;
|
|
|
|
let etag = upload_part_output.e_tag().unwrap().to_string();
|
|
completed_parts.push(
|
|
aws_sdk_s3::types::CompletedPart::builder()
|
|
.part_number(part_number as i32)
|
|
.e_tag(&etag)
|
|
.build(),
|
|
);
|
|
|
|
info!("Part {} uploaded successfully", part_number);
|
|
}
|
|
|
|
// Step 3: Complete multipart upload
|
|
let completed_multipart_upload = aws_sdk_s3::types::CompletedMultipartUpload::builder()
|
|
.set_parts(Some(completed_parts))
|
|
.build();
|
|
|
|
let _complete_output = s3_client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(object_key)
|
|
.upload_id(upload_id)
|
|
.multipart_upload(completed_multipart_upload)
|
|
.send()
|
|
.await?;
|
|
|
|
info!("Large multipart upload completed");
|
|
|
|
// Step 4: Download and verify (this tests streaming decryption)
|
|
let get_response = s3_client.get_object().bucket(bucket).key(object_key).send().await?;
|
|
|
|
assert_eq!(
|
|
get_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
let downloaded_data = get_response.body.collect().await?.into_bytes();
|
|
assert_eq!(downloaded_data.len(), total_size);
|
|
|
|
// Verify data integrity
|
|
for (i, (&actual, &expected)) in downloaded_data.iter().zip(test_data.iter()).enumerate() {
|
|
if actual != expected {
|
|
panic!("Data mismatch at byte {i}: got {actual}, expected {expected}");
|
|
}
|
|
}
|
|
|
|
info!(
|
|
"✅ Large multipart upload test passed - streaming encryption/decryption works correctly for {}MB file",
|
|
total_size / (1024 * 1024)
|
|
);
|
|
Ok(())
|
|
}
|