Skip to content

Commit 938a6b1

Browse files
authored
kn: signing (#103)
1 parent a79c167 commit 938a6b1

File tree

17 files changed

+1551
-239
lines changed

17 files changed

+1551
-239
lines changed

.github/workflows/lint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
- '!main'
8+
pull_request:
9+
branches: [ main ]
10+
workflow_dispatch:
11+
12+
env:
13+
PACKAGE_NAME: aws-crt-kotlin
14+
15+
jobs:
16+
ktlint:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout sources
20+
uses: actions/checkout@v2
21+
- name: Lint ${{ env.PACKAGE_NAME }}
22+
run: |
23+
./gradlew ktlint

aws-crt-kotlin/api/aws-crt-kotlin.api

Lines changed: 998 additions & 0 deletions
Large diffs are not rendered by default.

aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt

Lines changed: 151 additions & 150 deletions
Large diffs are not rendered by default.

aws-crt-kotlin/native/interop/crt.def

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ headers = aws/common/allocator.h \
2020
aws/http/request_response.h \
2121
aws/http/proxy.h \
2222
aws/compression/compression.h \
23+
aws/auth/credentials.h \
24+
aws/auth/signing.h \
25+
aws/auth/signing_config.h \
26+
aws/auth/signable.h \
27+
aws/auth/signing_result.h \
28+
aws/compression/compression.h \
2329
aws/cal/hash.h \
30+
aws/cal/cal.h \
2431
aws/checksums/crc.h
25-
headerFilter = aws/common/* aws/io/* aws/http/* aws/compression/* aws/cal/* aws/checksums/*
32+
headerFilter = aws/common/* aws/io/* aws/http/* aws/compression/* aws/auth/* aws/checksums/* aws/cal/*
2633

2734
linkerOpts.osx = -framework Security
2835
linkerOpts.ios = -framework Security

aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/CRTNative.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public actual object CRT {
3434
aws_compression_library_init(Allocator.Default)
3535
aws_io_library_init(Allocator.Default)
3636
aws_http_library_init(Allocator.Default)
37+
aws_auth_library_init(Allocator.Default)
38+
aws_cal_library_init(Allocator.Default)
3739

3840
Logging.initialize(config)
3941
aws_register_log_subject_info_list(s_crt_log_subject_list.ptr)
@@ -110,6 +112,8 @@ private fun cleanup() {
110112
aws_compression_library_clean_up()
111113
aws_io_library_clean_up()
112114
aws_common_library_clean_up()
115+
aws_auth_library_clean_up()
116+
aws_cal_library_clean_up()
113117

114118
s_crt_kotlin_clean_up()
115119
}

aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/auth/credentials/StaticCredentialsProviderNative.kt

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,11 @@ package aws.sdk.kotlin.crt.auth.credentials
88
/**
99
* A credentials provider for a fixed set of credentials
1010
*/
11-
public actual class StaticCredentialsProvider internal actual constructor(builder: StaticCredentialsProviderBuilder) :
12-
CredentialsProvider {
13-
public actual companion object {}
14-
15-
override suspend fun getCredentials(): Credentials {
16-
TODO("Not yet implemented")
17-
}
11+
public actual class StaticCredentialsProvider internal actual constructor(builder: StaticCredentialsProviderBuilder) : CredentialsProvider {
12+
private val credentials = Credentials(builder.accessKeyId!!, builder.secretAccessKey!!, builder.sessionToken)
1813

19-
override fun close() {
20-
TODO("Not yet implemented")
21-
}
22-
23-
override suspend fun waitForShutdown() {
24-
TODO("Not yet implemented")
25-
}
14+
public actual companion object {}
15+
override suspend fun getCredentials(): Credentials = credentials
16+
override fun close() { }
17+
override suspend fun waitForShutdown() { }
2618
}

aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/auth/signing/AwsSignerNative.kt

Lines changed: 234 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@
55

66
package 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
812
import 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+
)

aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/http/HttpClientConnectionManagerNative.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ package aws.sdk.kotlin.crt.http
88
import aws.sdk.kotlin.crt.*
99
import aws.sdk.kotlin.crt.Allocator
1010
import aws.sdk.kotlin.crt.awsAssertOpSuccess
11+
import aws.sdk.kotlin.crt.io.SocketDomain
1112
import aws.sdk.kotlin.crt.io.SocketOptions
13+
import aws.sdk.kotlin.crt.io.SocketType
1214
import aws.sdk.kotlin.crt.io.requiresTls
1315
import aws.sdk.kotlin.crt.util.*
1416
import cnames.structs.aws_http_connection_manager
@@ -155,13 +157,24 @@ public actual class HttpClientConnectionManager actual constructor(
155157

156158
// initialize from Kotlin equivalent
157159
private fun aws_socket_options.kinit(opts: SocketOptions) {
158-
type = aws_socket_type.byValue(opts.type.value.convert())
159-
domain = aws_socket_domain.byValue(opts.domain.value.convert())
160+
type = opts.type.toNativeSocketType()
161+
domain = opts.domain.toNativeSocketDomain()
160162
connect_timeout_ms = opts.connectTimeoutMs.convert()
161163
keep_alive_interval_sec = opts.keepAliveIntervalSecs.convert()
162164
keep_alive_timeout_sec = opts.keepAliveTimeoutSecs.convert()
163165
}
164166

167+
private fun SocketType.toNativeSocketType() = when (this) {
168+
SocketType.STREAM -> aws_socket_type.AWS_SOCKET_STREAM
169+
SocketType.DGRAM -> aws_socket_type.AWS_SOCKET_DGRAM
170+
}
171+
172+
private fun SocketDomain.toNativeSocketDomain() = when (this) {
173+
SocketDomain.IPv4 -> aws_socket_domain.AWS_SOCKET_IPV4
174+
SocketDomain.IPv6 -> aws_socket_domain.AWS_SOCKET_IPV6
175+
SocketDomain.LOCAL -> aws_socket_domain.AWS_SOCKET_LOCAL
176+
}
177+
165178
private fun onShutdownComplete(userdata: COpaquePointer?) {
166179
if (userdata == null) return
167180
val notify = userdata.asStableRef<ShutdownChannel>()

0 commit comments

Comments
 (0)