Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add toggle to SigV4AuthScheme to turn off body signing #1822

Merged
merged 9 commits into from
Nov 18, 2024
20 changes: 20 additions & 0 deletions IntegrationTests/AWSIntegrationTestUtils/GenerateDataHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

public func generateRandomTextData(ofSizeInBytes byteCount: Int) -> Data {
let allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".utf8
let allowedBytes = Array(allowedCharacters)
let randomBytes = (0..<byteCount).map { _ in allowedBytes.randomElement()! }
return Data(randomBytes)
}

public func generateRandomTextData(ofSizeInMB megabytes: Double) -> Data {
let byteCount = Int(megabytes * 1024 * 1024) // Convert megabytes to bytes
return generateRandomTextData(ofSizeInBytes: byteCount)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,26 @@ final class S3ConcurrentTests: S3XCTestCase {
// Payload just below chunked threshold
// Tests concurrent upload of simple data payloads
func test_10x_1MB_getObject() async throws {
fileData = try generateDummyTextData(count: CHUNKED_THRESHOLD - 1)
fileData = generateRandomTextData(ofSizeInMB: 1)
try await repeatConcurrentlyWithArgs(count: 10, test: getObject, args: fileData!)
}

// Payload at chunked threshold, just large enough to chunk
// Tests concurrent upload with aws-chunked encoding & flexible checksums
func test_10x_1_5MB_getObject() async throws {
fileData = try generateDummyTextData(count: CHUNKED_THRESHOLD)
fileData = generateRandomTextData(ofSizeInMB: 1.5)
try await repeatConcurrentlyWithArgs(count: 10, test: getObject, args: fileData!)
}

// Payload 256 bytes with 200 concurrent requests, sends as simple data
// Tests very high concurrency with small data payloads
func test_200x_256B_getObject() async throws {
fileData = try generateDummyTextData(count: 256)
fileData = generateRandomTextData(ofSizeInBytes: 256)
try await repeatConcurrentlyWithArgs(count: 200, test: getObject, args: fileData!)
}

// MARK: - Private methods

// Generates text data of the exact length requested
private func generateDummyTextData(count: Int) throws -> Data {
let segment = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
let segmentData = Data(segment.utf8)
var wholeData = Data()
for _ in 0..<(count / segmentData.count + 1) {
wholeData.append(contentsOf: segmentData.shuffled())
}
// Truncate data to exactly the required length
return wholeData.subdata(in: 0..<count)
}

// Puts data to S3, gets the uploaded file, asserts retrieved data == original data, deletes S3 object
private func getObject(args: Any...) async throws {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import XCTest
import AWSS3
import SmithyHTTPAPI
@testable import ClientRuntime
import AWSIntegrationTestUtils
import class SmithyStreams.BufferedStream
import class SmithyChecksums.ValidatingBufferedStream

Expand All @@ -19,7 +20,7 @@ final class S3FlexibleChecksumsTests: S3XCTestCase {
override func setUp() {
super.setUp()
// Fill one MB with random data. Data is refreshed for each flexible checksums tests below.
originalData = Data((0..<(1024 * 1024)).map { _ in UInt8.random(in: UInt8.min...UInt8.max) })
originalData = generateRandomTextData(ofSizeInMB: 1)
}

// MARK: - Data uploads
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import AWSSDKHTTPAuth
import XCTest
import AWSS3
import ClientRuntime
import AWSClientRuntime
import SmithyHTTPAPI
import AWSIntegrationTestUtils
import class SmithyStreams.BufferedStream

/// Tests toggle unsigned payload using S3.
class S3ToggleUnsignedPayloadTests: S3XCTestCase {
private var s3Config: S3Client.S3ClientConfiguration!

override func setUp() async throws {
try await super.setUp()
s3Config = try await S3Client.S3ClientConfiguration(region: region)
s3Config.authSchemes = [SigV4AuthScheme(requestUnsignedBody: true)]
}

class CheckUnsignedPayloadHeader<InputType, OutputType>: Interceptor {
typealias RequestType = HTTPRequest
typealias ResponseType = HTTPResponse

func readBeforeTransmit(context: some AfterSerialization<InputType, RequestType>) async throws {
XCTAssertTrue(
context.getRequest().headers.value(for: "x-amz-content-sha256") == "UNSIGNED-PAYLOAD"
)
}
}

class CheckStreamingUnsignedPayloadHeader<InputType, OutputType>: Interceptor {
typealias RequestType = HTTPRequest
typealias ResponseType = HTTPResponse

func readBeforeTransmit(context: some AfterSerialization<InputType, RequestType>) async throws {
XCTAssertTrue(
context.getRequest().headers.value(for: "x-amz-content-sha256") == "STREAMING-UNSIGNED-PAYLOAD-TRAILER"
)
}
}

class CheckUnsignedPayloadHeaderProvider: HttpInterceptorProvider {
func create<InputType, OutputType>() -> any Interceptor<InputType, OutputType, HTTPRequest, HTTPResponse> {
return CheckUnsignedPayloadHeader()
}
}

class CheckStreamingUnsignedPayloadHeaderProvider: HttpInterceptorProvider {
func create<InputType, OutputType>() -> any Interceptor<InputType, OutputType, HTTPRequest, HTTPResponse> {
return CheckStreamingUnsignedPayloadHeader()
}
}

func testS3ToggleUnsignedPayloadNonStreaming() async throws {
let key = "test.txt"
let putObjectInput = PutObjectInput(
body: .noStream,
bucket: bucketName,
key: key,
metadata: ["filename": key]
)

// Upload
s3Config.addInterceptorProvider(CheckUnsignedPayloadHeaderProvider())
let s3Client = S3Client(config: s3Config)
_ = try await s3Client.putObject(input: putObjectInput)

// Get
let getObjectInput = GetObjectInput(bucket: bucketName, key: key)
let fetchedObject = try await client.getObject(input: getObjectInput)

XCTAssertNotNil(fetchedObject.metadata)
let metadata = try XCTUnwrap(fetchedObject.metadata)
XCTAssertEqual(metadata["filename"], key)
}

func testS3ToggleUnsignedPayloadStreaming() async throws {
let key = "test-streaming.txt"
let data = generateRandomTextData(ofSizeInMB: 1)
let bufferedStream = BufferedStream(data: data, isClosed: true)
let putObjectInput = PutObjectInput(
body: .stream(bufferedStream),
bucket: bucketName,
key: key,
metadata: ["filename": key]
)

// Upload
s3Config.addInterceptorProvider(CheckStreamingUnsignedPayloadHeaderProvider())
let s3Client = S3Client(config: s3Config)
_ = try await s3Client.putObject(input: putObjectInput)

// Get
let getObjectInput = GetObjectInput(bucket: bucketName, key: key)
let fetchedObject = try await client.getObject(input: getObjectInput)

XCTAssertNotNil(fetchedObject.metadata)
let metadata = try XCTUnwrap(fetchedObject.metadata)
XCTAssertEqual(metadata["filename"], key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ public class AWSSigV4Signer: SmithyHTTPAuthAPI.Signer {
let checksumIsPresent = signingProperties.get(key: SigningPropertyKeys.checksum) != nil
let isChunkedEligibleStream = signingProperties.get(key: SigningPropertyKeys.isChunkedEligibleStream) ?? false
let preComputedSha256 = signingProperties.get(key: AttributeKey<String>(name: "SignedBodyValue"))
let requestedUnsignedBody = signingProperties.get(key: SigningPropertyKeys.requestUnsignedBody)

let signedBodyValue: AWSSignedBodyValue = determineSignedBodyValue(
checksumIsPresent: checksumIsPresent,
isChunkedEligbleStream: isChunkedEligibleStream,
isUnsignedBody: unsignedBody,
isUnsignedBody: requestedUnsignedBody ?? unsignedBody,
preComputedSha256: preComputedSha256
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ import struct Smithy.Attributes
public struct SigV4AuthScheme: AuthScheme {
public let schemeID: String = "aws.auth#sigv4"
public let signer: Signer = AWSSigV4Signer()
public let requestUnsignedBody: Bool

public init() {}
public init() {
self.requestUnsignedBody = false
}

public init(requestUnsignedBody: Bool) {
self.requestUnsignedBody = requestUnsignedBody
}

public func customizeSigningProperties(signingProperties: Attributes, context: Context) throws -> Attributes {
var updatedSigningProperties = signingProperties
Expand Down Expand Up @@ -69,6 +76,11 @@ public struct SigV4AuthScheme: AuthScheme {
value: context.isChunkedEligibleStream
)

// Optionally toggle unsigned body
if self.requestUnsignedBody {
updatedSigningProperties.set(key: SigningPropertyKeys.requestUnsignedBody, value: true)
}

// Set service-specific signing properties if needed.
try CustomSigningPropertiesSetter().setServiceSpecificSigningProperties(
signingProperties: &updatedSigningProperties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,20 @@ class SigV4AuthSchemeTests: XCTestCase {
let updatedProperties = try sigV4AuthScheme.customizeSigningProperties(signingProperties: Attributes(), context: context)
XCTAssertTrue(try XCTUnwrap(updatedProperties.get(key: SigningPropertyKeys.shouldNormalizeURIPath)))
}

func testRequestUnsignedBody() throws {
let customSigV4AuthScheme = SigV4AuthScheme(requestUnsignedBody: true)
let context = contextBuilder
.withBidirectionalStreamingEnabled(value: true)
.withServiceName(value: "filler")
.withFlowType(value: .NORMAL)
.withOperation(value: "filler")
.withUnsignedPayloadTrait(value: false)
.build()
let updatedProperties = try customSigV4AuthScheme.customizeSigningProperties(signingProperties: Attributes(), context: context)
let unwrappedRequestUnsignedBodyValue = try XCTUnwrap(
updatedProperties.get(key: SigningPropertyKeys.requestUnsignedBody)
)
XCTAssertTrue(unwrappedRequestUnsignedBodyValue)
}
}
Loading