Skip to content

Commit

Permalink
feat: add toggle to SigV4AuthScheme to turn off body signing (#1822)
Browse files Browse the repository at this point in the history
  • Loading branch information
dayaffe authored Nov 18, 2024
1 parent 3dc79b2 commit f5e6c7f
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 18 deletions.
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)
}
}

0 comments on commit f5e6c7f

Please sign in to comment.