Improve handling of CA certificates (#518)

Instead of bundling Pkl's built-in CA certificates as a class path resource and loading them at runtime,
pass them to the native image compiler as the default SSL context's trust store.
This results in faster SSL initialization and is more consistent with how default certificates
are handled when running on the JVM.

Further related improvements:
- Remove HttpClientBuilder methods `addDefaultCliCertificates` and `addBuiltInCertificates`.
- Remove pkl-certs subproject and the optional dependencies on it.
- Move `PklCARoots.pem` to `pkl-cli/src/certs`.
- Fix certificate related error messages that were missing an argument.
- Prevent PklBugException if initialization of `CliBaseOptions.httpClient` fails.
- Add ability to set CA certificates as a byte array
- Add CA certificates option to message passing API
This commit is contained in:
Daniel Chao
2024-06-12 17:53:03 -07:00
committed by GitHub
parent d7a1778199
commit 919de4838c
28 changed files with 240 additions and 275 deletions
@@ -12,9 +12,8 @@ import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Path
import java.time.Duration
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.createFile
import kotlin.io.path.readBytes
class HttpClientTest {
@Test
@@ -52,14 +51,21 @@ class HttpClientTest {
}
@Test
fun `can load certificates from file system`() {
fun `can load certificates from regular file`() {
assertDoesNotThrow {
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
}
}
@Test
fun `certificate file located on file system cannot be empty`(@TempDir tempDir: Path) {
fun `can load certificates from a byte array`() {
assertDoesNotThrow {
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate.readBytes()).build()
}
}
@Test
fun `certificate file cannot be empty`(@TempDir tempDir: Path) {
val file = tempDir.resolve("certs.pem").createFile()
val e = assertThrows<HttpClientInitException> {
@@ -69,56 +75,10 @@ class HttpClientTest {
assertThat(e).hasMessageContaining("empty")
}
@Test
fun `can load certificates from class path`() {
assertDoesNotThrow {
HttpClient.builder().addCertificates(javaClass.getResource("/org/pkl/certs/PklCARoots.pem")!!.toURI()).build()
}
}
@Test
fun `only allows loading jar and file certificate URIs`() {
assertThrows<HttpClientInitException> {
HttpClient.builder().addCertificates(URI("https://example.com"))
}
}
@Test
fun `certificate file located on class path cannot be empty`() {
val uri = javaClass.getResource("emptyCerts.pem")!!.toURI()
val e = assertThrows<HttpClientInitException> {
HttpClient.builder().addCertificates(uri).build()
}
assertThat(e).hasMessageContaining("empty")
}
@Test
fun `can load built-in certificates`() {
assertDoesNotThrow {
HttpClient.builder().addBuiltInCertificates().build()
}
}
@Test
fun `can load certificates from Pkl user home cacerts directory`(@TempDir tempDir: Path) {
val certsDir = tempDir.resolve(".pkl")
.resolve("cacerts")
.createDirectories()
.also { dir ->
FileTestUtils.selfSignedCertificate.copyTo(dir.resolve("certs.pem"))
}
assertDoesNotThrow {
HttpClientBuilder(certsDir).addDefaultCliCertificates().build()
}
}
@Test
fun `loading certificates from cacerts directory falls back to built-in certificates`(@TempDir certsDir: Path) {
assertDoesNotThrow {
HttpClientBuilder(certsDir).addDefaultCliCertificates().build()
HttpClient.builder().build()
}
}
@@ -3,15 +3,20 @@ package org.pkl.core.http
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.createTempFile
import org.pkl.commons.writeString
import java.net.URI
import java.net.http.HttpRequest
import java.net.http.HttpResponse.BodyHandlers
import java.nio.file.Path
class LazyHttpClientTest {
@Test
fun `builds underlying client on first send`() {
fun `builds underlying client on first send`(@TempDir tempDir: Path) {
val certFile = tempDir.resolve("cert.pem").apply { writeString("broken") }
val client = HttpClient.builder()
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
.addCertificates(certFile)
.buildLazily()
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
@@ -21,9 +26,10 @@ class LazyHttpClientTest {
}
@Test
fun `does not build underlying client unnecessarily`() {
fun `does not build underlying client unnecessarily`(@TempDir tempDir: Path) {
val certFile = tempDir.createTempFile().apply { writeString("broken") }
val client = HttpClient.builder()
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
.addCertificates(certFile)
.buildLazily()
assertDoesNotThrow {
@@ -13,7 +13,6 @@ import org.pkl.core.SecurityManagers
import org.pkl.core.packages.PackageResolver
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import java.nio.file.Path
class ProjectDependenciesResolverTest {
companion object {
@@ -1 +0,0 @@
broken