diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt index 32b891486..8204b6f7f 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt @@ -13,7 +13,14 @@ import kotlinx.rpc.grpc.codec.ThrowingMessageCodecResolver import kotlinx.rpc.grpc.codec.plus import kotlinx.rpc.grpc.descriptor.GrpcServiceDelegate import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor -import kotlinx.rpc.grpc.internal.* +import kotlinx.rpc.grpc.internal.GrpcDefaultCallOptions +import kotlinx.rpc.grpc.internal.MethodDescriptor +import kotlinx.rpc.grpc.internal.MethodType +import kotlinx.rpc.grpc.internal.bidirectionalStreamingRpc +import kotlinx.rpc.grpc.internal.clientStreamingRpc +import kotlinx.rpc.grpc.internal.serverStreamingRpc +import kotlinx.rpc.grpc.internal.type +import kotlinx.rpc.grpc.internal.unaryRpc import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap import kotlin.time.Duration @@ -124,10 +131,11 @@ public class GrpcClient internal constructor( public fun GrpcClient( hostname: String, port: Int, + credentials: ClientCredentials? = null, messageCodecResolver: MessageCodecResolver = EmptyMessageCodecResolver, configure: ManagedChannelBuilder<*>.() -> Unit = {}, ): GrpcClient { - val channel = ManagedChannelBuilder(hostname, port).apply(configure).buildChannel() + val channel = ManagedChannelBuilder(hostname, port, credentials).apply(configure).buildChannel() return GrpcClient(channel, messageCodecResolver) } @@ -136,9 +144,10 @@ public fun GrpcClient( */ public fun GrpcClient( target: String, + credentials: ClientCredentials? = null, messageCodecResolver: MessageCodecResolver = EmptyMessageCodecResolver, configure: ManagedChannelBuilder<*>.() -> Unit = {}, ): GrpcClient { - val channel = ManagedChannelBuilder(target).apply(configure).buildChannel() + val channel = ManagedChannelBuilder(target, credentials).apply(configure).buildChannel() return GrpcClient(channel, messageCodecResolver) } diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt index eb1e3abb7..1a73e9c74 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.job import kotlinx.rpc.RpcServer import kotlinx.rpc.descriptor.RpcCallable import kotlinx.rpc.descriptor.flowInvokator @@ -49,6 +48,7 @@ private typealias ResponseServer = Any */ public class GrpcServer internal constructor( override val port: Int = 8080, + credentials: ServerCredentials? = null, messageCodecResolver: MessageCodecResolver = EmptyMessageCodecResolver, parentContext: CoroutineContext = EmptyCoroutineContext, configure: ServerBuilder<*>.() -> Unit, @@ -61,7 +61,7 @@ public class GrpcServer internal constructor( private var isBuilt = false private lateinit var internalServer: Server - private val serverBuilder: ServerBuilder<*> = ServerBuilder(port).apply(configure) + private val serverBuilder: ServerBuilder<*> = ServerBuilder(port, credentials).apply(configure) private val registry: MutableHandlerRegistry by lazy { MutableHandlerRegistry().apply { serverBuilder.fallbackHandlerRegistry(this) } } @@ -192,10 +192,12 @@ public class GrpcServer internal constructor( */ public fun GrpcServer( port: Int, + credentials: ServerCredentials? = null, messageCodecResolver: MessageCodecResolver = EmptyMessageCodecResolver, parentContext: CoroutineContext = EmptyCoroutineContext, configure: ServerBuilder<*>.() -> Unit = {}, builder: RpcServer.() -> Unit = {}, ): GrpcServer { - return GrpcServer(port, messageCodecResolver, parentContext, configure).apply(builder).apply { build() } + return GrpcServer(port, credentials, messageCodecResolver, parentContext, configure).apply(builder) + .apply { build() } } diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt index 64f6ee4aa..bcfa015f5 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt @@ -69,9 +69,19 @@ public interface ManagedChannel { */ public expect abstract class ManagedChannelBuilder> { public fun usePlaintext(): T + + public abstract fun overrideAuthority(authority: String): T } -internal expect fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> -internal expect fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> +internal expect fun ManagedChannelBuilder( + hostname: String, + port: Int, + credentials: ClientCredentials? = null, +): ManagedChannelBuilder<*> + +internal expect fun ManagedChannelBuilder( + target: String, + credentials: ClientCredentials? = null, +): ManagedChannelBuilder<*> internal expect fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt index c35b6dfb9..90e4b317b 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt @@ -31,7 +31,7 @@ public expect abstract class ServerBuilder> { public abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T } -internal expect fun ServerBuilder(port: Int): ServerBuilder<*> +internal expect fun ServerBuilder(port: Int, credentials: ServerCredentials? = null): ServerBuilder<*> /** * Server for listening for and dispatching incoming calls. diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/credentials.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/credentials.kt new file mode 100644 index 000000000..dcebf9d5f --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/credentials.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public expect abstract class ClientCredentials +public expect abstract class ServerCredentials + +public expect class InsecureClientCredentials : ClientCredentials +public expect class InsecureServerCredentials : ServerCredentials + +public expect class TlsClientCredentials : ClientCredentials +public expect class TlsServerCredentials : ServerCredentials + +public fun TlsClientCredentials(configure: TlsClientCredentialsBuilder.() -> Unit = {}): ClientCredentials { + val builder = TlsClientCredentialsBuilder() + builder.configure() + return builder.build() +} + +public fun TlsServerCredentials( + certChain: String, + privateKey: String, + configure: TlsServerCredentialsBuilder.() -> Unit = {}, +): ServerCredentials { + val builder = TlsServerCredentialsBuilder(certChain, privateKey) + builder.configure() + return builder.build() +} + +public interface TlsClientCredentialsBuilder { + public fun trustManager(rootCertsPem: String): TlsClientCredentialsBuilder + public fun keyManager(certChainPem: String, privateKeyPem: String): TlsClientCredentialsBuilder +} + +public enum class TlsClientAuth { + /** Clients will not present any identity. */ + NONE, + + /** + * Clients are requested to present their identity, but clients without identities are + * permitted. + * Also, if the client certificate is provided but cannot be verified, + * the client is permitted. + */ + OPTIONAL, + + /** + * Clients are requested to present their identity, and are required to provide a valid + * identity. + */ + REQUIRE +} + +public interface TlsServerCredentialsBuilder { + public fun trustManager(rootCertsPem: String): TlsServerCredentialsBuilder + public fun clientAuth(clientAuth: TlsClientAuth): TlsServerCredentialsBuilder +} + +internal expect fun TlsClientCredentialsBuilder(): TlsClientCredentialsBuilder +internal expect fun TlsServerCredentialsBuilder( + certChain: String, + privateKey: String, +): TlsServerCredentialsBuilder + +internal expect fun TlsClientCredentialsBuilder.build(): ClientCredentials +internal expect fun TlsServerCredentialsBuilder.build(): ServerCredentials diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.kt index 165f41733..cbffa0b1b 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.kt @@ -13,5 +13,5 @@ public expect abstract class GrpcChannel { callOptions: GrpcCallOptions, ): ClientCall - public abstract fun authority(): String + public abstract fun authority(): String? } diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/certs.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/certs.kt new file mode 100644 index 000000000..e1540f5cc --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/certs.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.test + +// Certs are taken from grpc-java/testing/src/main/resources/certs + +val CA_PEM = """ + -----BEGIN CERTIFICATE----- + MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw + MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV + BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 + ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 + diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO + Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k + QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c + qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV + LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud + DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a + THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S + CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 + /OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt + bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw + eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw== + -----END CERTIFICATE----- + """.trimIndent() + +val SERVER_CERT_PEM = """ + -----BEGIN CERTIFICATE----- + MIIDtDCCApygAwIBAgIUbJfTREJ6k6/+oInWhV1O1j3ZT0IwDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw + MDMxODAzMTA0MloXDTMwMDMxNjAzMTA0MlowZTELMAkGA1UEBhMCVVMxETAPBgNV + BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl + LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz + Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY + GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe + 8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c + 6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV + YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo2swaTAJBgNV + HRMEAjAAMAsGA1UdDwQEAwIF4DBPBgNVHREESDBGghAqLnRlc3QuZ29vZ2xlLmZy + ghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55b3V0dWJlLmNvbYcE + wKgBAzANBgkqhkiG9w0BAQsFAAOCAQEAS8hDQA8PSgipgAml7Q3/djwQ644ghWQv + C2Kb+r30RCY1EyKNhnQnIIh/OUbBZvh0M0iYsy6xqXgfDhCB93AA6j0i5cS8fkhH + Jl4RK0tSkGQ3YNY4NzXwQP/vmUgfkw8VBAZ4Y4GKxppdATjffIW+srbAmdDruIRM + wPeikgOoRrXf0LA1fi4TqxARzeRwenQpayNfGHTvVF9aJkl8HoaMunTAdG5pIVcr + 9GKi/gEMpXUJbbVv3U5frX1Wo4CFo+rZWJ/LyCMeb0jciNLxSdMwj/E/ZuExlyeZ + gc9ctPjSMvgSyXEKv6Vwobleeg88V2ZgzenziORoWj4KszG/lbQZvg== + -----END CERTIFICATE----- + """.trimIndent() + +val SERVER_KEY_PEM = """ + -----BEGIN PRIVATE KEY----- + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq + 6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+ + WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t + PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86 + qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh + 23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte + MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU + koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj + 1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5 + nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB + 8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi + y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t + sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB + gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y + biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC + Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l + dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP + V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp + Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1 + QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA + xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys + DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83 + FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv + nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH + awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r + uGIYs9Ek7YXlXIRVrzMwcsrt1w== + -----END PRIVATE KEY----- + """.trimIndent() + +val CLIENT_CERT_PEM = """ + -----BEGIN CERTIFICATE----- + MIIDNzCCAh8CFGyX00RCepOv/qCJ1oVdTtY92U83MA0GCSqGSIb3DQEBCwUAMFYx + CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl + cm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMMBnRlc3RjYTAeFw0yMDAzMTgw + MTA2MTBaFw0zMDAzMTYwMTA2MTBaMFoxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApT + b21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEzAR + BgNVBAMMCnRlc3RjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB + AQCyqYRp+DXVp72NFbQH8hdhTZLycZXOlJhmMsrJmrjn2p7pI/8mTZ/0FC+SGWBG + ZV+ELiHrmCX5zfaILr9Iuw7Ghr3Vzoefi8r62rLupVPNi/qdqyjWk2dECHC9Z3+A + g3KzKTyerXWjKcvyKVmM0ZxE0RXhDW/RoQbqZsU2GKg1B2rhUU8KN0gVmKn0rJHO + xzRVSYeYLYp5Yn7KrtPJcKyo9aVuEr7dGANzpyF6lg/nYBWc+9SGwkoLdFvKvABY + JMyrbNhHUQfv0fzaZ0P86dfTENrDxzALrzGnqcx3KTrwJjkZ/aSr1tyD0/tXvukR + FiPxWBJhjHQ70GqTFQY19RbhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFXCewK8 + cWT+zWxXyGFnouFSBzTi0BMBJRrhsiNoiQxkqityJHWFExiQZie+7CA+EabXCQUB + +JwMSWM29j3mSw10DTfmC3rhheQqGxy304BZyUpdpvI2dt3p/mcsE7O+p4sQrSep + gijiDssKAfxTAmUM93N6+Q8yJK5immxlbeYfijoBvmkzyB/B+qNRPsx0n7aFGnfv + oWfkW296iPhWLiwknpC3xB6oK3vRbK4Zj1OaGb0grK7VN8EyhBix2xVF61i4dzCK + kMIpl7CUpw1Mb2z8q3F2bHBS7iF7g1Ccn5VGcO+aJ+6PWydaeqJ6VEBF0Nwv9woe + mL5AluNRLaqjZvE= + -----END CERTIFICATE----- + """.trimIndent() + +val CLIENT_KEY_PEM = """ + -----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyqYRp+DXVp72N + FbQH8hdhTZLycZXOlJhmMsrJmrjn2p7pI/8mTZ/0FC+SGWBGZV+ELiHrmCX5zfaI + Lr9Iuw7Ghr3Vzoefi8r62rLupVPNi/qdqyjWk2dECHC9Z3+Ag3KzKTyerXWjKcvy + KVmM0ZxE0RXhDW/RoQbqZsU2GKg1B2rhUU8KN0gVmKn0rJHOxzRVSYeYLYp5Yn7K + rtPJcKyo9aVuEr7dGANzpyF6lg/nYBWc+9SGwkoLdFvKvABYJMyrbNhHUQfv0fza + Z0P86dfTENrDxzALrzGnqcx3KTrwJjkZ/aSr1tyD0/tXvukRFiPxWBJhjHQ70GqT + FQY19RbhAgMBAAECggEAIL8JUhL4awyvpWhQ8xPgTSlWwbEn8BE0TacJnCILuhNM + BRdf8LlRk/8PKQwVpVF3TFbYSMI+U6b4hMVssfv3HVQc/083dHq+3XOwUCVlUstR + SAzTE2E5EDMr1stdh0SQhV4Nilfos9s5Uk1Z6IGSztoz1GgOErIc/mGPy/aA/hbr + fRWHvTp35+MbCJSvZuOeevX2iLs0dNzqdk6DiOWIH/BVGirVPtO6ykrkuTj1FWiN + hyZ3MBChShlNH2poNX46ntOc7nEus0qteOgxBK8lummFEtlehCA7hd/8xuvYlP0k + 7aN684LCRDajmAGpoZO57NSDYQhAFGZeUZ93SMFucQKBgQDe7GGkzZFEiv91u1q9 + lgMy1h5dZjIZKgQaOarPC6wCQMUdqCf6cSLsAPr4T8EDoWsnY7dSnrTZ6YCIFL1T + idg8M3BQXipICCJkFORS76pKKZ0wMn3/NgkSepsmNct91WHr6okvx4tOaoRCtdzU + g7jt4Mr3sfLCiZtqTQyySdMUEwKBgQDNK+ZFKL0XhkWZP+PGKjWG8LWpPiK3d78/ + wYBFXzSTGlkr6FvRmYtZeNwXWRYLB4UxZ9At4hbJVEdi/2dITOz/sehVDyCAjjs3 + gycsc3UJqiZbcw5XKhI5TWBuWxkKENdbMSayogVbp2aSYoRblH764//t0ACmbfTW + KUQRQPB/uwKBgQC5QjjjfPL8w4cJkGoYpFKELO2PMR7xSrmeEc6hwlFwjeNCgjy3 + JM6g0y++rIj7O2qRkY0IXFxvvF3UuWedxTCu1xC/uYHp2ti506LsScB7YZoAM/YB + 4iYn9Tx6xLoYGP0H0iGwU2SyBlNkHT8oXU+SYP5MWtYkVbeS3/VtNWz1gQKBgQCA + 6Nk4kN0mH7YxEKRzSOfyzeDF4oV7kuB2FYUbkTL+TirC3K58JiYY5Egc31trOKFm + Jlz1xz0b6DkmKWTiV3r9OPHKJ8P7IeJxAZWmZzCdDuwkv0i+WW+z0zsIe3JjEavN + 3zb6O7R0HtziksWoqMeTqZeO+wa9iw6vVKQw1wWEqwKBgFHfahFs0DZ5cUTpGpBt + F/AQG7ukgipB6N6AkB9kDbgCs1FLgd199MQrEncug5hfpq8QerbyMatmA+GXoGMb + 7vztKEH85yzp4n02FNL6H7xL4VVILvyZHdolmiORJ4qT2hZnl8pEQ2TYuF4RlHUd + nSwXX+2o0J/nF85fm4AwWKAc + -----END PRIVATE KEY----- + """.trimIndent() \ No newline at end of file diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcProtoTest.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcProtoTest.kt index 735f6f4f7..92050dd19 100644 --- a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcProtoTest.kt +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcProtoTest.kt @@ -8,30 +8,46 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.runTest import kotlinx.rpc.RpcServer +import kotlinx.rpc.grpc.ClientCredentials import kotlinx.rpc.grpc.GrpcClient import kotlinx.rpc.grpc.GrpcServer +import kotlinx.rpc.grpc.ServerCredentials abstract class GrpcProtoTest { private val serverMutex = Mutex() abstract fun RpcServer.registerServices() - protected fun runGrpcTest(test: suspend (GrpcClient) -> Unit) = runTest { + protected fun runGrpcTest( + serverCreds: ServerCredentials? = null, + clientCreds: ClientCredentials? = null, + overrideAuthority: String? = null, + test: suspend (GrpcClient) -> Unit, + ) = runTest { serverMutex.withLock { - val grpcClient = GrpcClient("localhost", PORT) { - usePlaintext() + val grpcClient = GrpcClient("localhost", PORT, credentials = clientCreds) { + if (overrideAuthority != null) overrideAuthority(overrideAuthority) + if (clientCreds == null) { + usePlaintext() + } } - val grpcServer = GrpcServer(PORT, builder = { - registerServices() - }) + val grpcServer = GrpcServer( + PORT, + credentials = serverCreds, + builder = { + registerServices() + }) grpcServer.start() - test(grpcClient) - grpcServer.shutdown() - grpcServer.awaitTermination() - grpcClient.shutdown() - grpcClient.awaitTermination() + try { + test(grpcClient) + } finally { + grpcServer.shutdown() + grpcServer.awaitTermination() + grpcClient.shutdown() + grpcClient.awaitTermination() + } } } diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcTlsTest.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcTlsTest.kt new file mode 100644 index 000000000..196629f3f --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcTlsTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.test.proto + +import hello.HelloRequest +import hello.HelloService +import hello.invoke +import kotlinx.coroutines.test.runTest +import kotlinx.rpc.RpcServer +import kotlinx.rpc.grpc.GrpcClient +import kotlinx.rpc.grpc.StatusCode +import kotlinx.rpc.grpc.TlsClientAuth +import kotlinx.rpc.grpc.TlsClientCredentials +import kotlinx.rpc.grpc.TlsServerCredentials +import kotlinx.rpc.grpc.test.CA_PEM +import kotlinx.rpc.grpc.test.CLIENT_CERT_PEM +import kotlinx.rpc.grpc.test.CLIENT_KEY_PEM +import kotlinx.rpc.grpc.test.EchoRequest +import kotlinx.rpc.grpc.test.EchoService +import kotlinx.rpc.grpc.test.EchoServiceImpl +import kotlinx.rpc.grpc.test.SERVER_CERT_PEM +import kotlinx.rpc.grpc.test.SERVER_KEY_PEM +import kotlinx.rpc.grpc.test.assertGrpcFailure +import kotlinx.rpc.grpc.test.invoke +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.test.Test +import kotlin.test.assertEquals + +class GrpcTlsTest : GrpcProtoTest() { + + override fun RpcServer.registerServices() { + registerService { EchoServiceImpl() } + } + + @Test + fun `test client side TLS with default credentials - should succeed`() = runTest { + // uses default client TLS credentials + // TODO: Use a test server controlled by us (KRPC-215) + val grpcClient = GrpcClient("grpcb.in", 9001) + val service = grpcClient.withService() + val request = HelloRequest { + greeting = "world" + } + val result = service.SayHello(request) + + assertEquals("hello world", result.reply) + + grpcClient.shutdown() + grpcClient.awaitTermination() + } + + @Test + fun `test TLS with valid certificates - should succeed`() { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) + val clientTls = TlsClientCredentials { trustManager(SERVER_CERT_PEM) } + + runGrpcTest(serverTls, clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + + @Test + fun `test mTLS with valid certificates - should succeed`() = runTest { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) { + trustManager(CA_PEM) + clientAuth(TlsClientAuth.REQUIRE) + } + val clientTls = TlsClientCredentials { + keyManager(CLIENT_CERT_PEM, CLIENT_KEY_PEM) + trustManager(CA_PEM) + } + + runGrpcTest(serverTls, clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + + @Test + fun `test mTLS with clientAuth optional - should succeed`() = runTest { + // the server uses a trustManager that does not know about the client certificate, + // so the client can authentication cannot be verified. + // but as the clientAuth is optional, the connection will succeed. + val caCertWithoutClient = SERVER_CERT_PEM + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) { + trustManager(caCertWithoutClient) + // clientAuth is optional, so a client without a certificate can connect + clientAuth(TlsClientAuth.OPTIONAL) + } + val clientTls = TlsClientCredentials { + keyManager(CLIENT_CERT_PEM, CLIENT_KEY_PEM) + trustManager(CA_PEM) + } + + runGrpcTest(serverTls, clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + + @Test + fun `test mTLS with clientAuth required - should fail`() = runTest { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) { + trustManager(CA_PEM) + // client must authenticate + clientAuth(TlsClientAuth.REQUIRE) + } + // client does NOT provide keyManager, only trusts CA + val clientTls = TlsClientCredentials { + trustManager(CA_PEM) + } + + assertGrpcFailure(StatusCode.UNAVAILABLE) { + runGrpcTest(serverTls, clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + } + + @Test + fun `test TLS with no client trustManager - should fail`() = runTest { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) + // client credential doesn't contain a trustManager, so server authentication will fail + val clientTls = TlsClientCredentials {} + assertGrpcFailure(StatusCode.UNAVAILABLE) { + runGrpcTest(serverTls, clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + } + + @Test + fun `test TLS with invalid authority - should fail`() = runTest { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) + val clientTls = TlsClientCredentials { trustManager(CA_PEM) } + // the authority does not match the certificate + assertGrpcFailure(StatusCode.UNAVAILABLE) { + runGrpcTest(serverTls, clientTls, overrideAuthority = "invalid.host.name", test = ::defaultUnaryTest) + } + } + + @Test + fun `test TLS server with plaintext client - should fail`() = runTest { + val serverTls = TlsServerCredentials(SERVER_CERT_PEM, SERVER_KEY_PEM) + assertGrpcFailure(StatusCode.UNAVAILABLE) { + runGrpcTest(serverCreds = serverTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + } + + @Test + fun `test TLS client with plaintext server - should fail`() = runTest { + val clientTls = TlsClientCredentials { trustManager(CA_PEM) } + assertGrpcFailure(StatusCode.UNAVAILABLE) { + runGrpcTest(clientCreds = clientTls, overrideAuthority = "foo.test.google.fr", test = ::defaultUnaryTest) + } + } + +} + +private suspend fun defaultUnaryTest(client: GrpcClient) { + val service = client.withService() + val request = EchoRequest { message = "Echo" } + val response = service.UnaryEcho(request) + assertEquals("Echo", response.message) +} \ No newline at end of file diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/utils.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/utils.kt new file mode 100644 index 000000000..57282bb57 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/utils.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.test + +import kotlinx.rpc.grpc.StatusCode +import kotlinx.rpc.grpc.StatusException +import kotlinx.rpc.grpc.statusCode +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +fun assertGrpcFailure(statusCode: StatusCode, message: String? = null, block: () -> Unit) { + val exc = assertFailsWith(message) { block() } + assertEquals(statusCode, exc.getStatus().statusCode) + if (message != null) { + assertContains(message, exc.getStatus().getDescription() ?: "") + } +} diff --git a/grpc/grpc-core/src/commonTest/proto/grpcb_in_hello.proto b/grpc/grpc-core/src/commonTest/proto/grpcb_in_hello.proto new file mode 100644 index 000000000..084fa2875 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/proto/grpcb_in_hello.proto @@ -0,0 +1,22 @@ +syntax = "proto2"; + +// grpc://grpcb.in:9000 (without TLS) +// grpc://grpcb.in:9001 (with TLS) + + +package hello; + +service HelloService { + rpc SayHello(HelloRequest) returns (HelloResponse); + rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); + rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); + rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); +} + +message HelloRequest { + optional string greeting = 1; +} + +message HelloResponse { + required string reply = 1; +} \ No newline at end of file diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt index 559007568..ebd658136 100644 --- a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt @@ -24,11 +24,20 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { return build().toKotlin() } -internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> { +internal actual fun ManagedChannelBuilder( + hostname: String, + port: Int, + credentials: ClientCredentials?, +): ManagedChannelBuilder<*> { + if (credentials != null) return io.grpc.Grpc.newChannelBuilderForAddress(hostname, port, credentials) return io.grpc.ManagedChannelBuilder.forAddress(hostname, port) } -internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { +internal actual fun ManagedChannelBuilder( + target: String, + credentials: ClientCredentials?, +): ManagedChannelBuilder<*> { + if (credentials != null) return io.grpc.Grpc.newChannelBuilder(target, credentials) return io.grpc.ManagedChannelBuilder.forTarget(target) } diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt index cf6fce68a..ba84ffecd 100644 --- a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt @@ -14,7 +14,8 @@ import kotlin.time.Duration */ public actual typealias ServerBuilder = io.grpc.ServerBuilder -internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { +internal actual fun ServerBuilder(port: Int, credentials: ServerCredentials?): ServerBuilder<*> { + if (credentials != null) return io.grpc.Grpc.newServerBuilderForPort(port, credentials) return io.grpc.ServerBuilder.forPort(port) } diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/credentials.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/credentials.jvm.kt new file mode 100644 index 000000000..3ba8e4ce4 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/credentials.jvm.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual typealias ClientCredentials = io.grpc.ChannelCredentials + +public actual typealias ServerCredentials = io.grpc.ServerCredentials + +// we need a wrapper for InsecureChannelCredentials as our constructor would conflict with the private +// java constructor. +public actual typealias InsecureClientCredentials = io.grpc.InsecureChannelCredentials +public actual typealias InsecureServerCredentials = io.grpc.InsecureServerCredentials + +public actual typealias TlsClientCredentials = io.grpc.TlsChannelCredentials +public actual typealias TlsServerCredentials = io.grpc.TlsServerCredentials + +internal actual fun TlsClientCredentialsBuilder(): TlsClientCredentialsBuilder = JvmTlsCLientCredentialBuilder() +internal actual fun TlsServerCredentialsBuilder( + certChain: String, + privateKey: String, +): TlsServerCredentialsBuilder = JvmTlsServerCredentialBuilder(certChain, privateKey) + +internal actual fun TlsClientCredentialsBuilder.build(): ClientCredentials { + return (this as JvmTlsCLientCredentialBuilder).build() +} + +internal actual fun TlsServerCredentialsBuilder.build(): ServerCredentials { + return (this as JvmTlsServerCredentialBuilder).build() +} + +private class JvmTlsCLientCredentialBuilder : TlsClientCredentialsBuilder { + private var cb = TlsClientCredentials.newBuilder() + + + override fun trustManager(rootCertsPem: String): TlsClientCredentialsBuilder { + cb.trustManager(rootCertsPem.byteInputStream()) + return this + } + + override fun keyManager( + certChainPem: String, + privateKeyPem: String, + ): TlsClientCredentialsBuilder { + cb.keyManager(certChainPem.byteInputStream(), privateKeyPem.byteInputStream()) + return this + } + + fun build(): ClientCredentials { + return cb.build() + } +} + +private class JvmTlsServerCredentialBuilder(certChain: String, privateKey: String) : TlsServerCredentialsBuilder { + private var sb = TlsServerCredentials.newBuilder() + + init { + sb.keyManager(certChain.byteInputStream(), privateKey.byteInputStream()) + } + + override fun trustManager(rootCertsPem: String): TlsServerCredentialsBuilder { + sb.trustManager(rootCertsPem.byteInputStream()) + return this + } + + override fun clientAuth(clientAuth: TlsClientAuth): TlsServerCredentialsBuilder { + sb.clientAuth(clientAuth.toJava()) + return this + } + + + fun build(): ServerCredentials { + return sb.build() + } +} + + +private fun TlsClientAuth.toJava(): io.grpc.TlsServerCredentials.ClientAuth = when (this) { + TlsClientAuth.NONE -> io.grpc.TlsServerCredentials.ClientAuth.NONE + TlsClientAuth.OPTIONAL -> io.grpc.TlsServerCredentials.ClientAuth.OPTIONAL + TlsClientAuth.REQUIRE -> io.grpc.TlsServerCredentials.ClientAuth.REQUIRE +} + + diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt index 766ffbb2b..85a5439e6 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt @@ -7,8 +7,6 @@ package kotlinx.rpc.grpc import kotlinx.rpc.grpc.internal.GrpcChannel -import kotlinx.rpc.grpc.internal.GrpcChannelCredentials -import kotlinx.rpc.grpc.internal.GrpcInsecureChannelCredentials import kotlinx.rpc.grpc.internal.NativeManagedChannel import kotlinx.rpc.grpc.internal.internalError @@ -24,22 +22,32 @@ public actual abstract class ManagedChannelBuilder> public actual open fun usePlaintext(): T { error("Builder does not support usePlaintext()") } + + public actual abstract fun overrideAuthority(authority: String): T } internal class NativeManagedChannelBuilder( private val target: String, + private var credentials: Lazy, ) : ManagedChannelBuilder() { - private var credentials: GrpcChannelCredentials? = null + + private var authority: String? = null override fun usePlaintext(): NativeManagedChannelBuilder { - credentials = GrpcInsecureChannelCredentials() + credentials = lazy { InsecureChannelCredentials() } + return this + } + + override fun overrideAuthority(authority: String): NativeManagedChannelBuilder { + this.authority = authority return this } fun buildChannel(): NativeManagedChannel { return NativeManagedChannel( target, - credentials = credentials ?: error("No credentials set"), + authority = authority, + credentials = credentials.value, ) } @@ -50,12 +58,18 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { return buildChannel() } -internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> { - return NativeManagedChannelBuilder(target = "$hostname:$port") +internal actual fun ManagedChannelBuilder( + hostname: String, + port: Int, + credentials: ClientCredentials?, +): ManagedChannelBuilder<*> { + val credentials = if (credentials == null) lazy { TlsClientCredentials() } else lazy { credentials } + return NativeManagedChannelBuilder(target = "$hostname:$port", credentials) } -internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { - return NativeManagedChannelBuilder(target) +internal actual fun ManagedChannelBuilder(target: String, credentials: ClientCredentials?): ManagedChannelBuilder<*> { + val credentials = if (credentials == null) lazy { TlsClientCredentials() } else lazy { credentials } + return NativeManagedChannelBuilder(target, credentials) } diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt index 5e8f53cba..476214555 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt @@ -4,7 +4,7 @@ package kotlinx.rpc.grpc -import kotlinx.rpc.grpc.internal.GrpcInsecureServerCredentials + import kotlinx.rpc.grpc.internal.NativeServer import kotlinx.rpc.grpc.internal.ServerMethodDefinition @@ -21,10 +21,8 @@ public actual abstract class ServerBuilder> { private class NativeServerBuilder( val port: Int, + val credentials: ServerCredentials, ) : ServerBuilder() { - - // TODO: Add actual credentials - private val credentials = GrpcInsecureServerCredentials() private val services = mutableListOf() private var fallbackRegistry: HandlerRegistry = DefaultFallbackRegistry @@ -44,8 +42,8 @@ private class NativeServerBuilder( } -internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { - return NativeServerBuilder(port) +internal actual fun ServerBuilder(port: Int, credentials: ServerCredentials?): ServerBuilder<*> { + return NativeServerBuilder(port, credentials ?: InsecureServerCredentials()) } internal actual fun Server(builder: ServerBuilder<*>): Server { @@ -64,4 +62,4 @@ private object DefaultFallbackRegistry : HandlerRegistry() { return null } -} +} \ No newline at end of file diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/credentials.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/credentials.native.kt new file mode 100644 index 000000000..b10e7f65b --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/credentials.native.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) + +package kotlinx.rpc.grpc + +import cnames.structs.grpc_channel_credentials +import cnames.structs.grpc_server_credentials +import cnames.structs.grpc_tls_credentials_options +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import libkgrpc.grpc_channel_credentials_release +import libkgrpc.grpc_insecure_credentials_create +import libkgrpc.grpc_insecure_server_credentials_create +import libkgrpc.grpc_server_credentials_release +import libkgrpc.grpc_ssl_client_certificate_request_type +import libkgrpc.grpc_tls_certificate_provider_release +import libkgrpc.grpc_tls_certificate_provider_static_data_create +import libkgrpc.grpc_tls_credentials_create +import libkgrpc.grpc_tls_credentials_options_create +import libkgrpc.grpc_tls_credentials_options_destroy +import libkgrpc.grpc_tls_credentials_options_set_cert_request_type +import libkgrpc.grpc_tls_credentials_options_set_certificate_provider +import libkgrpc.grpc_tls_credentials_options_watch_identity_key_cert_pairs +import libkgrpc.grpc_tls_credentials_options_watch_root_certs +import libkgrpc.grpc_tls_identity_pairs_add_pair +import libkgrpc.grpc_tls_identity_pairs_create +import libkgrpc.grpc_tls_server_credentials_create +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.createCleaner + +public actual abstract class ClientCredentials internal constructor( + internal val raw: CPointer, +) { + @Suppress("unused") + internal val rawCleaner = createCleaner(raw) { + grpc_channel_credentials_release(it) + } +} + +public actual abstract class ServerCredentials internal constructor( + internal val raw: CPointer, +) { + @Suppress("unused") + internal val rawCleaner = createCleaner(raw) { + grpc_server_credentials_release(it) + } +} + +public actual class InsecureClientCredentials internal constructor( + raw: CPointer, +) : ClientCredentials(raw) + +public actual class InsecureServerCredentials internal constructor( + raw: CPointer, +) : ServerCredentials(raw) + +public actual class TlsClientCredentials internal constructor( + raw: CPointer, +) : ClientCredentials(raw) + +public actual class TlsServerCredentials( + raw: CPointer, +) : ServerCredentials(raw) + + +public fun InsecureChannelCredentials(): ClientCredentials { + return InsecureClientCredentials( + grpc_insecure_credentials_create() ?: error("grpc_insecure_credentials_create() returned null") + ) +} + +public fun InsecureServerCredentials(): ServerCredentials { + return InsecureServerCredentials( + grpc_insecure_server_credentials_create() ?: error("grpc_insecure_server_credentials_create() returned null") + ) +} + +internal actual fun TlsClientCredentialsBuilder(): TlsClientCredentialsBuilder = NativeTlsClientCredentialsBuilder() +internal actual fun TlsServerCredentialsBuilder( + certChain: String, + privateKey: String, +): TlsServerCredentialsBuilder = NativeTlsServerCredentialsBuilder(certChain, privateKey) + +internal actual fun TlsClientCredentialsBuilder.build(): ClientCredentials { + return (this as NativeTlsClientCredentialsBuilder).build() +} + +internal actual fun TlsServerCredentialsBuilder.build(): ServerCredentials { + return (this as NativeTlsServerCredentialsBuilder).build() +} + +private class NativeTlsClientCredentialsBuilder : TlsClientCredentialsBuilder { + var optionsBuilder = TlsCredentialsOptionsBuilder() + + override fun trustManager(rootCertsPem: String): TlsClientCredentialsBuilder { + optionsBuilder.trustManager(rootCertsPem) + return this + } + + override fun keyManager( + certChainPem: String, + privateKeyPem: String, + ): TlsClientCredentialsBuilder { + optionsBuilder.keyManager(certChainPem, privateKeyPem) + return this + } + + fun build(): ClientCredentials { + val opts = optionsBuilder.build() + val creds = grpc_tls_credentials_create(opts) + ?: run { + grpc_tls_credentials_options_destroy(opts); + error("TLS channel credential creation failed") + } + return TlsClientCredentials(creds) + } +} + +private class NativeTlsServerCredentialsBuilder(certChain: String, privateKey: String) : TlsServerCredentialsBuilder { + var optionsBuilder = TlsCredentialsOptionsBuilder() + + init { + optionsBuilder.keyManager(certChain, privateKey) + } + + override fun trustManager(rootCertsPem: String): TlsServerCredentialsBuilder { + optionsBuilder.trustManager(rootCertsPem) + return this + } + + override fun clientAuth(clientAuth: TlsClientAuth): TlsServerCredentialsBuilder { + optionsBuilder.clientAuth(clientAuth) + return this + } + + fun build(): TlsServerCredentials { + val opts = optionsBuilder.build() + val creds = grpc_tls_server_credentials_create(opts) + ?: run { + grpc_tls_credentials_options_destroy(opts); + error("TLS server credential creation failed") + } + return TlsServerCredentials(creds) + } +} + + +private class TlsCredentialsOptionsBuilder { + private var roots: String? = null + private var cert: String? = null + private var key: String? = null + + private var clientAuth: TlsClientAuth? = null + + fun trustManager(rootCertsPem: String) { + roots = rootCertsPem + } + + fun keyManager(certChainPem: String, privateKeyPem: String) = apply { + cert = certChainPem; key = privateKeyPem + } + + fun clientAuth(clientAuth: TlsClientAuth) { + this.clientAuth = clientAuth + } + + fun build(): CPointer { + val opts = grpc_tls_credentials_options_create() ?: error("alloc opts failed") + + val pairs = if (cert != null && key != null) { + val p = grpc_tls_identity_pairs_create() ?: error("pairs alloc failed") + grpc_tls_identity_pairs_add_pair(p, key, cert); + p + } else null + + if (roots != null || pairs != null) { + val provider = grpc_tls_certificate_provider_static_data_create( + roots, pairs + ) ?: error("provider alloc failed") + grpc_tls_credentials_options_set_certificate_provider(opts, provider) + grpc_tls_certificate_provider_release(provider) + } + + + if (pairs != null) grpc_tls_credentials_options_watch_identity_key_cert_pairs(opts) + if (roots != null) grpc_tls_credentials_options_watch_root_certs(opts) + + val clientAuth = clientAuth + if (clientAuth != null) grpc_tls_credentials_options_set_cert_request_type(opts, clientAuth.toRaw()) + + return opts + } +} + +private fun TlsClientAuth.toRaw(): grpc_ssl_client_certificate_request_type = when (this) { + TlsClientAuth.NONE -> grpc_ssl_client_certificate_request_type.GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE + TlsClientAuth.OPTIONAL -> grpc_ssl_client_certificate_request_type.GRPC_SSL_REQUEST_CLIENT_CERTIFICATE_BUT_DONT_VERIFY + TlsClientAuth.REQUIRE -> grpc_ssl_client_certificate_request_type.GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY +} \ No newline at end of file diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.native.kt index 3a1e78d32..74a1d5ca8 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.native.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/GrpcChannel.native.kt @@ -13,5 +13,5 @@ public actual abstract class GrpcChannel { callOptions: GrpcCallOptions, ): ClientCall - public actual abstract fun authority(): String + public actual abstract fun authority(): String? } diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeManagedChannel.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeManagedChannel.kt index 7d465f4d0..ba0a7d9f0 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeManagedChannel.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeManagedChannel.kt @@ -7,48 +7,36 @@ package kotlinx.rpc.grpc.internal import cnames.structs.grpc_channel -import cnames.structs.grpc_channel_credentials import kotlinx.atomicfu.atomic import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.cstr +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.rpc.grpc.ClientCredentials import kotlinx.rpc.grpc.ManagedChannel import kotlinx.rpc.grpc.ManagedChannelPlatform import libkgrpc.GPR_CLOCK_REALTIME import libkgrpc.GRPC_PROPAGATE_DEFAULTS import libkgrpc.gpr_inf_future +import libkgrpc.grpc_arg +import libkgrpc.grpc_arg_type +import libkgrpc.grpc_channel_args import libkgrpc.grpc_channel_create import libkgrpc.grpc_channel_create_call -import libkgrpc.grpc_channel_credentials_release import libkgrpc.grpc_channel_destroy -import libkgrpc.grpc_insecure_credentials_create import libkgrpc.grpc_slice_unref import kotlin.coroutines.cancellation.CancellationException import kotlin.experimental.ExperimentalNativeApi import kotlin.native.ref.createCleaner import kotlin.time.Duration -/** - * Wrapper for [cnames.structs.grpc_channel_credentials]. - */ -internal sealed class GrpcChannelCredentials( - internal val raw: CPointer, -) { - val rawCleaner = createCleaner(raw) { - grpc_channel_credentials_release(it) - } -} - -/** - * Insecure credentials. - */ -internal class GrpcInsecureChannelCredentials() : - GrpcChannelCredentials(grpc_insecure_credentials_create() ?: error("Failed to create channel credentials")) - /** * Native implementation of [ManagedChannel]. @@ -58,8 +46,9 @@ internal class GrpcInsecureChannelCredentials() : */ internal class NativeManagedChannel( target: String, + val authority: String?, // we must store them, otherwise the credentials are getting released - credentials: GrpcChannelCredentials, + credentials: ClientCredentials, ) : ManagedChannel, ManagedChannelPlatform() { // a reference to make sure the grpc_init() was called. (it is released after shutdown) @@ -73,8 +62,25 @@ internal class NativeManagedChannel( // the channel's completion queue, handling all request operations private val cq = CompletionQueue() - internal val raw: CPointer = grpc_channel_create(target, credentials.raw, null) - ?: error("Failed to create channel") + internal val raw: CPointer = memScoped { + val args = authority?.let { + // the C Core API doesn't have a way to override the authority (used for TLS SNI) as it + // is available in the Java gRPC implementation. + // instead, it can be done by setting the "grpc.ssl_target_name_override" argument. + val authorityOverride = alloc { + type = grpc_arg_type.GRPC_ARG_STRING + key = "grpc.ssl_target_name_override".cstr.ptr + value.string = authority.cstr.ptr + } + + alloc { + num_args = 1u + args = authorityOverride.ptr + } + } + grpc_channel_create(target, credentials.raw, args?.ptr) + ?: error("Failed to create channel") + } @Suppress("unused") private val rawCleaner = createCleaner(raw) { @@ -141,6 +147,7 @@ internal class NativeManagedChannel( // to construct a valid HTTP/2 path, we must prepend the name with a slash. // the user does not do this to align it with the java implementation. val methodNameSlice = "/$methodFullName".toGrpcSlice() + val rawCall = grpc_channel_create_call( channel = raw, parent_call = null, @@ -159,8 +166,8 @@ internal class NativeManagedChannel( ) } - override fun authority(): String { - TODO("Not yet implemented") + override fun authority(): String? { + return authority } } diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeServer.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeServer.kt index 39ede8291..a5b26084f 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeServer.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeServer.kt @@ -7,7 +7,6 @@ package kotlinx.rpc.grpc.internal import cnames.structs.grpc_server -import cnames.structs.grpc_server_credentials import kotlinx.atomicfu.atomic import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.CPointer @@ -20,12 +19,11 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.withTimeoutOrNull import kotlinx.rpc.grpc.HandlerRegistry import kotlinx.rpc.grpc.Server +import kotlinx.rpc.grpc.ServerCredentials import kotlinx.rpc.grpc.ServerServiceDefinition -import libkgrpc.grpc_insecure_server_credentials_create import libkgrpc.grpc_server_add_http2_port import libkgrpc.grpc_server_cancel_all_calls import libkgrpc.grpc_server_create -import libkgrpc.grpc_server_credentials_release import libkgrpc.grpc_server_destroy import libkgrpc.grpc_server_register_completion_queue import libkgrpc.grpc_server_register_method @@ -37,29 +35,13 @@ import libkgrpc.kgrpc_registered_call_allocation import libkgrpc.kgrpc_server_set_batch_method_allocator import libkgrpc.kgrpc_server_set_register_method_allocator import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner import kotlin.time.Duration -/** - * Wrapper for [grpc_server_credentials]. - */ -internal sealed class GrpcServerCredentials( - internal val raw: CPointer, -) { - val rawCleaner = createCleaner(raw) { - grpc_server_credentials_release(it) - } -} - -internal class GrpcInsecureServerCredentials : - GrpcServerCredentials(grpc_insecure_server_credentials_create() ?: error("Failed to create server credentials")) - - internal class NativeServer( override val port: Int, // we must reference them, otherwise the credentials are getting garbage collected @Suppress("Redundant") - private val credentials: GrpcServerCredentials, + private val credentials: ServerCredentials, services: List, val fallbackRegistry: HandlerRegistry, ) : Server { diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/serverCallTags.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/serverCallTags.kt index 58f58d21d..f22bd1f0e 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/serverCallTags.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/serverCallTags.kt @@ -120,9 +120,6 @@ internal class LookupServerCallTag( return } - // TODO: check authority - // val host = rawDetails.host.toByteArray().decodeToString() - var method = rawDetails.method.toByteArray().decodeToString() // gRPC preserves the '/' character in the method name, diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/utils.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/utils.kt index 83b807652..28f7d8136 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/utils.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/utils.kt @@ -43,7 +43,7 @@ import libkgrpc.grpc_slice_unref import libkgrpc.grpc_status_code import platform.posix.memcpy -internal fun internalError(message: String) { +internal fun internalError(message: String): Nothing { error("Unexpected internal error: $message. Please, report the issue here: https://github.com/Kotlin/kotlinx-rpc/issues/new?template=bug_report.md") } diff --git a/protobuf/protobuf-core/src/commonMain/kotlin/kotlinx/rpc/protobuf/internal/ProtobufException.kt b/protobuf/protobuf-core/src/commonMain/kotlin/kotlinx/rpc/protobuf/internal/ProtobufException.kt index 39c27a42f..24ca908a8 100644 --- a/protobuf/protobuf-core/src/commonMain/kotlin/kotlinx/rpc/protobuf/internal/ProtobufException.kt +++ b/protobuf/protobuf-core/src/commonMain/kotlin/kotlinx/rpc/protobuf/internal/ProtobufException.kt @@ -13,7 +13,7 @@ public class ProtobufDecodingException : ProtobufException { public constructor(message: String, cause: Throwable? = null) : super(message, cause) public companion object Companion { - internal fun missingRequiredField(messageName: String, fieldName: String) = + public fun missingRequiredField(messageName: String, fieldName: String): ProtobufDecodingException = ProtobufDecodingException("Message '$messageName' is missing a required field: $fieldName") internal fun negativeSize() = ProtobufDecodingException(