Security

This page summarizes security considerations and configuration options when running genhttp-kt applications.

General Considerations

genhttp-kt is designed to be used as an application server behind a reverse proxy, and therefore does not provide built-in DoS protection (such as IP connection limits or Slowloris prevention). However, its engines are hardened against typical attack vectors such as request smuggling, header injection, or malformed chunk attacks: both parse requests with a port of Glyph11, the hardened parser of GenHTTP, and passes the ported acceptance tests of GenHTTP, which is explicitly tested for these attacks.

GenHTTP offers a Kestrel-based engine to run without a reverse proxy in front of it. Kestrel is specific to .NET, so genhttp-kt has its own engines only - put a reverse proxy in front of an application that faces the internet.

Encryption

To add an SSL/TLS secured endpoint, you can use the overload of the bind() method that accepts a certificate provider. CertificateProvider.from() creates one for a single certificate.

On the JVM, a certificate together with its private key is a KeyStore.PrivateKeyEntry, which you typically read from a PKCS#12 file (.pfx or .p12):

Kotlin
import org.genhttp.api.infrastructure.CertificateProvider
import org.genhttp.engine.internal.Host
import org.genhttp.modules.layouting.Layout

import java.io.File
import java.security.KeyStore

fun loadCertificate(file: File, password: CharArray): KeyStore.PrivateKeyEntry {
    val store = KeyStore.getInstance(file, password)

    val alias = store.aliases().toList().first(store::isKeyEntry)

    return store.getEntry(alias, KeyStore.PasswordProtection(password)) as KeyStore.PrivateKeyEntry
}

suspend fun main() {
    val password = System.getenv("CERTIFICATE_PASSWORD").orEmpty().toCharArray()

    val certificate = loadCertificate(File("./mycert.pfx"), password)

    Host.create()
        .handler(Layout.create())
        .bind(null, 80)
        .bind(null, 443, CertificateProvider.from(certificate))
        .run()
}

The given certificate will be used to encrypt all incoming requests with. Please note, that the client expects the server to use a certificate with a CN matching the requested host name. Certificates issued as PEM files (such as the ones of Let’s Encrypt) can be converted into a PKCS#12 file with openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out mycert.pfx.

Dynamic Certificate Selection

If you would like to dynamically select the certificate to be used to authenticate a connection (e.g. by the host name requested by the client), you can pass a custom CertificateProvider instance instead of a single certificate. The provider is called during the TLS handshake with the host name the client sent via SNI (null if it did not send one); returning null aborts the handshake.

Kotlin
import org.genhttp.api.infrastructure.CertificateProvider
import org.genhttp.engine.internal.Host
import org.genhttp.modules.layouting.Layout

import java.io.File
import java.security.KeyStore

class CustomCertificateProvider(
    private val host1: KeyStore.PrivateKeyEntry,
    private val host2: KeyStore.PrivateKeyEntry
) : CertificateProvider {

    override fun provide(host: String?): KeyStore.PrivateKeyEntry? = when (host) {
        "host1.com", "www.host1.com" -> host1
        "host2.com" -> host2
        else -> null
    }

}

suspend fun main() {
    val password = System.getenv("KEYSTORE_PASSWORD").orEmpty().toCharArray()

    // a key store holding the certificates of both hosts
    val store = KeyStore.getInstance(File("./certificates.p12"), password)

    fun entry(alias: String) = store.getEntry(alias, KeyStore.PasswordProtection(password)) as KeyStore.PrivateKeyEntry

    Host.create()
        .handler(Layout.create())
        .bind(null, 80)
        .bind(null, 443, CustomCertificateProvider(entry("host1"), entry("host2")))
        .run()
}

As CertificateProvider is a functional interface, a lambda works as well: CertificateProvider { host -> certificates[host] }.

Client Certificates

To enable client certificates (mTLS), you can pass a custom CertificateValidator to the bind() method:

Kotlin
import org.genhttp.api.infrastructure.CertificateProvider
import org.genhttp.api.infrastructure.CertificateValidator
import org.genhttp.api.infrastructure.SslPolicyError
import org.genhttp.api.infrastructure.X509RevocationMode
import org.genhttp.engine.internal.Host
import org.genhttp.modules.layouting.Layout

import java.io.File
import java.security.KeyStore
import java.security.cert.X509Certificate

class MyValidator : CertificateValidator {

    override val requireCertificate: Boolean
        get() = true

    override val revocationCheck: X509RevocationMode
        get() = X509RevocationMode.Offline

    override fun validate(certificate: X509Certificate?, chain: List<X509Certificate>?, policyErrors: Set<SslPolicyError>): Boolean {
        if (policyErrors.isNotEmpty()) return false

        return certificate?.issuerX500Principal?.name == "..."
    }

}

suspend fun main() {
    val password = System.getenv("KEYSTORE_PASSWORD").orEmpty().toCharArray()

    val store = KeyStore.getInstance(File("./certificates.p12"), password)
    val certificate = store.getEntry("server", KeyStore.PasswordProtection(password)) as KeyStore.PrivateKeyEntry

    Host.create()
        .handler(Layout.create())
        .bind(null, 80)
        .bind(null, 443, CertificateProvider.from(certificate), certificateValidator = MyValidator())
        .run()
}

The server will only allow clients that can present a certificate that passes the validate function of your implementation. policyErrors is empty if the certificate chain could be validated against the trust store of the JDK - a certificate issued by your own certificate authority comes with RemoteCertificateChainErrors, so check its issuer yourself to accept it. Both properties default to the values shown above, so you only need to override them to change them.

If requireCertificate is false, clients may connect without a certificate: validate is then called with null and RemoteCertificateNotAvailable, and the client is accepted if it returns true.

Client certificates can be combined with the client certificate authentication concern to authorize access on content level.