Created
February 1, 2022 22:30
-
-
Save HoffiMuc/bb183db2e9d54cf1d94a71a3705117f4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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