Skip to content

Instantly share code, notes, and snippets.

@HoffiMuc
Created February 1, 2022 22:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HoffiMuc/bb183db2e9d54cf1d94a71a3705117f4 to your computer and use it in GitHub Desktop.
Save HoffiMuc/bb183db2e9d54cf1d94a71a3705117f4 to your computer and use it in GitHub Desktop.
package minimal_ktor_tls
import com.hoffi.minimal_ktor_tls.plugins.configureHTTP
import com.hoffi.minimal_ktor_tls.plugins.configureRouting
import com.hoffi.minimal_ktor_tls.plugins.configureSerialization
import io.ktor.server.engine.*
import io.ktor.server.jetty.*
import nl.altindag.ssl.SSLFactory
import nl.altindag.ssl.exception.CertificateParseException
import nl.altindag.ssl.exception.GenericIOException
import nl.altindag.ssl.util.IOUtils
import nl.altindag.ssl.util.KeyStoreUtils
import nl.altindag.ssl.util.PemUtils
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.X509TrustedCertificateBlock
import org.eclipse.jetty.http.HttpVersion
import org.eclipse.jetty.server.*
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.slf4j.LoggerFactory
import java.io.*
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.*
import java.util.stream.Collectors
import javax.naming.ldap.LdapName
fun main(args: Array<String>) {
//StatusPrinter.print(LoggerFactory.getILoggerFactory() as LoggerContext) // print logback's internal status
App().doIt(args)
}
class App {
private val log = LoggerFactory.getLogger(App::class.java)
val mutualTLS = false // if true, then client has to provide a certificate trusted by a trustStore ca certificate
val startedFromJar = App::class.java.getResource("App.class")?.toString()?.startsWith("jar:") ?: false
val thePort = 8080
val theSslPort = 8081
val theHost = "test.hoffilocal.com" // first commonName (CN) in certificate
val theBindHost= if (startedFromJar) {
theHost
} else {
//"127.0.0.1" // if theHost name is not resolvable by DNS
theHost
}
val truststorePW = ""
val keystorePW = ""
val privateKeyPW = "" // keystorePW
val certsDir = "certs" // from resources (also in jar)
val serverCertFileBasename = theHost.replace('.', '_')
val serverCertFilepath = "/${certsDir}/${serverCertFileBasename}.cert"
val serverCertKeyFilepath = "/${certsDir}/${serverCertFileBasename}.key"
val rootCaFilepath = "/${certsDir}/rootca_rootcahoffi.ca"
// val keystoreFile = if(startedFromJar) {
// File("/certs/${serverCertFilename}.p12")
// } else {
// File("certs/certs/${serverCertFilename}.p12")
// }
// // openssl pkcs12 -export -passout pass:${keystorePW} -in "domain.cert" -inkey "domain.key" -certfile "intermediateAndRootCAchain.ca" -name "aliasName" -out "webserverKeystore.p12"
// val keystore: KeyStore = KeyStore.getInstance(keystoreFile, keystorePW.toCharArray())
@Suppress("UNUSED_PARAMETER")
fun doIt(args: Array<String>) {
val (theTrustStore, theKeyStore) = createTrustStoreAndKeyStore()
val server = embeddedServer(Jetty, applicationEngineEnvironment {
module {
configureRouting()
configureHTTP(theSslPort)
configureSerialization()
}
connector {
this.host = theBindHost // redirected to https
this.port = thePort // redirected to sslPort
}
// // without mTLS (m = mutual)
// sslConnector(theKeyStore,
// keyAlias = theHost, // alias name inside keystore: keytool -v -list -keystore certs/keystore.jks
// keyStorePassword = { keystorePW.toCharArray() },
// privateKeyPassword = { privateKeyPW.toCharArray() } // somehow this is the same as keystorePW if using openssl pkcs12 -export from above
// ) {
// this.host = theBindHost
// this.port = theSslPort
// keyStorePath = null // only used by tomcat engine
// }
}) {
configureServer = {
val factory = SslConnectionFactory(
SslContextFactory.Server().apply {
keyStore = theKeyStore
trustStore = theTrustStore
// setKeyManagerPassword(...)
// setKeyStorePassword(...)
needClientAuth = mutualTLS
},
HttpVersion.HTTP_1_1.asString()
)
val httpConfig = HttpConfiguration()
httpConfig.secureScheme = "https"
httpConfig.securePort = theSslPort
// SSL HTTP Configuration
val httpsConfig = HttpConfiguration(httpConfig)
httpsConfig.addCustomizer(SecureRequestCustomizer()) // so that servlets can see the encryption details
val connector = ServerConnector(
this,
factory,
HttpConnectionFactory(httpsConfig)
).apply {
host = theBindHost
port = theSslPort
}
addConnector(connector)
}
}
server.start(wait = true)
}
private fun createTrustStoreAndKeyStore(): Pair<KeyStore, KeyStore> {
// java TrustStore (represented by a java.security.KeyStore) contains the trusted CA certificates (signers)
// java's default cacerts are usually found under $JAVA_HOME/lib/security/cacerts
// + OS and system dependant locations of cacerts on the target host
// java KeyStore (also represented by a java.security.KeyStore) contains the identity(s) of this server
// that is the server(process's) certificate and the certificate's private key
// that certificate does not have to be signed by a trusted CA certificate inside the TrustStore (!?)
// but calling client's have to present a certificate that is signed by a trusted CA certificate inside the TrustStore
log.info("loading own server rootca from classpath: \"${rootCaFilepath}\"")
val ownRootCaChain =
PemUtils.loadCertificate(App::class.java.getResourceAsStream(rootCaFilepath))
assert(ownRootCaChain.size == 1)
// val trustManager = PemUtils.loadTrustMaterial("${rootCaFilepath}")
// var keyManager = PemUtils.loadIdentityMaterial("${serverCertFilepath}", "${serverCertKeyFilepath}");
val sslFactory = SSLFactory.builder()
// // this server's certificate (=identity)
// .withIdentityMaterial(keyManager)
// https://riptutorial.com/java/example/1420/loading-truststore-and-keystore-from-inputstream
//trusted (root)CA certificate(s)
.withSystemTrustMaterial()
.withDefaultTrustMaterial()
//.withTrustMaterial(trustManager)
// client has to present a client certificate that is trusted/signed by one of the trustMaterial certificates
.withNeedClientAuthentication(true)
.build()
//var sslContext = sslFactory.getSslContext();
//var sslSocketFactory = sslFactory.getSslSocketFactory();
val theTrustStore = KeyStore.getInstance(KeyStore.getDefaultType())
theTrustStore.load(null, null) // initialized = true
sslFactory.trustedCertificates.forEach { x509Certificate ->
val certAlias = generatedAlias(x509Certificate)
log.info("adding trusted CA certificate to trustStore with alias: '${certAlias}'")
theTrustStore.setCertificateEntry(certAlias, x509Certificate)
}
val certAlias = generatedAlias(ownRootCaChain.first())
log.info("adding own rootCA certificate to trustStore with alias: '${certAlias}'")
theTrustStore.setCertificateEntry(certAlias, ownRootCaChain.first())
// servers identity
val certificateChainContent =
getContent(App::class.java.getResourceAsStream(serverCertFilepath)!!)
val privateKeyContent =
getContent(App::class.java.getResourceAsStream(serverCertKeyFilepath)!!)
val privateKey = PemUtils.parsePrivateKey(privateKeyContent, "".toCharArray())
val certificatesChain = parseCertificate(certificateChainContent)
val theKeyStore = KeyStoreUtils.createKeyStore()
theKeyStore.setKeyEntry(
generatedAlias(certificatesChain[0]),
privateKey,
privateKeyPW.toCharArray(),
certificatesChain.toTypedArray()
)
return Pair(theTrustStore, theKeyStore)
}
private fun generatedAlias(x509Certificate: X509Certificate): String {
val principalDistinguishedName = x509Certificate.subjectX500Principal.name
// certAlias = X500Name.asX500Name(certificate.getSubjectX500Principal()).getCommonName();
val ldapDistinguishedName = LdapName(principalDistinguishedName)
return try {
ldapDistinguishedName.rdns.filter { it.type.equals("CN", ignoreCase = true) }.first().value.toString()
} catch (e: Exception) {
log.info("below cert has no commonName(CN)")
principalDistinguishedName
}
}
// from PemUtils private static List<X509Certificate> parseCertificate(String certContent) {
private val BOUNCY_CASTLE_PROVIDER: BouncyCastleProvider = BouncyCastleProvider()
private val CERTIFICATE_CONVERTER: JcaX509CertificateConverter = JcaX509CertificateConverter().setProvider(BOUNCY_CASTLE_PROVIDER)
private fun parseCertificate(certContent: String): List<X509Certificate> {
return try {
val stringReader: Reader = StringReader(certContent)
val pemParser = PEMParser(stringReader)
val certificates: MutableList<X509Certificate> = ArrayList()
var o: Any? = pemParser.readObject()
while (o != null) {
if (o is X509CertificateHolder) {
val certificate: X509Certificate = CERTIFICATE_CONVERTER.getCertificate(o)
certificates.add(certificate)
} else if (o is X509TrustedCertificateBlock) {
val certificate: X509Certificate = CERTIFICATE_CONVERTER.getCertificate(o.certificateHolder)
certificates.add(certificate)
}
o = pemParser.readObject()
}
pemParser.close()
stringReader.close()
if (certificates.isEmpty()) {
throw CertificateParseException("Received an unsupported certificate type")
}
certificates
} catch (e: IOException) {
throw CertificateParseException(e)
} catch (e: CertificateException) {
throw CertificateParseException(e)
}
}
fun getContent(inputStream: InputStream): String {
try {
InputStreamReader(Objects.requireNonNull(inputStream), StandardCharsets.UTF_8).use { inputStreamReader ->
BufferedReader(inputStreamReader).use { bufferedReader ->
return bufferedReader.lines()
.collect(Collectors.joining(System.lineSeparator()))
}
}
} catch (e: java.lang.Exception) {
throw GenericIOException(e)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment