55
66package aws.sdk.kotlin.crt.auth.signing
77
8+ import aws.sdk.kotlin.crt.*
9+ import aws.sdk.kotlin.crt.Allocator
10+ import aws.sdk.kotlin.crt.auth.credentials.Credentials
11+ import aws.sdk.kotlin.crt.awsAssertOpSuccess
812import aws.sdk.kotlin.crt.http.*
13+ import aws.sdk.kotlin.crt.util.asAwsByteCursor
14+ import aws.sdk.kotlin.crt.util.initFromCursor
15+ import aws.sdk.kotlin.crt.util.toAwsString
16+ import aws.sdk.kotlin.crt.util.toKString
17+ import kotlinx.cinterop.*
18+ import kotlinx.coroutines.channels.Channel
19+ import kotlinx.coroutines.runBlocking
20+ import libcrt.*
21+ import platform.posix.UINT64_MAX
922
1023/* *
1124 * Static class for a variety of AWS signing APIs.
@@ -14,30 +27,244 @@ public actual object AwsSigner {
1427 public actual suspend fun signRequest (
1528 request : HttpRequest ,
1629 config : AwsSigningConfig ,
17- ): HttpRequest {
18- TODO (" Not yet implemented" )
19- }
30+ ): HttpRequest = checkNotNull(sign(request, config).signedRequest) { " received null signed request" }
2031
2132 public actual suspend fun sign (
2233 request : HttpRequest ,
2334 config : AwsSigningConfig ,
24- ): AwsSigningResult {
25- TODO (" Not yet implemented" )
35+ ): AwsSigningResult = memScoped {
36+ val nativeRequest = request.toNativeRequest().pin()
37+
38+ // Pair of HTTP request and callback channel containing the signature
39+ val userData = nativeRequest to Channel <ByteArray >(1 )
40+ val userDataStableRef = StableRef .create(userData)
41+
42+ val signable = checkNotNull(
43+ aws_signable_new_http_request(
44+ allocator = Allocator .Default .allocator,
45+ request = nativeRequest.get(),
46+ ),
47+ ) { " aws_signable_new_http_request" }
48+
49+ val nativeSigningConfig: CPointer < aws_signing_config_base> = config.toNativeSigningConfig().reinterpret()
50+
51+ awsAssertOpSuccess(
52+ aws_sign_request_aws(
53+ allocator = Allocator .Default .allocator,
54+ signable = signable,
55+ base_config = nativeSigningConfig,
56+ on_complete = staticCFunction(::signCallback),
57+ userdata = userDataStableRef.asCPointer(),
58+ ),
59+ ) { " sign() aws_sign_request_aws" }
60+
61+ val callbackChannel = userDataStableRef.get().second
62+ val signature = callbackChannel.receive() // wait for async signing to complete....
63+ return AwsSigningResult (nativeRequest.get().toHttpRequest(), signature)
2664 }
2765
2866 public actual suspend fun signChunk (
2967 chunkBody : ByteArray ,
3068 prevSignature : ByteArray ,
3169 config : AwsSigningConfig ,
3270 ): AwsSigningResult {
33- TODO (" Not yet implemented" )
71+ val chunkSignable = memScoped {
72+ chunkBody.usePinned { chunkBodyPinned ->
73+ val chunkInputStream: CValuesRef < aws_input_stream> = checkNotNull(
74+ aws_input_stream_new_from_cursor(
75+ Allocator .Default .allocator,
76+ chunkBodyPinned.asAwsByteCursor(),
77+ ),
78+ ) { " signChunk() aws_input_stream_new_from_cursor" }
79+
80+ prevSignature.usePinned { prevSignaturePinned ->
81+ checkNotNull(
82+ aws_signable_new_chunk(Allocator .Default .allocator, chunkInputStream, prevSignaturePinned.asAwsByteCursor()),
83+ ) { " aws_signable_new_chunk unexpectedly null" }
84+ }
85+ }
86+ }
87+
88+ return signChunkSignable(chunkSignable, config)
3489 }
3590
3691 public actual suspend fun signChunkTrailer (
3792 trailingHeaders : Headers ,
3893 prevSignature : ByteArray ,
3994 config : AwsSigningConfig ,
4095 ): AwsSigningResult {
41- TODO (" Not yet implemented" )
96+ val chunkTrailerSignable = memScoped {
97+ val nativeTrailingHeaders = aws_http_headers_new(Allocator .Default .allocator)
98+ trailingHeaders.forEach { key, values ->
99+ key.encodeToByteArray().usePinned { keyPinned ->
100+ val keyCursor = keyPinned.asAwsByteCursor()
101+ values.forEach {
102+ it.encodeToByteArray().usePinned { valuePinned ->
103+ val valueCursor = valuePinned.asAwsByteCursor()
104+ awsAssertOpSuccess(aws_http_headers_add(nativeTrailingHeaders, keyCursor, valueCursor)) {
105+ " signChunkTrailer() aws_http_headers_add"
106+ }
107+ }
108+ }
109+ }
110+ }
111+
112+ checkNotNull(
113+ aws_signable_new_trailing_headers(
114+ Allocator .Default .allocator,
115+ nativeTrailingHeaders,
116+ prevSignature.usePinned { it.asAwsByteCursor() },
117+ ),
118+ ) { " aws_signable_new_trailing_headers unexpectedly null" }
119+ }
120+
121+ return signChunkSignable(chunkTrailerSignable, config)
122+ }
123+ }
124+
125+ private fun signChunkSignable (signable : CPointer <aws_signable>, config : AwsSigningConfig ): AwsSigningResult = memScoped {
126+ val callbackChannel = Channel <ByteArray >(1 )
127+ val callbackChannelStableRef = StableRef .create(callbackChannel)
128+
129+ val nativeConfig: CPointer < aws_signing_config_base> = config.toNativeSigningConfig().reinterpret()
130+
131+ awsAssertOpSuccess(
132+ aws_sign_request_aws(
133+ allocator = Allocator .Default .allocator,
134+ signable = signable,
135+ base_config = nativeConfig,
136+ on_complete = staticCFunction(::signChunkCallback),
137+ userdata = callbackChannelStableRef.asCPointer(),
138+ ),
139+ ) { " aws_sign_request_aws() failed in signChunkSignable" }
140+
141+ // wait for async signing to complete....
142+ val signature = runBlocking { callbackChannel.receive() }.also {
143+ callbackChannelStableRef.dispose()
144+ callbackChannel.close()
42145 }
146+
147+ return AwsSigningResult (null , signature)
43148}
149+
150+ /* *
151+ * Get the signature of a given [aws_signing_result].
152+ * The signature is found in the "signature" property on the signing result.
153+ */
154+ private fun CPointer<aws_signing_result>.getSignature (): ByteArray {
155+ val signature = Allocator .Default .allocPointerTo< aws_string> ()
156+ val propertyName = " signature" .toAwsString()
157+
158+ awsAssertOpSuccess(aws_signing_result_get_property(this , propertyName, signature.ptr)) {
159+ " aws_signing_result_get_property"
160+ }
161+
162+ val kSignature = signature.value?.toKString()?.encodeToByteArray()
163+ return checkNotNull(kSignature) { " signature was null" }
164+ }
165+
166+ private fun AwsSigningAlgorithm.toNativeSigningAlgorithm (): aws_signing_algorithm = when (this ) {
167+ AwsSigningAlgorithm .SIGV4 -> aws_signing_algorithm.AWS_SIGNING_ALGORITHM_V4
168+ AwsSigningAlgorithm .SIGV4_ASYMMETRIC -> aws_signing_algorithm.AWS_SIGNING_ALGORITHM_V4_ASYMMETRIC
169+ }
170+
171+ private fun AwsSignatureType.toNativeSignatureType (): aws_signature_type = when (this ) {
172+ AwsSignatureType .HTTP_REQUEST_VIA_HEADERS -> aws_signature_type.AWS_ST_HTTP_REQUEST_HEADERS
173+ AwsSignatureType .HTTP_REQUEST_VIA_QUERY_PARAMS -> aws_signature_type.AWS_ST_HTTP_REQUEST_QUERY_PARAMS
174+ AwsSignatureType .HTTP_REQUEST_EVENT -> aws_signature_type.AWS_ST_HTTP_REQUEST_EVENT
175+ AwsSignatureType .HTTP_REQUEST_CHUNK -> aws_signature_type.AWS_ST_HTTP_REQUEST_CHUNK
176+ AwsSignatureType .HTTP_REQUEST_TRAILING_HEADERS -> aws_signature_type.AWS_ST_HTTP_REQUEST_TRAILING_HEADERS
177+ }
178+
179+ private fun AwsSignedBodyHeaderType.toNativeSignedBodyHeaderType () = when (this ) {
180+ AwsSignedBodyHeaderType .NONE -> aws_signed_body_header_type.AWS_SBHT_NONE
181+ AwsSignedBodyHeaderType .X_AMZ_CONTENT_SHA256 -> aws_signed_body_header_type.AWS_SBHT_X_AMZ_CONTENT_SHA256
182+ }
183+
184+ private fun AwsSigningConfig.toNativeSigningConfig (): CPointer <aws_signing_config_aws> {
185+ val config = Allocator .Default .alloc< aws_signing_config_aws> ()
186+
187+ config.apply {
188+ config_type = AWS_SIGNING_CONFIG_AWS
189+ algorithm = this @toNativeSigningConfig.algorithm.toNativeSigningAlgorithm()
190+ signature_type = this @toNativeSigningConfig.signatureType.toNativeSignatureType()
191+ region.initFromCursor(this @toNativeSigningConfig.region.toAwsString().asAwsByteCursor())
192+ service.initFromCursor(this @toNativeSigningConfig.service.toAwsString().asAwsByteCursor())
193+ aws_date_time_init_epoch_millis(date.ptr, this @toNativeSigningConfig.date.toULong())
194+
195+ this @toNativeSigningConfig.shouldSignHeader?.let {
196+ val shouldSignHeaderStableRef = StableRef .create(it)
197+ should_sign_header = staticCFunction(::nativeShouldSignHeaderFn)
198+ should_sign_header_ud = shouldSignHeaderStableRef.asCPointer()
199+ }
200+
201+ flags.use_double_uri_encode = if (this @toNativeSigningConfig.useDoubleUriEncode) 1u else 0u
202+ flags.should_normalize_uri_path = if (this @toNativeSigningConfig.normalizeUriPath) 1u else 0u
203+ flags.omit_session_token = if (this @toNativeSigningConfig.omitSessionToken) 1u else 0u
204+
205+ this @toNativeSigningConfig.signedBodyValue?.let {
206+ signed_body_value.initFromCursor(it.toAwsString().asAwsByteCursor())
207+ }
208+ signed_body_header = this @toNativeSigningConfig.signedBodyHeader.toNativeSignedBodyHeaderType()
209+
210+ credentials = this @toNativeSigningConfig.credentials?.toNativeCredentials()
211+ // TODO implement native CredentialsProvider
212+ // credentials_provider =
213+
214+ expiration_in_seconds = this @toNativeSigningConfig.expirationInSeconds.toULong()
215+ }
216+
217+ return config.ptr
218+ }
219+
220+ private typealias ShouldSignHeaderFunction = (String ) -> Boolean
221+ private fun nativeShouldSignHeaderFn (headerName : CPointer <aws_byte_cursor>? , userData : COpaquePointer ? ): Boolean {
222+ checkNotNull(headerName) { " aws_should_sign_header_fn expected non-null header name" }
223+ if (userData == null ) { return true }
224+
225+ val kShouldSignHeaderFn = userData.asStableRef<ShouldSignHeaderFunction >().get()
226+ val kHeaderName = headerName.pointed.toKString()
227+ return kShouldSignHeaderFn(kHeaderName)
228+ }
229+
230+ /* *
231+ * Callback for standard request signing. Applies the given signing result to the HTTP message and then returns the
232+ * signature via callback channel.
233+ */
234+ private fun signCallback (signingResult : CPointer <aws_signing_result>? , errorCode : Int , userData : COpaquePointer ? ) {
235+ awsAssertOpSuccess(errorCode) { " signing failed with code $errorCode : ${CRT .errorString(errorCode)} " }
236+ checkNotNull(signingResult) { " signing callback received null aws_signing_result" }
237+ checkNotNull(userData) { " signing callback received null user data" }
238+
239+ val (pinnedRequestToSign, callbackChannel) = userData
240+ .asStableRef<Pair <Pinned <CPointer <cnames.structs.aws_http_message>>, Channel <ByteArray >>>()
241+ .get()
242+
243+ val requestToSign = pinnedRequestToSign.get()
244+
245+ awsAssertOpSuccess(aws_apply_signing_result_to_http_request(requestToSign, Allocator .Default .allocator, signingResult)) {
246+ " aws_apply_signing_result_to_http_request"
247+ }
248+
249+ runBlocking { callbackChannel.send(signingResult.getSignature()) }
250+ }
251+
252+ /* *
253+ * Callback for chunked signing. Returns the signature via callback channel.
254+ */
255+ private fun signChunkCallback (signingResult : CPointer <aws_signing_result>? , errorCode : Int , userData : COpaquePointer ? ) {
256+ awsAssertOpSuccess(errorCode) { " signing failed with code $errorCode : ${CRT .errorString(errorCode)} " }
257+ checkNotNull(signingResult) { " signing callback received null aws_signing_result" }
258+ checkNotNull(userData) { " signing callback received null user data" }
259+
260+ val callbackChannel = userData.asStableRef<Channel <ByteArray >>().get()
261+ runBlocking { callbackChannel.send(signingResult.getSignature()) }
262+ }
263+
264+ private fun Credentials.toNativeCredentials (): CPointer <cnames.structs.aws_credentials>? = aws_credentials_new_from_string(
265+ Allocator .Default .allocator,
266+ access_key_id = accessKeyId.toAwsString(),
267+ secret_access_key = secretAccessKey.toAwsString(),
268+ session_token = sessionToken?.toAwsString(),
269+ expiration_timepoint_seconds = UINT64_MAX , // FIXME?: Our Credentials do not have an expiration field
270+ )
0 commit comments