-
Notifications
You must be signed in to change notification settings - Fork 594
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Cert loading and rotation utilities and docs (#4359)
- Loading branch information
1 parent
10d7599
commit 3f3417f
Showing
10 changed files
with
717 additions
and
13 deletions.
There are no files selected for viewing
This file contains 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
135 changes: 135 additions & 0 deletions
135
akka-http-tests/src/test/scala/akka/http/scaladsl/common/SSLContextFactorySpec.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright (C) 2009-2023 Lightbend Inc. <https://www.lightbend.com> | ||
*/ | ||
|
||
package akka.http.scaladsl.common | ||
|
||
import akka.http.scaladsl.ClientTransport | ||
import akka.http.scaladsl.ConnectionContext | ||
import akka.http.scaladsl.Http | ||
import akka.http.scaladsl.HttpsConnectionContext | ||
import akka.http.scaladsl.model.HttpRequest | ||
import akka.http.scaladsl.model.StatusCodes | ||
import akka.http.scaladsl.settings.ConnectionPoolSettings | ||
import akka.testkit.AkkaSpec | ||
import akka.testkit.TestProbe | ||
import com.typesafe.config.ConfigFactory | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
import java.io.InputStream | ||
import java.net.InetSocketAddress | ||
import java.nio.file.Path | ||
import java.security.KeyStore | ||
import java.security.SecureRandom | ||
import javax.net.ssl.SSLContext | ||
import javax.net.ssl.TrustManagerFactory | ||
import scala.concurrent.Future | ||
import scala.concurrent.duration.DurationInt | ||
|
||
class SSLContextFactorySpec extends AkkaSpec with Matchers { | ||
"The SSLContextUtils" should { | ||
"conveniently load pem files but use the system trust store" in { | ||
SSLContextFactory.createSSLContextFromPem(ConfigFactory.parseString( | ||
""" | ||
my-server { | ||
certificate = "akka-http-tests/src/test/resources/certs/example.com.crt" | ||
private-key = "akka-http-tests/src/test/resources/certs/example.com.key" | ||
trusted-ca-certificates = "system" | ||
} | ||
""").getConfig("my-server")) | ||
|
||
SSLContextFactory.createSSLContextFromPem( | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.crt"), | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.key"), | ||
Seq(Path.of("akka-http-tests/src/test/resources/certs/exampleca.crt")) | ||
) | ||
} | ||
|
||
"conveniently load pem files" in { | ||
SSLContextFactory.createSSLContextFromPem(ConfigFactory.parseString( | ||
""" | ||
my-server { | ||
certificate = "akka-http-tests/src/test/resources/certs/example.com.crt" | ||
private-key = "akka-http-tests/src/test/resources/certs/example.com.key" | ||
trusted-ca-certificates = ["akka-http-tests/src/test/resources/certs/exampleca.crt"] | ||
} | ||
""").getConfig("my-server")) | ||
|
||
SSLContextFactory.createSSLContextFromPem( | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.crt"), | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.key"), | ||
Seq(Path.of("akka-http-tests/src/test/resources/certs/exampleca.crt")) | ||
) | ||
} | ||
|
||
"create a refreshing context provider" in { | ||
val probe = TestProbe() | ||
val provider = SSLContextFactory.refreshingSSLEngineProvider(10.millis) { () => | ||
probe.ref ! "Constructed" | ||
SSLContextFactory.createSSLContextFromPem( | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.crt"), | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.key"), | ||
Seq(Path.of("akka-http-tests/src/test/resources/certs/exampleca.crt")) | ||
) | ||
} | ||
val context1 = provider() | ||
probe.expectMsg("Constructed") | ||
Thread.sleep(20) | ||
val context2 = provider() | ||
probe.expectMsg("Constructed") | ||
context1 shouldNot be theSameInstanceAs (context2) | ||
} | ||
|
||
"Work for usage when running HTTPS server" in { | ||
val https = ConnectionContext.httpsServer(SSLContextFactory.refreshingSSLEngineProvider(50.millis) { () => | ||
SSLContextFactory.createSSLContextFromPem( | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.crt"), | ||
Path.of("akka-http-tests/src/test/resources/certs/example.com.key"), | ||
Seq(Path.of("akka-http-tests/src/test/resources/certs/exampleca.crt")) | ||
) | ||
}) | ||
import akka.http.scaladsl.server.Directives._ | ||
val binding = Http(system).newServerAt("127.0.0.1", 8243).enableHttps(https).bind(get { | ||
complete("OK") | ||
}).futureValue | ||
|
||
try { | ||
def requestAndCheckResponse(): Unit = { | ||
val response = Http(system).singleRequest( | ||
HttpRequest(uri = "https://example.com:8243/"), | ||
clientHttpsConnectionContext(), | ||
// fake what server we are talking to for the cert validation | ||
ConnectionPoolSettings(system).withUpdatedConnectionSettings(_.withTransport(ClientTransport.withCustomResolver { | ||
case ("example.com", port) => Future.successful(new InetSocketAddress("127.0.0.1", port)) | ||
})) | ||
).futureValue | ||
|
||
response.status should ===(StatusCodes.OK) | ||
response.entity.discardBytes() | ||
} | ||
|
||
requestAndCheckResponse() | ||
Thread.sleep(100) | ||
requestAndCheckResponse() | ||
} finally { | ||
binding.unbind().futureValue | ||
} | ||
} | ||
} | ||
|
||
private def clientHttpsConnectionContext(): HttpsConnectionContext = { | ||
|
||
val ks: KeyStore = KeyStore.getInstance("JKS") | ||
val keystoreStream: InputStream = getClass.getClassLoader.getResourceAsStream("certs/exampletrust.jks") | ||
if (keystoreStream == null) fail("exampletrust.jkd could not be found") | ||
ks.load(keystoreStream, "changeit".toCharArray) | ||
|
||
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509") | ||
tmf.init(ks) | ||
|
||
val sslContext: SSLContext = SSLContext.getInstance("TLS") | ||
sslContext.init(null, tmf.getTrustManagers, new SecureRandom) | ||
ConnectionContext.httpsClient(sslContext) | ||
} | ||
|
||
} |
126 changes: 126 additions & 0 deletions
126
akka-http/src/main/scala/akka/http/javadsl/common/SSLContextFactory.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* Copyright (C) 2009-2023 Lightbend Inc. <https://www.lightbend.com> | ||
*/ | ||
|
||
package akka.http.javadsl.common | ||
|
||
import akka.annotation.ApiMayChange | ||
import akka.http.scaladsl.common.SSLContextFactory.createSSLContextFromPem | ||
|
||
import java.nio.file.Path | ||
import javax.net.ssl.SSLContext | ||
import java.util.{ List => JList } | ||
import akka.http.scaladsl.common.{ SSLContextFactory => ScalaSSLContextFactory } | ||
import akka.util.JavaDurationConverters.JavaDurationOps | ||
import com.typesafe.config.Config | ||
|
||
import java.security.SecureRandom | ||
import java.time.Duration | ||
import java.util.Optional | ||
import javax.net.ssl.SSLEngine | ||
import scala.jdk.CollectionConverters._ | ||
import scala.jdk.OptionConverters.RichOptional | ||
|
||
object SSLContextFactory { | ||
|
||
/** | ||
* Convenience factory for constructing an SSLContext out of a certificate file, a private key file and zero or more | ||
* CA-certificate files defined in config. | ||
* | ||
* The provided `Config` is required to have the field `certificate` containing | ||
* a path to a certificate file, `private-key` containing the path to a private key, and the key `trusted-ca-certificates` | ||
* either with the value "system" to use the default JDK truststore or containing a list of zero to many paths to CA certificate files | ||
* to explicitly list what CA certs to trust. All files must contain PEM encoded certificates or keys. | ||
* | ||
* Note that the paths are filesystem paths, not class path, | ||
* certificate files packaged in the JAR cannot be loaded using this method. | ||
* | ||
* Example usage: `createSSLContextFromPem(system.settings.config.getConfig("my-server"))` | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def createSSLContextFromPem(config: Config): SSLContext = ScalaSSLContextFactory.createSSLContextFromPem(config) | ||
|
||
/** | ||
* Convenience factory for constructing an SSLContext out of a certificate file, a private key file but use the | ||
* default JDK trust store. All files must contain PEM encoded certificates or keys. | ||
* | ||
* Note that the paths are filesystem paths, not class path, | ||
* certificate files packaged in the JAR cannot be loaded using this method. | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def createSSLContextFromPem( | ||
certificatePath: Path, | ||
privateKeyPath: Path): SSLContext = ScalaSSLContextFactory.createSSLContextFromPem(certificatePath, privateKeyPath) | ||
|
||
/** | ||
* Convenience factory for constructing an SSLContext out of a certificate file, a private key file and zero or more | ||
* CA-certificate files. All files must contain PEM encoded certificates or keys. | ||
* | ||
* Note that the paths are filesystem paths, not class path, | ||
* certificate files packaged in the JAR cannot be loaded using this method. | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def createSSLContextFromPem( | ||
certificatePath: Path, | ||
privateKeyPath: Path, | ||
trustedCaCertificatePaths: JList[Path]): SSLContext = | ||
ScalaSSLContextFactory.createSSLContextFromPem(certificatePath, privateKeyPath, trustedCaCertificatePaths.asScala.toVector) | ||
|
||
/** | ||
* Convenience factory for constructing an SSLContext out of a certificate file, a private key file and possibly zero or more | ||
* CA-certificate files to trust. All files must contain PEM encoded certificates or keys. | ||
* | ||
* Note that the paths are filesystem paths, not class path, | ||
* certificate files packaged in the JAR cannot be loaded using this method. | ||
* | ||
* @param certificatePath Path to a PEM encoded certificate file | ||
* @param privateKeyPath Path to a PEM encoded key file | ||
* @param trustedCaCertificatePaths empty `Optional` to use the default system trust store, or `Optional` with containing a list of | ||
* one or more CA certificate paths to explicitly control exactly what CAs are trusted | ||
* @param secureRandom a secure random to use for the SSL context or none to use a default instance | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def createSSLContextFromPem( | ||
certificatePath: Path, | ||
privateKeyPath: Path, | ||
trustedCaCertificatePaths: Optional[Seq[Path]], | ||
secureRandom: Optional[SecureRandom]): SSLContext = | ||
ScalaSSLContextFactory.createSSLContextFromPem(certificatePath, privateKeyPath, trustedCaCertificatePaths.toScala.map(_.toVector), secureRandom.toScala) | ||
|
||
/** | ||
* Keeps a created SSLContext around for a `refreshAfter` period, sharing it among connections, then creates a new | ||
* context. Useful for rotating certificates. | ||
* | ||
* @param refreshAfter Keep a created context around this long, then recreate it | ||
* @param construct A factory method to create the context when recreating is needed | ||
* @return An SSLEngine provider function to use with Akka HTTP `ConnectionContext.httpsServer()` and `ConnectionContext.httpsClient`. | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def refreshingSSLEngineProvider(refreshAfter: Duration)(construct: akka.japi.function.Creator[SSLContext]): akka.japi.function.Creator[SSLEngine] = | ||
() => ScalaSSLContextFactory.refreshingSSLEngineProvider(refreshAfter.asScala)(construct.create _)() | ||
|
||
/** | ||
* Keeps a created SSLContext around for a `refreshAfter` period, sharing it among connections, then creates a new | ||
* context. Actually constructing the `SSLEngine` is left to caller, to allow additional customization of the `SSLEngine`, | ||
* for example to require client certificates in a server application. | ||
* | ||
* @param refreshAfter Keep a created context around this long, then recreate it | ||
* @param construct A factory method to create the context when recreating is needed | ||
* @return An SSLEngine provider function to use with Akka HTTP `ConnectionContext.httpsServer()` and `ConnectionContext.httpsClient`. | ||
* | ||
* API May Change | ||
*/ | ||
@ApiMayChange | ||
def refreshingSSLContextProvider(refreshAfter: Duration)(construct: akka.japi.function.Creator[SSLContext]): akka.japi.function.Creator[SSLContext] = | ||
() => ScalaSSLContextFactory.refreshingSSLContextProvider(refreshAfter.asScala)(construct.create _)() | ||
} |
Oops, something went wrong.