Skip to content

Commit 8dbf48c

Browse files
author
Zelda Hessler
authored
Add operation customization for disabling payload signing (#3915)
## Motivation and Context <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here --> #3583 ## Description <!--- Describe your changes in detail --> This PR adds the ability for users to disable payload signing with an operation customization. ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> This PR includes tests. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 208ca79 commit 8dbf48c

File tree

11 files changed

+279
-16
lines changed

11 files changed

+279
-16
lines changed

.changelog/1732034799.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
applies_to: ["client"]
3+
authors: ["Velfi"]
4+
references: ["smithy-rs#3583"]
5+
breaking: false
6+
new_feature: true
7+
bug_fix: false
8+
---
9+
10+
It is now possible to disable payload signing through an operation customization.
11+
12+
```rust
13+
async fn put_example_object(client: &aws_sdk_s3::Client) {
14+
let res = client
15+
.put_object()
16+
.bucket("test-bucket")
17+
.key("test-key")
18+
.body(ByteStream::from_static(b"Hello, world!"))
19+
.customize()
20+
// Setting this will disable payload signing.
21+
.disable_payload_signing()
22+
.send()
23+
.await;
24+
}
25+
```
26+
27+
Disabling payload signing will result in a small speedup at the cost of removing a data integrity check.
28+
However, this is an advanced feature and **may not be supported by all services/operations**.

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
//! Interceptor for handling Smithy `@httpChecksum` request checksumming with AWS SigV4
99
10+
use aws_runtime::auth::PayloadSigningOverride;
11+
use aws_runtime::content_encoding::header_value::AWS_CHUNKED;
1012
use aws_runtime::content_encoding::{AwsChunkedBody, AwsChunkedBodyOptions};
11-
use aws_runtime::{auth::SigV4OperationSigningConfig, content_encoding::header_value::AWS_CHUNKED};
12-
use aws_sigv4::http_request::SignableBody;
1313
use aws_smithy_checksums::ChecksumAlgorithm;
1414
use aws_smithy_checksums::{body::calculate, http::HttpChecksum};
1515
use aws_smithy_runtime_api::box_error::BoxError;
@@ -190,11 +190,8 @@ fn add_checksum_for_request_body(
190190
// Body is streaming: wrap the body so it will emit a checksum as a trailer.
191191
None => {
192192
tracing::debug!("applying {checksum_algorithm:?} of the request body as a trailer");
193-
if let Some(mut signing_config) = cfg.load::<SigV4OperationSigningConfig>().cloned() {
194-
signing_config.signing_options.payload_override =
195-
Some(SignableBody::StreamingUnsignedPayloadTrailer);
196-
cfg.interceptor_state().store_put(signing_config);
197-
}
193+
cfg.interceptor_state()
194+
.store_put(PayloadSigningOverride::StreamingUnsignedPayloadTrailer);
198195
wrap_streaming_request_body_in_checksum_calculating_body(request, checksum_algorithm)?;
199196
}
200197
}

aws/rust-runtime/aws-runtime/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-runtime"
3-
version = "1.4.4"
3+
version = "1.5.0"
44
authors = ["AWS Rust SDK Team <[email protected]>"]
55
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
66
edition = "2021"

aws/rust-runtime/aws-runtime/src/auth.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use aws_smithy_runtime_api::box_error::BoxError;
1111
use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig;
1212
use aws_smithy_runtime_api::client::identity::Identity;
1313
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
14-
use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
14+
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
15+
use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer, Storable, StoreReplace};
1516
use aws_smithy_types::Document;
1617
use aws_types::region::{Region, SigningRegion, SigningRegionSet};
1718
use aws_types::SigningName;
@@ -265,3 +266,70 @@ fn apply_signing_instructions(
265266
}
266267
Ok(())
267268
}
269+
270+
/// When present in the config bag, this type will signal that the default
271+
/// payload signing should be overridden.
272+
#[non_exhaustive]
273+
#[derive(Clone, Debug)]
274+
pub enum PayloadSigningOverride {
275+
/// An unsigned payload
276+
///
277+
/// UnsignedPayload is used for streaming requests where the contents of the body cannot be
278+
/// known prior to signing
279+
UnsignedPayload,
280+
281+
/// A precomputed body checksum. The checksum should be a SHA256 checksum of the body,
282+
/// lowercase hex encoded. Eg:
283+
/// `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
284+
Precomputed(String),
285+
286+
/// Set when a streaming body has checksum trailers.
287+
StreamingUnsignedPayloadTrailer,
288+
}
289+
290+
impl PayloadSigningOverride {
291+
/// Create a payload signing override that will prevent the payload from
292+
/// being signed.
293+
pub fn unsigned_payload() -> Self {
294+
Self::UnsignedPayload
295+
}
296+
297+
/// Convert this type into the type used by the signer to determine how a
298+
/// request body should be signed.
299+
pub fn to_signable_body(self) -> SignableBody<'static> {
300+
match self {
301+
Self::UnsignedPayload => SignableBody::UnsignedPayload,
302+
Self::Precomputed(checksum) => SignableBody::Precomputed(checksum),
303+
Self::StreamingUnsignedPayloadTrailer => SignableBody::StreamingUnsignedPayloadTrailer,
304+
}
305+
}
306+
}
307+
308+
impl Storable for PayloadSigningOverride {
309+
type Storer = StoreReplace<Self>;
310+
}
311+
312+
/// A runtime plugin that, when set, will override how the signer signs request payloads.
313+
#[derive(Debug)]
314+
pub struct PayloadSigningOverrideRuntimePlugin {
315+
inner: FrozenLayer,
316+
}
317+
318+
impl PayloadSigningOverrideRuntimePlugin {
319+
/// Create a new runtime plugin that will force the signer to skip signing
320+
/// the request payload when signing an HTTP request.
321+
pub fn unsigned() -> Self {
322+
let mut layer = Layer::new("PayloadSigningOverrideRuntimePlugin");
323+
layer.store_put(PayloadSigningOverride::UnsignedPayload);
324+
325+
Self {
326+
inner: layer.freeze(),
327+
}
328+
}
329+
}
330+
331+
impl RuntimePlugin for PayloadSigningOverrideRuntimePlugin {
332+
fn config(&self) -> Option<FrozenLayer> {
333+
Some(self.inner.clone())
334+
}
335+
}

aws/rust-runtime/aws-runtime/src/auth/sigv4.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use crate::auth;
77
use crate::auth::{
88
extract_endpoint_auth_scheme_signing_name, extract_endpoint_auth_scheme_signing_region,
9-
SigV4OperationSigningConfig, SigV4SessionTokenNameOverride, SigV4SigningError,
9+
PayloadSigningOverride, SigV4OperationSigningConfig, SigV4SessionTokenNameOverride,
10+
SigV4SigningError,
1011
};
1112
use aws_credential_types::Credentials;
1213
use aws_sigv4::http_request::{
@@ -177,7 +178,7 @@ impl Sign for SigV4Signer {
177178
let (signing_instructions, _signature) = {
178179
// A body that is already in memory can be signed directly. A body that is not in memory
179180
// (any sort of streaming body or presigned request) will be signed via UNSIGNED-PAYLOAD.
180-
let signable_body = operation_config
181+
let mut signable_body = operation_config
181182
.signing_options
182183
.payload_override
183184
.as_ref()
@@ -192,6 +193,15 @@ impl Sign for SigV4Signer {
192193
.unwrap_or(SignableBody::UnsignedPayload)
193194
});
194195

196+
// Sometimes it's necessary to override the payload signing scheme.
197+
// If an override exists then fetch and apply it.
198+
if let Some(payload_signing_override) = config_bag.load::<PayloadSigningOverride>() {
199+
tracing::trace!(
200+
"payload signing was overridden, now set to {payload_signing_override:?}"
201+
);
202+
signable_body = payload_signing_override.clone().to_signable_body();
203+
}
204+
195205
let signable_request = SignableRequest::new(
196206
request.method(),
197207
request.uri(),

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ val DECORATORS: List<ClientCodegenDecorator> =
6666
TokenProvidersDecorator(),
6767
ServiceEnvConfigDecorator(),
6868
HttpRequestCompressionDecorator(),
69+
DisablePayloadSigningDecorator(),
6970
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3863): Comment in once the issue has been resolved
7071
// SmokeTestsDecorator(),
7172
),
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rustsdk
7+
8+
import software.amazon.smithy.model.shapes.OperationShape
9+
import software.amazon.smithy.model.shapes.ShapeId
10+
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
11+
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
12+
import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationCustomization
13+
import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationSection
14+
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
15+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
16+
import software.amazon.smithy.rust.codegen.core.rustlang.writable
17+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
18+
19+
internal val DISABLE_PAYLOAD_SIGNING_OPERATIONS by lazy {
20+
listOf(
21+
// S3
22+
ShapeId.from("com.amazonaws.s3#PutObject"),
23+
ShapeId.from("com.amazonaws.s3#UploadPart"),
24+
)
25+
}
26+
27+
class DisablePayloadSigningDecorator : ClientCodegenDecorator {
28+
override val name: String = "DisablePayloadSigning"
29+
override val order: Byte = 0
30+
31+
override fun operationCustomizations(
32+
codegenContext: ClientCodegenContext,
33+
operation: OperationShape,
34+
baseCustomizations: List<OperationCustomization>,
35+
): List<OperationCustomization> {
36+
return baseCustomizations +
37+
object : OperationCustomization() {
38+
private val runtimeConfig = codegenContext.runtimeConfig
39+
40+
override fun section(section: OperationSection): Writable {
41+
return writable {
42+
when (section) {
43+
is OperationSection.CustomizableOperationImpl -> {
44+
if (DISABLE_PAYLOAD_SIGNING_OPERATIONS.contains(operation.id)) {
45+
rustTemplate(
46+
"""
47+
/// Disable payload signing for this request.
48+
///
49+
/// **WARNING:** This is an advanced feature that removes
50+
/// the cost of signing a request payload by removing a data
51+
/// integrity check. Not all services/operations support
52+
/// this feature.
53+
pub fn disable_payload_signing(self) -> Self {
54+
self.runtime_plugin(#{PayloadSigningOverrideRuntimePlugin}::unsigned())
55+
}
56+
""",
57+
*preludeScope,
58+
"PayloadSigningOverrideRuntimePlugin" to
59+
AwsRuntimeType.awsRuntime(runtimeConfig)
60+
.resolve("auth::PayloadSigningOverrideRuntimePlugin"),
61+
)
62+
}
63+
}
64+
65+
else -> {}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}

aws/sdk/integration-tests/s3/tests/signing-it.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
use aws_credential_types::provider::SharedCredentialsProvider;
99
use aws_sdk_s3::config::{Credentials, Region};
10+
use aws_sdk_s3::primitives::ByteStream;
1011
use aws_sdk_s3::{Client, Config};
12+
use aws_smithy_runtime::client::http::test_util::capture_request;
1113
use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient};
1214
use aws_smithy_types::body::SdkBody;
1315
use http::header::AUTHORIZATION;
@@ -40,3 +42,80 @@ async fn test_signer() {
4042

4143
http_client.assert_requests_match(&[AUTHORIZATION.as_str()]);
4244
}
45+
46+
#[tokio::test]
47+
async fn disable_payload_signing_works() {
48+
let (http_client, request) = capture_request(None);
49+
let conf = aws_sdk_s3::Config::builder()
50+
.with_test_defaults()
51+
.behavior_version_latest()
52+
.region(Region::new("us-east-1"))
53+
.http_client(http_client)
54+
.build();
55+
let client = aws_sdk_s3::Client::from_conf(conf);
56+
let _ = client
57+
.put_object()
58+
.bucket("XXXXXXXXXXX")
59+
.key("test-key")
60+
.body(ByteStream::from_static(b"Hello, world!"))
61+
.customize()
62+
.disable_payload_signing()
63+
.send()
64+
.await;
65+
66+
let request = request.expect_request();
67+
let x_amz_content_sha256 = request
68+
.headers()
69+
.get("x-amz-content-sha256")
70+
.expect("x-amz-content-sha256 is set")
71+
.to_owned();
72+
assert_eq!("UNSIGNED-PAYLOAD", x_amz_content_sha256);
73+
}
74+
75+
// This test ensures that the request checksum interceptor payload signing
76+
// override takes priority over the runtime plugin's override. If it didn't,
77+
// then disabling payload signing would cause requests to incorrectly omit
78+
// trailers.
79+
#[tokio::test]
80+
async fn disable_payload_signing_works_with_checksums() {
81+
let (http_client, request) = capture_request(None);
82+
let conf = aws_sdk_s3::Config::builder()
83+
.with_test_defaults()
84+
.behavior_version_latest()
85+
.region(Region::new("us-east-1"))
86+
.http_client(http_client)
87+
.build();
88+
let client = aws_sdk_s3::Client::from_conf(conf);
89+
90+
// ByteStreams created from a file are streaming and have a known size
91+
let mut file = tempfile::NamedTempFile::new().unwrap();
92+
use std::io::Write;
93+
file.write_all(b"Hello, world!").unwrap();
94+
95+
let body = aws_sdk_s3::primitives::ByteStream::read_from()
96+
.path(file.path())
97+
.buffer_size(1024)
98+
.build()
99+
.await
100+
.unwrap();
101+
102+
let _ = client
103+
.put_object()
104+
.bucket("XXXXXXXXXXX")
105+
.key("test-key")
106+
.body(body)
107+
.checksum_algorithm(aws_sdk_s3::types::ChecksumAlgorithm::Crc32)
108+
.customize()
109+
.disable_payload_signing()
110+
.send()
111+
.await;
112+
113+
let request = request.expect_request();
114+
let x_amz_content_sha256 = request
115+
.headers()
116+
.get("x-amz-content-sha256")
117+
.expect("x-amz-content-sha256 is set")
118+
.to_owned();
119+
// The checksum interceptor sets this.
120+
assert_eq!("STREAMING-UNSIGNED-PAYLOAD-TRAILER", x_amz_content_sha256);
121+
}

aws/sdk/integration-tests/sts/tests/signing-it.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ async fn assume_role_signed() {
1111
let creds = Credentials::for_tests();
1212
let (http_client, request) = capture_request(None);
1313
let conf = aws_sdk_sts::Config::builder()
14+
.behavior_version_latest()
1415
.credentials_provider(creds)
1516
.region(Region::new("us-east-1"))
1617
.http_client(http_client)
@@ -28,6 +29,7 @@ async fn assume_role_signed() {
2829
async fn web_identity_unsigned() {
2930
let (http_client, request) = capture_request(None);
3031
let conf = aws_sdk_sts::Config::builder()
32+
.behavior_version_latest()
3133
.region(Region::new("us-east-1"))
3234
.http_client(http_client)
3335
.build();
@@ -44,6 +46,7 @@ async fn web_identity_unsigned() {
4446
async fn assume_role_saml_unsigned() {
4547
let (http_client, request) = capture_request(None);
4648
let conf = aws_sdk_sts::Config::builder()
49+
.behavior_version_latest()
4750
.region(Region::new("us-east-1"))
4851
.http_client(http_client)
4952
.build();
@@ -60,6 +63,7 @@ async fn assume_role_saml_unsigned() {
6063
async fn web_identity_no_creds() {
6164
let (http_client, request) = capture_request(None);
6265
let conf = aws_sdk_sts::Config::builder()
66+
.behavior_version_latest()
6367
.region(Region::new("us-east-1"))
6468
.http_client(http_client)
6569
.build();

0 commit comments

Comments
 (0)