Skip to content

Commit 37fd987

Browse files
committed
tst
1 parent c243301 commit 37fd987

File tree

3 files changed

+191
-52
lines changed

3 files changed

+191
-52
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
267267
val rtsTrustChain = TrustChainUtil.getTrustChain(qUri)
268268
val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply {
269269
writeText(
270-
buildList {
271-
rtsTrustChain.forEach {
272-
add("-----BEGIN CERTIFICATE-----")
273-
add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded))
274-
add("-----END CERTIFICATE-----")
275-
}
276-
}.joinToString(separator = System.lineSeparator())
270+
TrustChainUtil.certsToPem(rtsTrustChain)
277271
)
278272
}
279273

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import org.apache.http.conn.ssl.DefaultHostnameVerifier
1111
import org.apache.http.impl.client.HttpClientBuilder
1212
import org.apache.http.impl.client.SystemDefaultCredentialsProvider
1313
import org.apache.http.impl.conn.SystemDefaultRoutePlanner
14+
import org.jetbrains.annotations.TestOnly
1415
import software.aws.toolkits.core.utils.getLogger
1516
import software.aws.toolkits.core.utils.warn
17+
import software.aws.toolkits.core.utils.writeText
1618
import java.net.URI
1719
import java.security.KeyStore
1820
import java.security.cert.CertPathBuilder
@@ -23,46 +25,57 @@ import java.security.cert.PKIXBuilderParameters
2325
import java.security.cert.PKIXCertPathBuilderResult
2426
import java.security.cert.X509CertSelector
2527
import java.security.cert.X509Certificate
28+
import java.util.Base64
2629
import kotlin.collections.ifEmpty
2730

2831
object TrustChainUtil {
2932
private val LOG = getLogger<TrustChainUtil>()
3033

34+
@TestOnly
35+
fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: Collection<X509Certificate>) = resolveTrustChain(certs, keystoreFromCertificates(trustAnchors))
36+
3137
/**
3238
* Build and validate the complete certificate chain
3339
* @param certs The end-entity certificate
3440
* @param trustAnchors The truststore containing trusted CA certificates
3541
* @return The complete certificate chain
3642
*/
3743
fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: KeyStore): List<X509Certificate> {
38-
// Create the selector for the certificate
39-
val selector = X509CertSelector()
40-
selector.certificate = certs.first()
41-
42-
// Create the parameters for path validation
43-
val pkixParams = PKIXBuilderParameters(trustAnchors, selector)
44-
45-
// Disable CRL checking since we just want to build the path
46-
pkixParams.isRevocationEnabled = false
47-
48-
// Create a CertStore containing the certificate we want to validate
49-
val ccsp = CollectionCertStoreParameters(certs)
50-
val certStore = CertStore.getInstance("Collection", ccsp)
51-
pkixParams.addCertStore(certStore)
52-
53-
// Get the certification path
54-
val builder = CertPathBuilder.getInstance("PKIX")
55-
val result = builder.build(pkixParams) as PKIXCertPathBuilderResult
56-
val certPath = result.certPath
57-
val chain = (certPath.certificates as List<X509Certificate>).toMutableList()
58-
59-
// Add the trust anchor (root CA) to complete the chain
60-
val trustAnchorCert = result.trustAnchor.trustedCert
61-
if (trustAnchorCert != null) {
62-
chain.add(trustAnchorCert)
63-
}
44+
try {
45+
// Create the selector for the certificate
46+
val selector = X509CertSelector()
47+
selector.certificate = certs.first()
48+
49+
// Create the parameters for path validation
50+
val pkixParams = PKIXBuilderParameters(trustAnchors, selector)
51+
52+
// Disable CRL checking since we just want to build the path
53+
pkixParams.isRevocationEnabled = false
54+
55+
// Create a CertStore containing the certificate we want to validate
56+
val ccsp = CollectionCertStoreParameters(certs)
57+
val certStore = CertStore.getInstance("Collection", ccsp)
58+
pkixParams.addCertStore(certStore)
59+
60+
// Get the certification path
61+
val builder = CertPathBuilder.getInstance("PKIX")
62+
val result = builder.build(pkixParams) as PKIXCertPathBuilderResult
63+
val certPath = result.certPath
64+
val chain = (certPath.certificates as List<X509Certificate>).toMutableList()
65+
66+
// Add the trust anchor (root CA) to complete the chain
67+
val trustAnchorCert = result.trustAnchor.trustedCert
68+
if (trustAnchorCert != null) {
69+
chain.add(trustAnchorCert)
70+
}
6471

65-
return chain
72+
return chain
73+
} catch (e: Exception) {
74+
// Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS
75+
LOG.warn(e) { "Could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" }
76+
77+
return emptyList()
78+
}
6679
}
6780

6881
fun getTrustChain(uri: URI): List<X509Certificate> {
@@ -81,7 +94,7 @@ object TrustChainUtil {
8194
.setSSLContext(CertificateManager.getInstance().sslContext)
8295

8396
// client request will fail if user did not accept cert
84-
client.build().execute(RequestBuilder.options(uri).build())
97+
client.build().use { it.execute(RequestBuilder.options(uri).build()) }
8598

8699
val certificates = peerCerts as Array<X509Certificate>
87100

@@ -95,7 +108,7 @@ object TrustChainUtil {
95108
resolveTrustChain(certificates.toList(), ks)
96109
} catch (e: Exception) {
97110
// Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS
98-
LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" }
111+
LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not root" }
99112
emptyList()
100113
}
101114

@@ -106,6 +119,15 @@ object TrustChainUtil {
106119
}
107120
}
108121

122+
fun certsToPem(certs: List<X509Certificate>): String =
123+
buildList {
124+
certs.forEach {
125+
add("-----BEGIN CERTIFICATE-----")
126+
add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded))
127+
add("-----END CERTIFICATE-----")
128+
}
129+
}.joinToString(separator = System.lineSeparator())
130+
109131
private fun keystoreFromCertificates(certificates: Collection<X509Certificate>): KeyStore {
110132
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
111133
ks.load(null, null)

plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt

Lines changed: 139 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.github.tomakehurst.wiremock.WireMockServer
77
import com.github.tomakehurst.wiremock.common.Slf4jNotifier
88
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
99
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
10+
import com.intellij.execution.configurations.GeneralCommandLine
11+
import com.intellij.execution.util.ExecUtil
1012
import com.intellij.testFramework.ApplicationExtension
1113
import com.intellij.util.net.ssl.CertificateManager
1214
import org.assertj.core.api.Assertions.assertThat
@@ -26,9 +28,11 @@ import org.bouncycastle.asn1.x509.KeyUsage
2628
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
2729
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
2830
import 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
3134
import software.aws.toolkits.core.utils.outputStream
35+
import software.aws.toolkits.core.utils.writeText
3236
import java.nio.file.Files
3337
import java.nio.file.Path
3438
import java.security.KeyPair
@@ -38,12 +42,15 @@ import java.time.temporal.ChronoUnit
3842
import 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

Comments
 (0)