From 99b7d6223cfee185e0735633f45eaf78fe5d1e0f Mon Sep 17 00:00:00 2001 From: Waqar Ahmed Khan Date: Thu, 2 Feb 2023 15:02:34 -0800 Subject: [PATCH] Feat: Chunk Signer (#151) --- .../auth/signing/Signer.swift | 105 ++++++++++- .../auth/signing/SigningConfig.swift | 46 ++--- .../AwsCommonRuntimeKit/http/HTTPHeader.swift | 19 ++ .../http/HTTPRequestOptions.swift | 2 +- .../http/HTTPStreamCallbackCore.swift | 8 +- .../auth/ChunkSignerTests.swift | 165 ++++++++++++++++++ .../auth/SignerTests.swift | 7 +- aws-common-runtime/aws-c-auth | 2 +- 8 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 Test/AwsCommonRuntimeKitTests/auth/ChunkSignerTests.swift diff --git a/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift b/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift index f1b4ec926..a9b646c39 100644 --- a/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift +++ b/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift @@ -61,7 +61,7 @@ public class Signer { allocator.rawValue, signable, configBasePointer, - onSigningComplete, + onRequestSigningComplete, signRequestCore.passRetained()) != AWS_OP_SUCCESS { @@ -72,6 +72,82 @@ public class Signer { } } } + + /// Signs a body chunk according to the supplied signing configuration + /// - Parameters: + /// - chunk: Chunk to sign + /// - previousSignature: The signature of the previous component of the request: either the request itself for the first chunk, + /// or the previous chunk otherwise. + /// - config: The `SigningConfig` to use when signing. + /// - allocator: (Optional) allocator to override + /// - Returns: Signature of the chunk + /// - Throws: CommonRunTimeError.crtError + public static func signChunk(chunk: Data, + previousSignature: String, + config: SigningConfig, + allocator: Allocator = defaultAllocator) async throws -> String { + let iStreamCore = IStreamCore(iStreamable: ByteBuffer(data: chunk), allocator: allocator) + guard let signable = previousSignature.withByteCursorPointer({ previousSignatureCursor in + aws_signable_new_chunk(allocator.rawValue, iStreamCore.rawValue, previousSignatureCursor.pointee) + }) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + defer { + aws_signable_destroy(signable) + } + + return try await sign(config: config, signable: signable, allocator: allocator) + } + + /// Signs trailing headers according to the supplied signing configuration + /// - Parameters: + /// - headers: list of headers to be sent in the trailer. + /// - previousSignature: The signature of the previous component of the request: either the request itself for the first chunk, + /// or the previous chunk otherwise. + /// - config: The `SigningConfig` to use when signing. + /// - allocator: (Optional) allocator to override + /// - Returns: Signing Result + /// - Throws: CommonRunTimeError.crtError + public static func signTrailerHeaders(headers: [HTTPHeader], + previousSignature: String, + config: SigningConfig, + allocator: Allocator = defaultAllocator) async throws -> String { + + guard let signable = previousSignature.withByteCursorPointer({ previousSignatureCursor in + headers.withCHeaders(allocator: allocator) { cHeaders in + aws_signable_new_trailing_headers(allocator.rawValue, cHeaders, previousSignatureCursor.pointee) + } + }) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + defer { + aws_signable_destroy(signable) + } + return try await sign(config: config, signable: signable, allocator: allocator) + } + + private static func sign(config: SigningConfig, + signable: UnsafePointer, + allocator: Allocator) async throws -> String { + + try await withCheckedThrowingContinuation { continuation in + config.withCPointer { configPointer in + configPointer.withMemoryRebound(to: aws_signing_config_base.self, + capacity: 1) { configBasePointer in + let continuationCore = ContinuationCore(continuation: continuation) + if aws_sign_request_aws(allocator.rawValue, + signable, + configBasePointer, + onSigningComplete, + continuationCore.passRetained()) + != AWS_OP_SUCCESS { + continuationCore.release() + continuation.resume(throwing: CommonRunTimeError.crtError(.makeFromLastError())) + } + } + } + } + } } class SignRequestCore { @@ -102,9 +178,9 @@ class SignRequestCore { } } -private func onSigningComplete(signingResult: UnsafeMutablePointer?, - errorCode: Int32, - userData: UnsafeMutableRawPointer!) { +private func onRequestSigningComplete(signingResult: UnsafeMutablePointer?, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { let signRequestCore = Unmanaged.fromOpaque(userData).takeRetainedValue() if errorCode != AWS_OP_SUCCESS { signRequestCore.continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) @@ -121,3 +197,24 @@ private func onSigningComplete(signingResult: UnsafeMutablePointer?, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { + let chunkSignerCore = Unmanaged>.fromOpaque(userData).takeRetainedValue() + guard errorCode == AWS_OP_SUCCESS else { + chunkSignerCore.continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + return + } + + // Success + var awsStringPointer: UnsafeMutablePointer! + guard aws_signing_result_get_property( + signingResult!, + g_aws_signature_property_name, + &awsStringPointer) == AWS_OP_SUCCESS else { + chunkSignerCore.continuation.resume(throwing: CommonRunTimeError.crtError(.makeFromLastError())) + return + } + chunkSignerCore.continuation.resume(returning: String(awsString: awsStringPointer)!) +} diff --git a/Source/AwsCommonRuntimeKit/auth/signing/SigningConfig.swift b/Source/AwsCommonRuntimeKit/auth/signing/SigningConfig.swift index e5bfe6985..02f914d81 100644 --- a/Source/AwsCommonRuntimeKit/auth/signing/SigningConfig.swift +++ b/Source/AwsCommonRuntimeKit/auth/signing/SigningConfig.swift @@ -137,32 +137,22 @@ private func onShouldSignHeader(nameCursor: UnsafePointer!, } public enum SignatureType { - /** - A signature for a full http request should be computed, with header updates applied to the signing result. - */ + + /// A signature for a full http request should be computed, with header updates applied to the signing result. case requestHeaders - /** - A signature for a full http request should be computed, with query param updates applied to the signing result. - */ + /// A signature for a full http request should be computed, with query param updates applied to the signing result. case requestQueryParams - /** - Compute a signature for a payload chunk. The signable's input stream should be the chunk data and the - signable should contain the most recent signature value (either the original http request or the most recent - chunk) in the "previous-signature" property. - */ + /// Compute a signature for a payload chunk. The signable's input stream should be the chunk data and the + /// signable should contain the most recent signature value (either the original http request or the most recent + /// chunk) in the "previous-signature" property. case requestChunk - /** - Compute a signature for an event stream event. The signable's input stream should be the event payload, the - signable should contain the most recent signature value (either the original http request or the most recent - event) in the "previous-signature" property as well as any event headers that should be signed with the - exception of ":date" - - This option is not yet supported. - */ - case requestEvent + /// Compute a signature for the trailing headers. + /// the signable should contain the most recent signature value (either the original http request or the most recent + /// chunk) in the "previous-signature" property. + case requestTrailingHeaders } public enum SignedBodyHeaderType { @@ -174,14 +164,26 @@ public enum SignedBodyHeaderType { case contentSha256 } +/// Optional string to use as the canonical request's body value. +/// Typically, this is the SHA-256 of the (request/chunk/event) payload, written as lowercase hex. +/// If this has been precalculated, it can be set here. Special values used by certain services can also be set. public enum SignedBodyValue: String { - /// if string is empty a public value will be calculated from the payload during signing + /// if empty, a public value will be calculated from the payload during signing case empty = "" + /// For empty sha256 case emptySha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" /// Use this in the case of needing to not use the payload for signing case unsignedPayload = "UNSIGNED-PAYLOAD" + /// For streaming sha256 payload case streamingSha256Payload = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" + /// For streaming sigv4a sha256 payload + case streamingECDSA_P256Sha256Payload = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD" + /// For streaming sigv4a sha256 payload trailer + case streamingECDSA_P256Sha256PayloadTrailer = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER" + /// For streaming sigv4a sha256 events case streamingSha256Events = "STREAMING-AWS4-HMAC-SHA256-EVENTS" + /// For streaming unsigned payload trailer + case streamingUnSignedPayloadTrailer = "STREAMING-UNSIGNED-PAYLOAD-TRAILER" } public enum SigningAlgorithmType { @@ -201,7 +203,7 @@ extension SignatureType: RawRepresentable, CaseIterable { case .requestHeaders: return AWS_ST_HTTP_REQUEST_HEADERS case .requestQueryParams: return AWS_ST_HTTP_REQUEST_QUERY_PARAMS case .requestChunk: return AWS_ST_HTTP_REQUEST_CHUNK - case .requestEvent: return AWS_ST_HTTP_REQUEST_EVENT + case .requestTrailingHeaders: return AWS_ST_HTTP_REQUEST_TRAILING_HEADERS } } } diff --git a/Source/AwsCommonRuntimeKit/http/HTTPHeader.swift b/Source/AwsCommonRuntimeKit/http/HTTPHeader.swift index 44687cb2b..38f866e60 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPHeader.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPHeader.swift @@ -28,3 +28,22 @@ public class HTTPHeader: CStruct { } } } + +extension Array where Element == HTTPHeader { + func withCHeaders(allocator: Allocator, + _ body: (OpaquePointer) -> Result) -> Result { + let cHeaders: OpaquePointer = aws_http_headers_new(allocator.rawValue) + defer { + aws_http_headers_release(cHeaders) + } + forEach { + $0.withCPointer { + guard aws_http_headers_add_header(cHeaders, $0) == AWS_OP_SUCCESS else { + let error = CRTError.makeFromLastError() + fatalError("Unable to add header due to error code: \(error.code) message:\(error.message)") + } + } + } + return body(cHeaders) + } +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift b/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift index 0057368cd..62b9554f4 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift @@ -39,7 +39,7 @@ public struct HTTPRequestOptions { /// When using HTTP/2, set http2ManualDataWrites to true to specify that request body data will be provided over time. /// The stream will only be polled for writing when data has been supplied via `HTTP2Stream.writeData` public var http2ManualDataWrites: Bool = false - + public init(request: HTTPRequestBase, onInterimResponse: OnInterimResponse? = nil, onResponse: @escaping OnResponse, diff --git a/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift b/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift index 2b9257003..0d446aa7a 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift @@ -64,10 +64,10 @@ private func onResponseHeaderBlockDone(stream: UnsafeMutablePointer Credentials { + try! Credentials(accessKey: accessKey, secret: secret) + } + + func makeChunkedRequestSigningConfig(sigv4: Bool = true) -> SigningConfig { + SigningConfig( + algorithm: sigv4 ? .signingV4 : .signingV4Asymmetric, + signatureType: .requestHeaders, + service: service, + region: region, + date: getDate(), + credentials: makeCredentials(), + signedBodyHeader: .contentSha256, + signedBodyValue: sigv4 ? .streamingSha256Payload : .streamingECDSA_P256Sha256Payload, + useDoubleURIEncode: false) + } + + func makeChunkedSigningConfig(sigv4: Bool = true) -> SigningConfig { + SigningConfig( + algorithm: sigv4 ? .signingV4 : .signingV4Asymmetric, + signatureType: .requestChunk, + service: service, + region: region, + date: getDate(), + credentials: makeCredentials(), + useDoubleURIEncode: false) + } + + func makeTrailingSigningConfig() -> SigningConfig { + SigningConfig( + algorithm: .signingV4, + signatureType: .requestTrailingHeaders, + service: service, + region: region, + date: getDate(), + credentials: makeCredentials(), + useDoubleURIEncode: false) + } + + func makeChunkedRequest(trailer: Bool = false) -> HTTPRequestBase { + let request = try! HTTPRequest(method: "PUT", path: url.path, allocator: allocator) + request.addHeaders(headers: [ + HTTPHeader(name: "Host", value: url.host!), + HTTPHeader(name: "x-amz-storage-class", value: "REDUCED_REDUNDANCY"), + HTTPHeader(name: "Content-Encoding", value: "aws-chunked"), + HTTPHeader(name: "x-amz-decoded-content-length", value: "66560"), + HTTPHeader(name: "Content-Length", value: "66824"), + ]) + if trailer { + request.addHeader(header: HTTPHeader(name: "x-amz-trailer", value: "first,second,third")) + } + return request + } + + func getDate() -> Date { + let formatter = DateFormatter() + + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return formatter.date(from: date)! + } +} diff --git a/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift b/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift index 7397d4a98..6c637f8fa 100644 --- a/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift +++ b/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift @@ -29,9 +29,10 @@ class SignerTests: XCBaseTestCase { credentialsProvider: provider, shouldSignHeader: shouldSignHeader) - let signedRequest = try await Signer.signRequest(request: request, - config: config, - allocator: allocator) + let signedRequest = try await Signer.signRequest( + request: request, + config: config, + allocator: allocator) XCTAssertNotNil(signedRequest) let headers = signedRequest.getHeaders() XCTAssert(headers.contains(where: { diff --git a/aws-common-runtime/aws-c-auth b/aws-common-runtime/aws-c-auth index 97133a2b5..dd505b55f 160000 --- a/aws-common-runtime/aws-c-auth +++ b/aws-common-runtime/aws-c-auth @@ -1 +1 @@ -Subproject commit 97133a2b5dbca1ccdf88cd6f44f39d0531d27d12 +Subproject commit dd505b55fd46222834f35c6e54165d8cbebbfaaa