Skip to content

Commit 98b705e

Browse files
authored
service/s3: make streaming operations use unsigned payload by default (#1354)
Adds SwapPayloadSHA256ResolverMiddleware that swaps computPayloadSHA256 middleware if an S3 operation uses streaming payload. For S3 operations with streaming payload, we use unsigned payload strategy if TLS is enabled.
1 parent cf6f142 commit 98b705e

File tree

10 files changed

+180
-8
lines changed

10 files changed

+180
-8
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "41daeba5-1bff-488e-9510-08a60dfa2229",
3+
"type": "feature",
4+
"description": "Updates S3 streaming operations - PutObject, UploadPart, WriteGetObjectResponse to use unsigned payload signing auth when TLS is enabled.",
5+
"modules": [
6+
"service/s3"
7+
]
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "e5ca0363-5cf2-46a6-927b-2c99ec0f2fc2",
3+
"type": "feature",
4+
"description": "Adds dynamic signing middleware that switches to unsigned payload when TLS is enabled.",
5+
"modules": [
6+
"."
7+
]
8+
}

aws/signer/v4/middleware.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import (
66
"encoding/hex"
77
"fmt"
88
"io"
9+
"strings"
910

1011
"github.com/aws/aws-sdk-go-v2/aws"
1112
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
1213
v4Internal "github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4"
1314
"github.com/aws/aws-sdk-go-v2/internal/sdk"
1415
"github.com/aws/smithy-go/middleware"
15-
smithyHTTP "github.com/aws/smithy-go/transport/http"
16+
smithyhttp "github.com/aws/smithy-go/transport/http"
1617
)
1718

1819
const computePayloadHashMiddlewareID = "ComputePayloadHash"
@@ -46,6 +47,48 @@ func (e *SigningError) Unwrap() error {
4647
return e.Err
4748
}
4849

50+
// UseDynamicPayloadSigningMiddleware swaps the compute payload sha256 middleware with a resolver middleware that
51+
// switches between unsigned and signed payload based on TLS state for request.
52+
// This middleware should not be used for AWS APIs that do not support unsigned payload signing auth.
53+
// By default, SDK uses this middleware for known AWS APIs that support such TLS based auth selection .
54+
//
55+
// Usage example -
56+
// S3 PutObject API allows unsigned payload signing auth usage when TLS is enabled, and uses this middleware to
57+
// dynamically switch between unsigned and signed payload based on TLS state for request.
58+
func UseDynamicPayloadSigningMiddleware(stack *middleware.Stack) error {
59+
_, err := stack.Build.Swap(computePayloadHashMiddlewareID, &dynamicPayloadSigningMiddleware{})
60+
return err
61+
}
62+
63+
// dynamicPayloadSigningMiddleware dynamically resolves the middleware that computes and set payload sha256 middleware.
64+
type dynamicPayloadSigningMiddleware struct {
65+
}
66+
67+
// ID returns the resolver identifier
68+
func (m *dynamicPayloadSigningMiddleware) ID() string {
69+
return computePayloadHashMiddlewareID
70+
}
71+
72+
// HandleBuild sets a resolver that directs to the payload sha256 compute handler.
73+
func (m *dynamicPayloadSigningMiddleware) HandleBuild(
74+
ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler,
75+
) (
76+
out middleware.BuildOutput, metadata middleware.Metadata, err error,
77+
) {
78+
req, ok := in.Request.(*smithyhttp.Request)
79+
if !ok {
80+
return out, metadata, fmt.Errorf("unknown transport type %T", in.Request)
81+
}
82+
83+
// if TLS is enabled, use unsigned payload when supported
84+
if strings.EqualFold(req.URL.Scheme, "https") {
85+
return (&unsignedPayload{}).HandleBuild(ctx, in, next)
86+
}
87+
88+
// else fall back to signed payload
89+
return (&computePayloadSHA256{}).HandleBuild(ctx, in, next)
90+
}
91+
4992
// unsignedPayload sets the SigV4 request payload hash to unsigned.
5093
//
5194
// Will not set the Unsigned Payload magic SHA value, if a SHA has already been
@@ -120,7 +163,7 @@ func (m *computePayloadSHA256) HandleBuild(
120163
) (
121164
out middleware.BuildOutput, metadata middleware.Metadata, err error,
122165
) {
123-
req, ok := in.Request.(*smithyHTTP.Request)
166+
req, ok := in.Request.(*smithyhttp.Request)
124167
if !ok {
125168
return out, metadata, &HashComputationError{
126169
Err: fmt.Errorf("unexpected request middleware type %T", in.Request),
@@ -195,7 +238,7 @@ func (m *contentSHA256Header) HandleBuild(
195238
) (
196239
out middleware.BuildOutput, metadata middleware.Metadata, err error,
197240
) {
198-
req, ok := in.Request.(*smithyHTTP.Request)
241+
req, ok := in.Request.(*smithyhttp.Request)
199242
if !ok {
200243
return out, metadata, &HashComputationError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)}
201244
}
@@ -241,7 +284,7 @@ func (s *SignHTTPRequestMiddleware) HandleFinalize(ctx context.Context, in middl
241284
return next.HandleFinalize(ctx, in)
242285
}
243286

244-
req, ok := in.Request.(*smithyHTTP.Request)
287+
req, ok := in.Request.(*smithyhttp.Request)
245288
if !ok {
246289
return out, metadata, &SigningError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)}
247290
}

aws/signer/v4/middleware_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"net/url"
1011
"strconv"
1112
"strings"
1213
"testing"
@@ -306,6 +307,69 @@ func TestSwapComputePayloadSHA256ForUnsignedPayloadMiddleware(t *testing.T) {
306307
}
307308
}
308309

310+
func TestUseDynamicPayloadSigningMiddleware(t *testing.T) {
311+
cases := map[string]struct {
312+
content io.Reader
313+
url string
314+
expectedHash string
315+
expectedErr error
316+
}{
317+
"TLS disabled": {
318+
content: func() io.Reader {
319+
br := bytes.NewReader([]byte("some content"))
320+
return br
321+
}(),
322+
url: "http://localhost.com/",
323+
expectedHash: "290f493c44f5d63d06b374d0a5abd292fae38b92cab2fae5efefe1b0e9347f56",
324+
},
325+
"TLS enabled": {
326+
content: func() io.Reader {
327+
br := bytes.NewReader([]byte("some content"))
328+
return br
329+
}(),
330+
url: "https://localhost.com/",
331+
expectedHash: "UNSIGNED-PAYLOAD",
332+
},
333+
}
334+
335+
for name, tt := range cases {
336+
t.Run(name, func(t *testing.T) {
337+
c := &dynamicPayloadSigningMiddleware{}
338+
339+
next := middleware.BuildHandlerFunc(func(ctx context.Context, in middleware.BuildInput) (out middleware.BuildOutput, metadata middleware.Metadata, err error) {
340+
value := GetPayloadHash(ctx)
341+
if len(value) == 0 {
342+
t.Fatalf("expected payload hash value to be on context")
343+
}
344+
if e, a := tt.expectedHash, value; e != a {
345+
t.Errorf("expected %v, got %v", e, a)
346+
}
347+
348+
return out, metadata, err
349+
})
350+
351+
req := smithyhttp.NewStackRequest().(*smithyhttp.Request)
352+
req.URL, _ = url.Parse(tt.url)
353+
stream, err := req.SetStream(tt.content)
354+
if err != nil {
355+
t.Fatalf("expected no error, got %v", err)
356+
}
357+
358+
_, _, err = c.HandleBuild(context.Background(), middleware.BuildInput{Request: stream}, next)
359+
if err != nil && tt.expectedErr == nil {
360+
t.Errorf("expected no error, got %v", err)
361+
} else if err != nil && tt.expectedErr != nil {
362+
e, a := tt.expectedErr, err
363+
if !errors.As(a, &e) {
364+
t.Errorf("expected error type %T, got %T", e, a)
365+
}
366+
} else if err == nil && tt.expectedErr != nil {
367+
t.Errorf("expected error, got nil")
368+
}
369+
})
370+
}
371+
}
372+
309373
type nonSeeker struct{}
310374

311375
func (nonSeeker) Read(p []byte) (n int, err error) {

codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/S3ContentSHA256Header.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package software.amazon.smithy.aws.go.codegen.customization;
22

33
import java.util.List;
4+
import java.util.Optional;
45
import software.amazon.smithy.aws.go.codegen.AwsGoDependency;
56
import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait;
67
import software.amazon.smithy.go.codegen.SymbolUtils;
78
import software.amazon.smithy.go.codegen.integration.GoIntegration;
89
import software.amazon.smithy.go.codegen.integration.MiddlewareRegistrar;
910
import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin;
11+
import software.amazon.smithy.model.shapes.MemberShape;
12+
import software.amazon.smithy.model.shapes.Shape;
13+
import software.amazon.smithy.model.shapes.ShapeId;
14+
import software.amazon.smithy.model.shapes.StructureShape;
15+
import software.amazon.smithy.model.traits.StreamingTrait;
1016
import software.amazon.smithy.utils.ListUtils;
1117

1218

@@ -42,6 +48,37 @@ public List<RuntimeClientPlugin> getClientPlugins() {
4248
AwsGoDependency.AWS_SIGNER_V4
4349
).build())
4450
.build())
51+
.build(),
52+
RuntimeClientPlugin.builder()
53+
// If a S3 operation has a streaming payload but is not event stream payload,
54+
// client swaps signing middleware to use dynamic payload signing middleware.
55+
// This enables client to use unsigned payload when TLS is enabled, and switch
56+
// to signed payload for security when TLS is disabled.
57+
.operationPredicate(((model, service, operation) -> {
58+
if (!(S3ModelUtils.isServiceS3(model, service))) {
59+
return false;
60+
}
61+
62+
Optional<ShapeId> input = operation.getInput();
63+
if (!input.isPresent()) {
64+
return false;
65+
}
66+
67+
StructureShape inputShape = model.expectShape(input.get(), StructureShape.class);
68+
for (MemberShape memberShape : inputShape.getAllMembers().values()) {
69+
Shape targetShape = model.expectShape(memberShape.getTarget());
70+
if (targetShape.hasTrait(StreamingTrait.class)
71+
&& !StreamingTrait.isEventStream(model, memberShape)) {
72+
return true;
73+
}
74+
}
75+
return false;
76+
}))
77+
.registerMiddleware(MiddlewareRegistrar.builder()
78+
.resolvedFunction(SymbolUtils.createValueSymbolBuilder(
79+
"UseDynamicPayloadSigningMiddleware", AwsGoDependency.AWS_SIGNER_V4
80+
).build())
81+
.build())
4582
.build()
4683
);
4784
}

service/internal/integrationtest/s3/api_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package s3
55

66
import (
7-
"bytes"
87
"strings"
98
"testing"
109
)
@@ -14,7 +13,6 @@ func TestInteg_WriteToObject(t *testing.T) {
1413
"seekable body": {Body: strings.NewReader("hello world"), ExpectBody: []byte("hello world")},
1514
"empty string body": {Body: strings.NewReader(""), ExpectBody: []byte("")},
1615
"nil body": {Body: nil, ExpectBody: []byte("")},
17-
"unseekable body": {Body: bytes.NewBuffer([]byte("hello world")), ExpectError: "failed to compute payload hash"},
1816
}
1917

2018
for name, c := range cases {

service/s3/api_op_PutObject.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/s3/api_op_UploadPart.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/s3/api_op_WriteGetObjectResponse.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/s3/internal/customizations/write_get_object_response_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package customizations_test
33
import (
44
"bytes"
55
"context"
6+
"crypto/tls"
67
"fmt"
78
"github.com/aws/aws-sdk-go-v2/aws"
89
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -191,11 +192,15 @@ func TestWriteGetObjectResponse(t *testing.T) {
191192

192193
for name, tt := range cases {
193194
t.Run(name, func(t *testing.T) {
194-
server := httptest.NewServer(tt.Handler(t))
195+
server := httptest.NewTLSServer(tt.Handler(t))
195196
defer server.Close()
196-
197197
client := s3.New(s3.Options{
198198
Region: "us-west-2",
199+
HTTPClient: &http.Client{
200+
Transport: &http.Transport{
201+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
202+
},
203+
},
199204
EndpointResolver: s3.EndpointResolverFunc(func(region string, options s3.EndpointResolverOptions) (aws.Endpoint, error) {
200205
return aws.Endpoint{
201206
URL: server.URL,

0 commit comments

Comments
 (0)