diff --git a/harness-http-server/src/main/scala/harness/http/server/Server.scala b/harness-http-server/src/main/scala/harness/http/server/Server.scala index 731acee4..a66b1a1a 100644 --- a/harness-http-server/src/main/scala/harness/http/server/Server.scala +++ b/harness-http-server/src/main/scala/harness/http/server/Server.scala @@ -6,7 +6,10 @@ import harness.core.* import harness.zio.* import java.io.{ByteArrayInputStream, FileInputStream, InputStream, OutputStream} import java.net.InetSocketAddress -import java.security.{KeyStore, SecureRandom} +import java.security.{KeyFactory, KeyStore, SecureRandom} +import java.security.cert.CertificateFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 import javax.net.ssl.* import zio.* @@ -47,6 +50,7 @@ object Server { ZIO.hAttempt(HttpServer.create(inet, 0)) } + // TODO (KR) : fix bug where only first request fails to load private def configureSSL(server: HttpsServer, sslConfig: ServerConfig.SslConfig): HRIO[FileSystem & Logger, Unit] = { def wrapUnsafe[A](hint: String)(thunk: => A): HTask[A] = ZIO.hAttempt { thunk }.mapError(HError.InternalDefect(s"Error during SSL configuration: $hint", _)) @@ -58,27 +62,41 @@ object Server { case ServerConfig.SslConfig.RefType.File => Path(ref).flatMap(_.inputStream) } + val keyStorePassword = sslConfig.certificatePassword.map(_.toCharArray).orNull + ZIO.scoped { for { - // Load keystore - keystore <- wrapUnsafe("KeyStore.getInstance") { KeyStore.getInstance("JKS") } - keystoreInputStream <- getInputStream(sslConfig.keystoreRefType, sslConfig.keystoreRef) - _ <- wrapUnsafe("keystore.load") { keystore.load(keystoreInputStream, sslConfig.keystorePassword.toCharArray) } + // Load certificate chain + certificateStream <- getInputStream(sslConfig.certificateRefType, sslConfig.certificateRef) + certificate <- wrapUnsafe("CertificateFactory.getInstance") { CertificateFactory.getInstance("X.509").generateCertificate(certificateStream) } + + // Load certificate into keystore + keyStore <- wrapUnsafe("KeyStore.getInstance") { KeyStore.getInstance("PKCS12") } + _ <- wrapUnsafe("keystore.load") { keyStore.load(null, keyStorePassword) } + _ <- wrapUnsafe("keyStore.setCertificateEntry") { keyStore.setCertificateEntry("cert", certificate) } - // Load truststore - trustStore <- wrapUnsafe("KeyStore.getInstance") { KeyStore.getInstance("JKS") } - trustStoreStream <- getInputStream(sslConfig.truststoreRefType, sslConfig.truststoreRef) - _ <- wrapUnsafe("trustStore.load") { trustStore.load(trustStoreStream, sslConfig.truststorePassword.toCharArray) } + // Load private key into keystore + privateKeyStream <- getInputStream(sslConfig.privateKeyRefType, sslConfig.privateKeyRef) + privateKeyBytes <- wrapUnsafe("privateKeyStream.readAllBytes") { privateKeyStream.readAllBytes() } + privateKeyPEM = new String(privateKeyBytes) + privateKeyPEMContent = + privateKeyPEM + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", "") + decodedKey <- wrapUnsafe("Base64.getDecoder.decode") { Base64.getDecoder.decode(privateKeyPEMContent) } + keyFactory <- wrapUnsafe("KeyFactory.getInstance") { KeyFactory.getInstance("RSA") } + privateKeySpec <- wrapUnsafe("new PKCS8EncodedKeySpec") { new PKCS8EncodedKeySpec(decodedKey) } + privateKey <- wrapUnsafe("keyFactory.generatePrivate") { keyFactory.generatePrivate(privateKeySpec) } + _ <- wrapUnsafe("keyStore.setKeyEntry") { keyStore.setKeyEntry("key", privateKey, keyStorePassword, Array(certificate)) } // Initialize KeyManagerFactory and TrustManagerFactory keyManagerFactory <- wrapUnsafe("KeyManagerFactory.getInstance") { KeyManagerFactory.getInstance("SunX509") } - _ <- wrapUnsafe("keyManagerFactory.init") { keyManagerFactory.init(keystore, sslConfig.keystorePassword.toCharArray) } - trustManagerFactory <- wrapUnsafe("TrustManagerFactory.getInstance") { TrustManagerFactory.getInstance("SunX509") } - _ <- wrapUnsafe("trustManagerFactory.init") { trustManagerFactory.init(keystore) } + _ <- wrapUnsafe("keyManagerFactory.init") { keyManagerFactory.init(keyStore, keyStorePassword) } // Initialize SSLContext sslContext <- wrapUnsafe("SSLContext.getInstance") { SSLContext.getInstance("TLS") } - _ <- wrapUnsafe("sslContext.init") { sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom()) } + _ <- wrapUnsafe("sslContext.init") { sslContext.init(keyManagerFactory.getKeyManagers, null, new SecureRandom()) } _ <- wrapUnsafe("server.setHttpsConfigurator") { server.setHttpsConfigurator( diff --git a/harness-http-server/src/main/scala/harness/http/server/ServerConfig.scala b/harness-http-server/src/main/scala/harness/http/server/ServerConfig.scala index 3400d181..e9e70fb4 100644 --- a/harness-http-server/src/main/scala/harness/http/server/ServerConfig.scala +++ b/harness-http-server/src/main/scala/harness/http/server/ServerConfig.scala @@ -15,14 +15,13 @@ final case class ServerConfig( object ServerConfig { final case class SslConfig( - // keystore - keystoreRef: String, - keystorePassword: String, - keystoreRefType: SslConfig.RefType, - // truststore - truststoreRef: String, - truststorePassword: String, - truststoreRefType: SslConfig.RefType, + // certificate + certificateRef: String, + certificateRefType: SslConfig.RefType, + certificatePassword: Option[String], + // private key + privateKeyRef: String, + privateKeyRefType: SslConfig.RefType, ) object SslConfig { diff --git a/harness-http-server/src/test/scala/harness/http/server/GenSslKeys.scala b/harness-http-server/src/test/scala/harness/http/server/GenSslKeys.scala deleted file mode 100644 index 1bd43102..00000000 --- a/harness-http-server/src/test/scala/harness/http/server/GenSslKeys.scala +++ /dev/null @@ -1,125 +0,0 @@ -package harness.http.server - -import harness.cli.* -import harness.http.server.ServerConfig.SslConfig -import harness.zio.* -import java.util.UUID -import zio.json.* - -object GenSslKeys extends ExecutableApp { - - final case class Cfg( - alias: String, - jarResDir: String, - keyStorePass: Option[String], - trustStorePass: Option[String], - resourcePrefix: String, - ) - object Cfg { - - val parser: Parser[Cfg] = { - Parser.value[String](LongName.unsafe("alias")) && - Parser.value[String](LongName.unsafe("jar-res-dir")) && - Parser.value[String](LongName.unsafe("key-store-pass")).optional && - Parser.value[String](LongName.unsafe("trust-store-pass")).optional && - Parser.value[String](LongName.unsafe("resource-prefix")).default("ssl") - }.map { Cfg.apply } - - } - - override val executable: Executable = - Executable - .withParser(Cfg.parser) - .withEffect { cfg => - for { - _ <- Logger.log.info("Running key generation") - - baseKeystore = "keystore.jks" - baseTruststore = "truststore.jks" - resourcePrefix = if (cfg.resourcePrefix.isEmpty) "" else s"${cfg.resourcePrefix}/" - keystoreRef = s"$resourcePrefix$baseKeystore" - truststoreRef = s"$resourcePrefix$baseTruststore" - keystoreInRes = s"${cfg.jarResDir}/$keystoreRef" - truststoreInRes = s"${cfg.jarResDir}/$truststoreRef" - certFile = "server.crt" - validity = "365" - - keystorePass = cfg.keyStorePass.getOrElse(UUID.randomUUID.toString) - truststorePass = cfg.trustStorePass.getOrElse(UUID.randomUUID.toString) - - makeParentDirIfDNE = - (path: String) => - for { - file <- Path(path) - parentFile <- file.absolutePath.parent - _ <- parentFile.mkdirs.unlessZIO(parentFile.exists) - } yield () - - _ <- Logger.log.debug("step 0") - _ <- makeParentDirIfDNE(keystoreInRes) - _ <- makeParentDirIfDNE(truststoreInRes) - - _ <- Logger.log.debug("step 1") - _ <- Sys.execute0( - "keytool", - "-genkeypair", - "-keyalg", - "RSA", - "-keysize", - "2048", - "-alias", - cfg.alias, - "-keystore", - baseKeystore, - "-storepass", - keystorePass, - "-validity", - validity, - ) - - _ <- Logger.log.debug("step 2") - _ <- Sys.execute0( - "keytool", - "-export", - "-alias", - cfg.alias, - "-keystore", - baseKeystore, - "-storepass", - keystorePass, - "-file", - certFile, - ) - - _ <- Logger.log.debug("step 3") - _ <- Sys.execute0( - "keytool", - "-import", - "-alias", - cfg.alias, - "-file", - certFile, - "-keystore", - baseTruststore, - "-storepass", - truststorePass, - ) - - _ <- Logger.log.debug("step 4") - _ <- Sys.execute0("rm", certFile) - _ <- Sys.execute0("mv", baseKeystore, keystoreInRes) - _ <- Sys.execute0("mv", baseTruststore, truststoreInRes) - - cfg = SslConfig( - keystoreRef = keystoreRef, - keystorePassword = keystorePass, - keystoreRefType = SslConfig.RefType.Jar, - truststoreRef = truststoreRef, - truststorePassword = truststorePass, - truststoreRefType = SslConfig.RefType.Jar, - ) - _ <- Logger.log.important(s"Config:\n${cfg.toJson}") - } yield () - } - -} diff --git a/harness-web-app-template/api/src/main/resources/application.conf.json b/harness-web-app-template/api/src/main/resources/application.conf.json index f0b4f8cf..113aa061 100644 --- a/harness-web-app-template/api/src/main/resources/application.conf.json +++ b/harness-web-app-template/api/src/main/resources/application.conf.json @@ -35,12 +35,10 @@ "resDir": "harness-web-app-template/res", "useJarResource": false, "ssl": { - "keystoreRef": "ssl/keystore.jks", - "keystorePassword": "5d6ada9a-c953-43e6-9da0-db3607b702b8", - "keystoreRefType": "Jar", - "truststoreRef": "ssl/truststore.jks", - "truststorePassword": "a1995899-2b5d-4834-bdb8-f1ce49af3679", - "truststoreRefType": "Jar" + "certificateRef": "ssl/localhost.pem", + "certificateRefType": "Jar", + "privateKeyRef": "ssl/localhost-key.pem", + "privateKeyRefType": "Jar" } }, "email": {