@@ -7,6 +7,8 @@ import com.github.tomakehurst.wiremock.WireMockServer
77import com.github.tomakehurst.wiremock.common.Slf4jNotifier
88import com.github.tomakehurst.wiremock.core.WireMockConfiguration
99import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
10+ import com.intellij.execution.configurations.GeneralCommandLine
11+ import com.intellij.execution.util.ExecUtil
1012import com.intellij.testFramework.ApplicationExtension
1113import com.intellij.util.net.ssl.CertificateManager
1214import org.assertj.core.api.Assertions.assertThat
@@ -26,9 +28,11 @@ import org.bouncycastle.asn1.x509.KeyUsage
2628import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
2729import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
2830import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
29- import org.junit.jupiter.api.extension.AfterAllCallback
30- import org.junit.jupiter.api.extension.ExtensionContext
31+ import org.junit.jupiter.api.AfterEach
32+ import org.junit.jupiter.api.BeforeEach
33+ import org.junit.jupiter.api.TestInstance
3134import software.aws.toolkits.core.utils.outputStream
35+ import software.aws.toolkits.core.utils.writeText
3236import java.nio.file.Files
3337import java.nio.file.Path
3438import java.security.KeyPair
@@ -38,12 +42,15 @@ import java.time.temporal.ChronoUnit
3842import java.util.Date
3943
4044@ExtendWith(ApplicationExtension ::class )
41- class TrustChainUtilTest : AfterAllCallback {
45+ @TestInstance(TestInstance .Lifecycle .PER_CLASS )
46+ class TrustChainUtilTest {
4247 companion object {
4348 private val certs = CertificateGenerator .generateCertificateChain()
4449 }
4550
46- override fun afterAll (context : ExtensionContext ) {
51+ @BeforeEach
52+ @AfterEach
53+ fun clearCerts () {
4754 CertificateManager .getInstance().customTrustManager.apply {
4855 certificates.toList().forEach { removeCertificate(it) }
4956 }
@@ -77,7 +84,7 @@ class TrustChainUtilTest : AfterAllCallback {
7784 }
7885
7986 @Test
80- fun `returns entire chain if CA is trusted ` () {
87+ fun `returns entire chain if CA is trust anchor ` () {
8188 CertificateManager .getInstance().customTrustManager.addCertificate(certs.keys.last())
8289
8390 mockWithOptions(
@@ -97,16 +104,43 @@ class TrustChainUtilTest : AfterAllCallback {
97104 }
98105 ) {
99106 val trustChain = TrustChainUtil .getTrustChain(URI (" https://localhost:${it.httpsPort()} " ))
100- // leaf, intermediate, root
107+ // leaf, intermediate
101108 assertThat(trustChain)
102109 .isEqualTo(certs.keys.toList())
103110 }
104111 }
105112
106113 @Test
107- fun `returns entire chain if CA is trusted but only returns leaf` () {
108- CertificateManager .getInstance().customTrustManager.addCertificate(certs.keys.last())
114+ fun `returns empty if CA is trusted but does not provide intermediate` () {
115+ val (leaf, _, root) = certs.keys.take(3 )
116+ assertThat(
117+ TrustChainUtil .resolveTrustChain(listOf (leaf), listOf (root))
118+ ).isEmpty()
119+ }
120+
121+ @Test
122+ fun `returns entire chain if CA is trusted and provides intermediate` () {
123+ val (leaf, intermediate, root) = certs.keys.take(3 )
124+ assertThat(
125+ TrustChainUtil .resolveTrustChain(listOf (leaf, intermediate), listOf (root))
126+ ).isEqualTo(
127+ listOf (leaf, intermediate, root)
128+ )
129+ }
109130
131+ @Test
132+ fun `returns empty if CA is not trusted` () {
133+ val (leaf, intermediate) = certs.keys.take(2 )
134+ assertThat(
135+ TrustChainUtil .resolveTrustChain(
136+ listOf (leaf, intermediate),
137+ listOf (CertificateManager .getInstance().trustManager.acceptedIssuers.first())
138+ )
139+ ).isEmpty()
140+ }
141+
142+ @Test
143+ fun `node accepts full chain` () {
110144 mockWithOptions(
111145 {
112146 it.keystorePath(
@@ -116,20 +150,108 @@ class TrustChainUtilTest : AfterAllCallback {
116150 CertificateGenerator .saveToKeyStore(
117151 this ,
118152 certs.values.first(),
119- certs.keys.take(1 ).toTypedArray(),
153+ certs.keys.take(2 ).toTypedArray(),
120154 )
121155 }
122156 .toString()
123157 )
158+ },
159+ {
160+ val pemFile = Files .createTempFile(" test" , " .pem" ).apply {
161+ writeText(
162+ TrustChainUtil .certsToPem(certs.keys.toList())
163+ )
164+ }
165+
166+ val output = ExecUtil .execAndGetOutput(
167+ GeneralCommandLine (
168+ " node" ,
169+ " --use-bundled-ca" ,
170+ Files .createTempFile(" test" , " .js" ).apply { writeText(nodeTest(it.httpsPort())) }.toAbsolutePath().toString(),
171+ ).withEnvironment(" NODE_EXTRA_CA_CERTS" , pemFile.toAbsolutePath().toString())
172+ )
173+
174+ assertThat(output.exitCode).withFailMessage { " node validation failed: ${output.stdout} \n ${output.stderr} " }
175+ .isEqualTo(0 )
124176 }
125- ) {
126- val trustChain = TrustChainUtil .getTrustChain(URI (" https://localhost:${it.httpsPort()} " ))
127- // leaf, intermediate, root
128- assertThat(trustChain)
129- .isEqualTo(certs.keys.toList())
130- }
177+ )
178+ }
179+
180+ @Test
181+ fun `node does not accept intermediate only` () {
182+ mockWithOptions(
183+ {
184+ it.keystorePath(
185+ Files .createTempFile(" certs" , " jks" )
186+ .toAbsolutePath()
187+ .apply {
188+ CertificateGenerator .saveToKeyStore(
189+ this ,
190+ certs.values.first(),
191+ certs.keys.take(2 ).toTypedArray(),
192+ )
193+ }
194+ .toString()
195+ )
196+ },
197+ {
198+ val pemFile = Files .createTempFile(" test" , " .pem" ).apply {
199+ writeText(
200+ TrustChainUtil .certsToPem(certs.keys.take(2 ).toList())
201+ )
202+ }
203+
204+ // node does not support partial chains
205+ val output = ExecUtil .execAndGetOutput(
206+ GeneralCommandLine (
207+ " node" ,
208+ " --use-bundled-ca" ,
209+ Files .createTempFile(" test" , " .js" ).apply { writeText(nodeTest(it.httpsPort())) }.toAbsolutePath().toString(),
210+ ).withEnvironment(" NODE_EXTRA_CA_CERTS" , pemFile.toAbsolutePath().toString())
211+ )
212+
213+ assertThat(output.exitCode).withFailMessage { " node validation succeeded instead of failed: ${output.stdout} \n ${output.stderr} " }
214+ .isEqualTo(1 )
215+ }
216+ )
131217 }
132218
219+ // language=JavaScript
220+ private fun nodeTest (port : Int ) = """
221+ const https = require("https");
222+
223+ async function main() { // Wrapped in async function for better error handling
224+ try {
225+ const options = {
226+ host: "localhost",
227+ port: $port ,
228+ path: "/",
229+ requestCert: true,
230+ rejectUnauthorized: true,
231+ };
232+
233+ const req = https.get(options, (res) => {
234+ console.log("Certificate authorized:", res.socket.authorized);
235+ const cert = res.socket.getPeerCertificate();
236+ console.log("Certificate details:", cert);
237+ process.exit(0)
238+ });
239+
240+ req.on("error", (err) => { // Added error handling
241+ console.error("Request error:", err);
242+ process.exit(1)
243+ });
244+
245+ req.end();
246+ } catch (error) {
247+ console.error("Error:", error);
248+ process.exit(1)
249+ }
250+ }
251+
252+ main();
253+ """ .trimIndent()
254+
133255 private fun mockWithOptions (options : (WireMockConfiguration ) -> Unit , runnable : (WireMockServer ) -> Unit ) {
134256 val server = WireMockServer (
135257 wireMockConfig()
@@ -251,7 +373,8 @@ class CertificateGenerator {
251373 addExtension(
252374 Extension .basicConstraints,
253375 true ,
254- BasicConstraints (true )
376+ // not allowed to issue sub-CA
377+ BasicConstraints (0 )
255378 )
256379 addExtension(
257380 Extension .keyUsage,
0 commit comments