nettyktormtlssslcontexttrustmanager

Ktor and Netty control trustManager or sslContext


I'm trying to add a custom logic to accept tls connection to the server using Ktor server and Netty, I didn't find a way to modify trustManger or sslContext.

    val environment = applicationEngineEnvironment {
        connector {
            port = 8080
        }
        sslConnector(
            keyStore = keystore,
            keyAlias = "selfsigned",
            keyStorePassword = { "".toCharArray() },
            privateKeyPassword = { "changeit".toCharArray() }
        ) {
            port = 8443
            trustStore = clientkeystore
        }
        module {
            routing {
                get("/204") {
                    call.response.status(HttpStatusCode.NoContent)
                }
            }
        }
    }

    embeddedServer(Netty, environment).start(true)

this is for mtls using keystore, but i want to control the logic of accepting the client certificates like ignore the validity of the client certificate, this is why I need to provide trustManager.

I have search the source code, it seem that sslContext and trustManger is setup in NettyChannelInitializer

public class NettyChannelInitializer(
    private val enginePipeline: EnginePipeline,
    private val environment: ApplicationEngineEnvironment,
    private val callEventGroup: EventExecutorGroup,
    private val engineContext: CoroutineContext,
    private val userContext: CoroutineContext,
    private val connector: EngineConnectorConfig,
    private val requestQueueLimit: Int,
    private val runningLimit: Int,
    private val responseWriteTimeout: Int,
    private val requestReadTimeout: Int,
    private val httpServerCodec: () -> HttpServerCodec,
    private val channelPipelineConfig: ChannelPipeline.() -> Unit
) : ChannelInitializer<SocketChannel>() {
    private var sslContext: SslContext? = null

    init {
        if (connector is EngineSSLConnectorConfig) {

            // It is better but netty-openssl doesn't support it
//              val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
//              kmf.init(ktorConnector.keyStore, password)
//              password.fill('\u0000')

            @Suppress("UNCHECKED_CAST")
            val chain1 = connector.keyStore.getCertificateChain(connector.keyAlias).toList() as List<X509Certificate>
            val certs = chain1.toList().toTypedArray()
            val password = connector.privateKeyPassword()
            val pk = connector.keyStore.getKey(connector.keyAlias, password) as PrivateKey
            password.fill('\u0000')

            sslContext = SslContextBuilder.forServer(pk, *certs).apply {
                if (alpnProvider != null) {
                    sslProvider(alpnProvider)
                    ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
                    applicationProtocolConfig(
                        ApplicationProtocolConfig(
                            ApplicationProtocolConfig.Protocol.ALPN,
                            ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
                            ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
                            ApplicationProtocolNames.HTTP_2,
                            ApplicationProtocolNames.HTTP_1_1
                        )
                    )
                }
                connector.trustManagerFactory()?.let { this.trustManager(it) }
            }
                .build()
        }
    }

is there a way to configure the logic using channelPipelineConfig or configureBootstrap? or I need to create custom netty engine?

I have tried to use childHandler but it never been called, maybe I have missed something.

    embeddedServer(Netty, port = 8443, configure = {
        configureBootstrap = {
            val certsChain = keystore2.getCertificateChain("selfsigned").toList() as List<X509Certificate>
            val certs = certsChain.toTypedArray()
            val password = "changeit".toCharArray()
            val privateKey = keystore2.getKey("selfsigned", password) as PrivateKey
            val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
                .also { it.init(keystore2) }



            childHandler(object : ChannelInitializer<SocketChannel>() {
                override fun initChannel(ch: SocketChannel) {
                    println("initChannel")
                    val sslContext = SslContextBuilder.forServer(privateKey, *certs)
                        .trustManager(trustManagerFactory)
                        .build()
                    val sslEngine = sslContext.newEngine(ch.alloc()).apply {
                        useClientMode = false
                        needClientAuth = false
                    }
                    ch.pipeline().addLast(SslHandler(sslEngine))
                }
            })
        }
    }, module = {
        routing {
            get("/204") {
                call.response.status(HttpStatusCode.NoContent)
            }
        }
    }).start(wait = true)

Solution

  • I have managed to add sslContext to Netty using channelPipelineConfig and SslHandler

        embeddedServer(Netty, port = 8443, configure = {
        val certsChain = keystore2.getCertificateChain("selfsigned").toList() as List<X509Certificate>
        val certs = certsChain.toTypedArray()
        val password = "changeit".toCharArray()
        val privateKey = keystore2.getKey("selfsigned", password) as PrivateKey
        val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
            .also { it.init(keystore2) }
    
    
        channelPipelineConfig = {
            val sslContext = SslContextBuilder.forServer(privateKey, *certs)
                .trustManager(trustManagerFactory)
                .build()
    
            val sslEngine = sslContext.newEngine(channel().alloc()).apply {
                useClientMode = false
                needClientAuth = true
            }
    
            addFirst("ssl", SslHandler(sslEngine))
        }
    }, module = {
        routing {
            get("/204") {
                call.response.status(HttpStatusCode.NoContent)
            }
        }
    }).start(wait = true)