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: customizations #1221

Merged
merged 11 commits into from
Jan 9, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import ClientRuntime

public struct SigV4AAuthScheme: ClientRuntime.AuthScheme {
public let schemeID: String = "aws.auth#sigv4a"
public let signer: ClientRuntime.Signer = AWSSigV4Signer()
public let idKind: ClientRuntime.IdentityKind = .aws

public init() {}

public func customizeSigningProperties(signingProperties: Attributes, context: HttpContext) -> Attributes {
var updatedSigningProperties = signingProperties

// Set signing algorithm flag
updatedSigningProperties.set(key: AttributeKeys.signingAlgorithm, value: .sigv4a)

// Set bidirectional streaming flag
updatedSigningProperties.set(
key: AttributeKeys.bidirectionalStreaming,
value: context.isBidirectionalStreamingEnabled()
)

// Set signing name and signing region flags
updatedSigningProperties.set(key: AttributeKeys.signingName, value: context.getSigningName())
updatedSigningProperties.set(key: AttributeKeys.signingRegion, value: context.getSigningRegion())

// Set expiration flag
//
// Expiration is only used for presigning (presign request flow or presign URL flow).
updatedSigningProperties.set(key: AttributeKeys.expiration, value: context.getExpiration())

// Set signature type flag
//
// AWSSignatureType.requestQueryParams is only used for presign URL flow.
// Out of the AWSSignatureType enum cases, only two are used. .requestHeaders and .requestQueryParams.
// .requestHeaders is the deafult signing used for AWS operations.
let serviceName = context.getServiceName()
let isPresignURLFlow = context.getFlowType() == .PRESIGN_URL
updatedSigningProperties.set(
key: AttributeKeys.signatureType,
value: isPresignURLFlow ? .requestQueryParams : .requestHeaders
)

// Set unsignedBody flag
// Operation name is guaranteed to be in middleware context from generic codegen.
let operationName = context.getOperation()!
let shouldForceUnsignedBody = SigV4Util.shouldForceUnsignedBody(
flow: context.getFlowType(),
serviceName: serviceName,
opName: operationName
)
let unsignedBody = context.hasUnsignedPayloadTrait() || shouldForceUnsignedBody
updatedSigningProperties.set(key: AttributeKeys.unsignedBody, value: unsignedBody)

// Set signedBodyHeader flag
let useSignedBodyHeader = SigV4Util.serviceUsesUnsignedBodyHeader(serviceName: serviceName) && !unsignedBody
updatedSigningProperties.set(
key: AttributeKeys.signedBodyHeader,
value: useSignedBodyHeader ? .contentSha256 : AWSSignedBodyHeader.none
)

let serviceIsS3 = serviceName == "S3"
// Set flags in SigningFlags object
// Set useDoubleURIEncode to false IFF service is S3
updatedSigningProperties.set(key: AttributeKeys.useDoubleURIEncode, value: !serviceIsS3)
// Set shouldNormalizeURIPath to false IFF service is S3
updatedSigningProperties.set(key: AttributeKeys.shouldNormalizeURIPath, value: !serviceIsS3)
updatedSigningProperties.set(key: AttributeKeys.omitSessionToken, value: false)
dayaffe marked this conversation as resolved.
Show resolved Hide resolved

return updatedSigningProperties
}
}
Original file line number Diff line number Diff line change
@@ -16,34 +16,63 @@ public struct SigV4AuthScheme: ClientRuntime.AuthScheme {

public func customizeSigningProperties(signingProperties: Attributes, context: HttpContext) -> Attributes {
var updatedSigningProperties = signingProperties
updatedSigningProperties.set(key: AttributeKeys.bidirectionalStreaming, value: context.isBidirectionalStreamingEnabled())

// Set signing algorithm flag
updatedSigningProperties.set(key: AttributeKeys.signingAlgorithm, value: .sigv4)

// Set bidirectional streaming flag
updatedSigningProperties.set(
key: AttributeKeys.bidirectionalStreaming,
value: context.isBidirectionalStreamingEnabled()
)

// Set signing name and signing region flags
updatedSigningProperties.set(key: AttributeKeys.signingName, value: context.getSigningName())
updatedSigningProperties.set(key: AttributeKeys.signingRegion, value: context.getSigningRegion())
updatedSigningProperties.set(key: AttributeKeys.signingAlgorithm, value: .sigv4)

// Expiration is only used for presigning URLs. E.g., in AWSS3, and in AWSPolly.
updatedSigningProperties.set(key: AttributeKeys.expiration, value: 0)
// AWSSignatureType.requestQueryParams is only used for S3 GetObject and PutObject
// Out of all AWSSignatureType cases, only two are used. .requestHeaders and .requestQueryParams.
// .requestHeaders is the deafult signing used for all AWS operations except S3 customizations.
updatedSigningProperties.set(key: AttributeKeys.signatureType, value: .requestHeaders)
// Set expiration flag
//
// Expiration is only used for presigning (presign request flow or presign URL flow).
updatedSigningProperties.set(key: AttributeKeys.expiration, value: context.getExpiration())

// SigningFlags
// Set signature type flag
//
// AWSSignatureType.requestQueryParams is only used for presign URL flow.
// Out of the AWSSignatureType enum cases, only two are used. .requestHeaders and .requestQueryParams.
// .requestHeaders is the deafult signing used for AWS operations.
let serviceName = context.getServiceName()
let isPresignURLFlow = context.getFlowType() == .PRESIGN_URL
updatedSigningProperties.set(
key: AttributeKeys.signatureType,
value: isPresignURLFlow ? .requestQueryParams : .requestHeaders
)

// Set unsignedBody flag
// Operation name is guaranteed to be in middleware context from generic codegen.
let operationName = context.getOperation()!
dayaffe marked this conversation as resolved.
Show resolved Hide resolved
let shouldForceUnsignedBody = SigV4Util.shouldForceUnsignedBody(
flow: context.getFlowType(),
serviceName: serviceName,
opName: operationName
)
let unsignedBody = context.hasUnsignedPayloadTrait() || shouldForceUnsignedBody
updatedSigningProperties.set(key: AttributeKeys.unsignedBody, value: unsignedBody)

// Set signedBodyHeader flag
let useSignedBodyHeader = SigV4Util.serviceUsesUnsignedBodyHeader(serviceName: serviceName) && !unsignedBody
updatedSigningProperties.set(
key: AttributeKeys.signedBodyHeader,
value: useSignedBodyHeader ? .contentSha256 : AWSSignedBodyHeader.none
)

let serviceIsS3 = serviceName == "S3"
// Set flags in SigningFlags object
// Set useDoubleURIEncode to false IFF service is S3
updatedSigningProperties.set(key: AttributeKeys.useDoubleURIEncode, value: serviceName != "S3")
updatedSigningProperties.set(key: AttributeKeys.useDoubleURIEncode, value: !serviceIsS3)
// Set shouldNormalizeURIPath to false IFF service is S3
updatedSigningProperties.set(key: AttributeKeys.shouldNormalizeURIPath, value: serviceName != "S3")
// FIXME: Flag doesn't seem to be used by anything - investigate
updatedSigningProperties.set(key: AttributeKeys.shouldNormalizeURIPath, value: !serviceIsS3)
updatedSigningProperties.set(key: AttributeKeys.omitSessionToken, value: false)
dayaffe marked this conversation as resolved.
Show resolved Hide resolved

/*
* The boolean flag .unsignedBody for AWSSigningConfig.signedBodyValue &
* the AWSSignedBodyHeader enum value for AWSSigningConfig.signedBodyHeader
* will be generated into signingProperties during service specific auth scheme resolver codegen and be part of
* the returned auth option's signing properties.
* By the time the call chain arrives here, code-generated flags are already included in signingProperties.
*/
return updatedSigningProperties
}
}
26 changes: 26 additions & 0 deletions Sources/Core/AWSClientRuntime/Auth/AuthSchemes/SigV4Util.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import ClientRuntime

public class SigV4Util {
static let unsignedBodyHeader = ["S3", "Glacier"]
static let forceUnsignedBodyForPresigningURL = [
"S3": ["getObject", "putObject"]
]

static func shouldForceUnsignedBody(flow: FlowType, serviceName: String, opName: String) -> Bool {
let serviceQualifies = forceUnsignedBodyForPresigningURL.keys.contains(serviceName)
let flowQualies = flow == .PRESIGN_URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as pointed out.

return serviceQualifies && flowQualies && forceUnsignedBodyForPresigningURL[serviceName]!.contains(opName)
}

static func serviceUsesUnsignedBodyHeader(serviceName: String) -> Bool {
return unsignedBodyHeader.contains(serviceName)
}
}
32 changes: 24 additions & 8 deletions Sources/Core/AWSClientRuntime/EventStream/AWSMessageSigner.swift
Original file line number Diff line number Diff line change
@@ -11,8 +11,12 @@ extension AWSEventStream {
/// Signs a `Message` using the AWS SigV4 signing algorithm
public class AWSMessageSigner: MessageSigner {
let encoder: MessageEncoder
let signer: () async throws -> ClientRuntime.Signer
let signingConfig: () async throws -> AWSSigningConfig
let requestSignature: () -> String
// Attribute key used to save AWSSigningConfig into signingProperties argument
// for AWSSigV4Signer::signEvent call that conforms to Signer::signEvent.
static let signingConfigKey = AttributeKey<AWSSigningConfig>(name: "EventStreamSigningConfig")

private var _previousSignature: String?

@@ -35,9 +39,11 @@ extension AWSEventStream {
}

public init(encoder: MessageEncoder,
signer: @escaping () async throws -> ClientRuntime.Signer,
signingConfig: @escaping () async throws -> AWSSigningConfig,
requestSignature: @escaping () -> String) {
self.encoder = encoder
self.signer = signer
self.signingConfig = signingConfig
self.requestSignature = requestSignature
}
@@ -49,11 +55,15 @@ extension AWSEventStream {
// encode to bytes
let encodedMessage = try encoder.encode(message: message)
let signingConfig = try await self.signingConfig()

// sign encoded bytes
let signingResult = try await AWSSigV4Signer.signEvent(payload: encodedMessage,
previousSignature: previousSignature,
signingConfig: signingConfig)
// Fetch signer
let signer = try await self.signer()
// Wrap config into signingProperties: Attributes
var configWrapper = Attributes()
configWrapper.set(key: AWSMessageSigner.signingConfigKey, value: signingConfig)
// Sign encoded bytes
let signingResult = try await signer.signEvent(payload: encodedMessage,
previousSignature: previousSignature,
signingProperties: configWrapper)
previousSignature = signingResult.signature
return signingResult.output
}
@@ -62,9 +72,15 @@ extension AWSEventStream {
/// - Returns: Signed `Message` with `:chunk-signature` & `:date` headers
public func signEmpty() async throws -> ClientRuntime.EventStream.Message {
let signingConfig = try await self.signingConfig()
let signingResult = try await AWSSigV4Signer.signEvent(payload: .init(),
previousSignature: previousSignature,
signingConfig: signingConfig)
// Fetch signer
let signer = try await self.signer()
// Wrap config into signingProperties: Attributes
var configWrapper = Attributes()
configWrapper.set(key: AWSMessageSigner.signingConfigKey, value: signingConfig)
// Sign empty payload
let signingResult = try await signer.signEvent(payload: .init(),
previousSignature: previousSignature,
signingProperties: configWrapper)
return signingResult.output
}
}
36 changes: 18 additions & 18 deletions Sources/Core/AWSClientRuntime/HttpContextBuilder+Extension.swift
Original file line number Diff line number Diff line change
@@ -14,10 +14,6 @@ extension HttpContext {
return attributes.get(key: AttributeKeys.credentialsProvider)
}

public func getRequestSignature() -> String {
return attributes.get(key: AttributeKeys.requestSignature)!
}

public func getSigningAlgorithm() -> AWSSigningAlgorithm? {
return attributes.get(key: AttributeKeys.signingAlgorithm)
}
@@ -55,17 +51,30 @@ extension HttpContext {
public func setupBidirectionalStreaming() throws {
// setup client to server
let messageEncoder = AWSClientRuntime.AWSEventStream.AWSMessageEncoder()
let messageSigner = AWSClientRuntime.AWSEventStream.AWSMessageSigner(encoder: messageEncoder) {
try await self.makeEventStreamSigningConfig()
} requestSignature: {
self.getRequestSignature()
}
let messageSigner = AWSClientRuntime.AWSEventStream.AWSMessageSigner(
encoder: messageEncoder,
signer: { try self.fetchSigner() },
signingConfig: { try await self.makeEventStreamSigningConfig() },
requestSignature: { self.getRequestSignature() }
)
attributes.set(key: AttributeKeys.messageEncoder, value: messageEncoder)
attributes.set(key: AttributeKeys.messageSigner, value: messageSigner)

// enable the flag
attributes.set(key: AttributeKeys.bidirectionalStreaming, value: true)
}

func fetchSigner() throws -> ClientRuntime.Signer {
guard let authScheme = self.getSelectedAuthScheme() else {
throw ClientError.authError(
"Signer for event stream could not be loaded because auth scheme was not configured."
)
}
guard let signer = authScheme.signer else {
throw ClientError.authError("Signer was not configured for the selected auth scheme.")
}
return signer
}
}

extension HttpContextBuilder {
@@ -75,14 +84,6 @@ extension HttpContextBuilder {
return self
}

/// Sets the request signature for the event stream operation
/// - Parameter value: `String` request signature
@discardableResult
public func withRequestSignature(value: String) -> HttpContextBuilder {
self.attributes.set(key: AttributeKeys.requestSignature, value: value)
return self
}

@discardableResult
public func withSigningAlgorithm(value: AWSSigningAlgorithm) -> HttpContextBuilder {
self.attributes.set(key: AttributeKeys.signingAlgorithm, value: value)
@@ -93,7 +94,6 @@ extension HttpContextBuilder {
extension AttributeKeys {
public static let credentialsProvider = AttributeKey<(any CredentialsProviding)>(name: "CredentialsProvider")
public static let signingAlgorithm = AttributeKey<AWSSigningAlgorithm>(name: "SigningAlgorithm")
public static let requestSignature = AttributeKey<String>(name: "AWS_HTTP_SIGNATURE")

// Keys used to store/retrieve AWSSigningConfig fields in/from signingProperties passed to AWSSigV4Signer
public static let unsignedBody = AttributeKey<Bool>(name: "UnsignedBody")
21 changes: 14 additions & 7 deletions Sources/Core/AWSClientRuntime/Signing/AWSSigV4Signer.swift
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import ClientRuntime
import Foundation

public class AWSSigV4Signer: ClientRuntime.Signer {
public func sign<IdentityT: Identity>(
public func signRequest<IdentityT: Identity>(
requestBuilder: SdkHttpRequestBuilder,
identity: IdentityT,
signingProperties: ClientRuntime.Attributes
@@ -94,6 +94,18 @@ public class AWSSigV4Signer: ClientRuntime.Signer {
)
}

public func signEvent(
payload: Data,
previousSignature: String,
signingProperties: Attributes
) async throws -> SigningResult<EventStream.Message> {
let signingConfig = signingProperties.get(key: AWSEventStream.AWSMessageSigner.signingConfigKey)
guard let signingConfig else {
throw ClientError.dataNotFound("Failed to sign event stream message due to missing signing config.")
}
return try await signEvent(payload: payload, previousSignature: previousSignature, signingConfig: signingConfig)
}

static let logger: SwiftLogger = SwiftLogger(label: "AWSSigV4Signer")

public static func sigV4SignedURL(
@@ -143,7 +155,7 @@ public class AWSSigV4Signer: ClientRuntime.Signer {
/// the current event payload like a rolling signature calculation.
/// - signingConfig: The signing configuration
/// - Returns: The signed event with :date and :chunk-signature headers
static func signEvent(payload: Data,
public func signEvent(payload: Data,
previousSignature: String,
signingConfig: AWSSigningConfig) async throws -> SigningResult<EventStream.Message> {
let signature = try await Signer.signEvent(event: payload,
@@ -183,8 +195,3 @@ public class AWSSigV4Signer: ClientRuntime.Signer {
}
}
}

public struct SigningResult<T> {
public let output: T
public let signature: String
}
Original file line number Diff line number Diff line change
@@ -40,6 +40,8 @@ final class AWSMessageEncoderStreamTests: XCTestCase {
.build()

let messageSigner = AWSEventStream.AWSMessageSigner(encoder: messageEncoder) {
return AWSSigV4Signer()
} signingConfig: {
return try await context.makeEventStreamSigningConfig()
} requestSignature: {
return context.getRequestSignature()
@@ -68,6 +70,8 @@ final class AWSMessageEncoderStreamTests: XCTestCase {
.build()

let messageSigner = AWSEventStream.AWSMessageSigner(encoder: messageEncoder) {
return AWSSigV4Signer()
} signingConfig: {
return try await context.makeEventStreamSigningConfig()
} requestSignature: {
return context.getRequestSignature()
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ class Sigv4SigningTests: XCTestCase {

let messagePayload = try! encoder.encode(message: message)

let result = try! await AWSSigV4Signer.signEvent(payload: messagePayload,
let result = try! await AWSSigV4Signer().signEvent(payload: messagePayload,
previousSignature: prevSignature,
signingConfig: signingConfig)
XCTAssertEqual(":date", result.output.headers[0].name)
Loading